Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python: passing functions as arguments to initialize the methods of an object. Pythonic or not?

Tags:

python

oop

I'm wondering if there is an accepted way to pass functions as parameters to objects (i.e. to define methods of that object in the init block).

More specifically, how would one do this if the function depends on the objects parameters.

It seems pythonic enough to pass functions to objects, functions are objects like anything else:

def foo(a,b):
    return a*b

class FooBar(object):
    def __init__(self, func):
        self.func = func

foobar = FooBar(foo)
foobar.func(5,6)

# 30

So that works, the problem shows up as soon as you introduce dependence on the object's other properties.

def foo1(self, b):
    return self.a*b

class FooBar1(object):
    def __init__(self, func, a):
        self.a=a
        self.func=func

# Now, if you try the following:
foobar1 = FooBar1(foo1,4)
foobar1.func(3)
# You'll get the following error:
# TypeError: foo0() missing 1 required positional argument: 'b'

This may simply violate some holy principles of OOP in python, in which case I'll just have to do something else, but it also seems like it might prove useful.

I've though of a few possible ways around this, and I'm wondering which (if any) is considered most acceptable.

Solution 1

foobar1.func(foobar1,3)

# 12
# seems ugly

Solution 2

class FooBar2(object):
    def __init__(self, func, a):
        self.a=a
        self.func = lambda x: func(self, x)

# Actually the same as the above but now the dirty inner-workings are hidden away. 
# This would not translate to functions with multiple arguments unless you do some ugly unpacking.
foobar2 = FooBar2(foo1, 7)
foobar2.func(3)

# 21

Any ideas would be appreciated!

like image 461
Jesse Avatar asked Mar 29 '19 08:03

Jesse


People also ask

Can I pass a function as an argument in Python?

Because functions are objects we can pass them as arguments to other functions. Functions that can accept other functions as arguments are also called higher-order functions.

Which can be passed as an argument to a function?

Arguments are passed by value; that is, when a function is called, the parameter receives a copy of the argument's value, not its address. This rule applies to all scalar values, structures, and unions passed as arguments. Modifying a parameter does not modify the corresponding argument passed by the function call.

How do you pass a function to another function in Python?

A function name can become a variable name (and thus be passed as an argument) by dropping the parentheses. A variable name can become a function name by adding the parentheses. In your example, equate the variable rules to one of your functions, leaving off the parentheses and the mention of the argument.


2 Answers

Passing functions to an object is fine. There's nothing wrong with that design.

If you want to turn that function into a bound method, though, you have to be a little careful. If you do something like self.func = lambda x: func(self, x), you create a reference cycle - self has a reference to self.func, and the lambda stored in self.func has a reference to self. Python's garbage collector does detect reference cycles and cleans them up eventually, but that can sometimes take a long time. I've had reference cycles in my code in the past, and those programs often used upwards of 500 MB memory because python would not garbage collect unneeded objects often enough.

The correct solution is to use the weakref module to create a weak reference to self, for example like this:

import weakref

class WeakMethod:
    def __init__(self, func, instance):
        self.func = func
        self.instance_ref = weakref.ref(instance)

        self.__wrapped__ = func  # this makes things like `inspect.signature` work

    def __call__(self, *args, **kwargs):
        instance = self.instance_ref()
        return self.func(instance, *args, **kwargs)

    def __repr__(self):
        cls_name = type(self).__name__
        return '{}({!r}, {!r})'.format(cls_name, self.func, self.instance_ref())


class FooBar(object):
    def __init__(self, func, a):
        self.a = a
        self.func = WeakMethod(func, self)

f = FooBar(foo1, 7)
print(f.func(3))  # 21

All of the following solutions create a reference cycle and are therefore bad:

  • self.func = MethodType(func, self)
  • self.func = func.__get__(self, type(self))
  • self.func = functools.partial(func, self)
like image 114
Aran-Fey Avatar answered Nov 15 '22 04:11

Aran-Fey


Inspired by this answer, a possible solution could be:

from types import MethodType

class FooBar1(object):
    def __init__(self, func, a):
        self.a=a
        self.func=MethodType(func, self)


def foo1(self, b):
    return self.a*b

def foo2(self, b):
    return 2*self.a*b

foobar1 = FooBar1(foo1,4)
foobar2 = FooBar1(foo2, 4)

print(foobar1.func(3))
# 12

print(foobar2.func(3))
# 24

The documentation on types.MethodType doesn't tell much, however:

types.MethodType

The type of methods of user-defined class instances.

like image 33
Thierry Lathuille Avatar answered Nov 15 '22 05:11

Thierry Lathuille