7

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!

Jesse
  • 860
  • 1
  • 11
  • 13
  • 1
    Better versions of Solution 2 can be found here: [Python Argument Binders](//stackoverflow.com/q/277922) – Aran-Fey Mar 29 '19 at 08:20
  • 2
    The downside of your solutions is, that the code gets harder to understand because you would need to know where the class gets instantiated and what function is passed into it. This makes refactoring and debugging a lot harder. You could take a look at Mixins instead: https://stackoverflow.com/questions/533631/what-is-a-mixin-and-why-are-they-useful Mixins allow you to add optional functionality to classes through multiple inheritance. – dudenr33 Mar 29 '19 at 08:35
  • @Aran-Fey no, these are not versions of Solution 2, because they are about *functions*, while this question is about a bound object *method*. – Yaroslav Nikitenko Jun 19 '22 at 16:03
  • 1
    @YaroslavNikitenko I don't understand. The only method in this question is `__init__`, and it's not bound. OP is merging a callable (`func`) and an argument (`self`) into a callable with 1 fewer argument (`lambda x: func(self, x)`). And that is exactly what the question I linked is about. Where's the problem? – Aran-Fey Jun 22 '22 at 09:37
  • 1
    @Aran-Fey I carefully re-read the question and the link you posted. I can't say what I thought exactly when I commented. Now you seem right. Sorry for a misunderstanding. – Yaroslav Nikitenko Jun 27 '22 at 11:09

2 Answers2

5

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)
Aran-Fey
  • 39,665
  • 11
  • 104
  • 149
  • I'm afraid that you are wrong in """If you do something like `self.func = lambda x: func(self, x)`, you create a reference cycle... the lambda stored in self.func has a reference to `self`.""". lambda expressions create anonymous functions, and user-defined functions don't store their non-default parameters (`dir(f)` for your lambda or read https://docs.python.org/3/reference/datamodel.html#the-standard-type-hierarchy). `self` is not stored in lambda; lambda result is evaluated every time it is run (see https://stackoverflow.com/questions/10452770/python-lambdas-binding-to-local-values). – Yaroslav Nikitenko Jun 19 '22 at 15:06
  • I think this supports what I wrote: "function object contains a reference to the current global namespace as the global namespace to be used when the function is called. The function definition does not execute the function body; this gets executed only when the function is called." https://docs.python.org/3/reference/compound_stmts.html#function-definitions And lambdas are just functions (see the definition at https://docs.python.org/3/reference/expressions.html#lambda). – Yaroslav Nikitenko Jun 19 '22 at 15:43
  • 1
    @YaroslavNikitenko You're technically correct; `self` is not stored in the lambda. `self` becomes a closure variable stored in a so-called "cell" (at least in CPython). But let's not go there, because that's deep in the internals of the interpreter. What matters is that yes, it does create a reference cycle. – Aran-Fey Jun 22 '22 at 09:30
  • Thanks for the explanation. Do you have any source for this information? I think that this may be rather an implementation detail, irrelevant to other interpreters (or may even change between different versions of CPython). – Yaroslav Nikitenko Jun 22 '22 at 23:47
  • @YaroslavNikitenko The important part - the reference cycle - is definitely not an implementation detail. Imagine this: You create a lambda with a nonlocal variable, like `f = lambda: x`. If you call this lambda, is it possible that it will crash because `x` no longer exists? No, of course not. Because the lambda holds a reference to `x`. That's just how python works. – Aran-Fey Jun 23 '22 at 09:34
  • I wrote why your logic is wrong and referenced Python documentation. I just opened the Python interpreter and typed `f = lambda: x`. What happened? Nothing. There is absolutely no problem with such function, because no `x` was really used in its definition, because it creates no references (and cycles). I'm afraid that your sources (that you don't refer to) are wrong. – Yaroslav Nikitenko Jun 23 '22 at 09:56
  • 1
    @YaroslavNikitenko All of the documentation you referenced is - at best - only tangentially related to the topic we're discussing. At this point, it's quite clear that you have a lot of misunderstandings about a lot of things, and this is not the right place for me to try and correct all of them. If you want you can pay a visit to the [python chat room](https://chat.stackoverflow.com/rooms/6), where I and some other folks can explain everything to you. But for now, [here](https://pastebin.com/H4pTzXKu) is a piece of code that proves that the lambda holds a reference to the nonlocal variable. – Aran-Fey Jun 23 '22 at 14:55
  • Yes, this piece of code is clear. Now I can understand what is happening, thank you very much! – Yaroslav Nikitenko Jun 23 '22 at 15:32
  • I'm surprised the best way to do this is to write an entire class `WeakMethod`. – Galen Mar 09 '23 at 02:37
3

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.

Thierry Lathuille
  • 23,663
  • 10
  • 44
  • 50
  • I'm afraid that `types.MethodType` is not intended for creating new objects, because its docs say: "If you instantiate any of these types, note that signatures may vary between Python versions" (and there is no official constructor in the docs). https://docs.python.org/3/library/types.html#standard-interpreter-types – Yaroslav Nikitenko Jun 19 '22 at 15:48