Reputation: 7131
Suppose we have a class we want to monkeypatch and some callables we want to monkeypatch onto it.
class Foo:
pass
def bar(*args):
print(list(map(type, args)))
class Baz:
def __call__(*args):
print(list(map(type, args)))
baz = Baz()
def wrapped_baz(*args):
return baz(*args)
Foo.bar = bar
Foo.baz = baz
Foo.biz = wrapped_baz
Foo().bar() # [<class '__main__.Foo'>]
Foo().baz() # [<class '__main__.Baz'>]
Foo().biz() # [<class '__main__.Baz'>, <class '__main__.Foo'>]
Even though baz
is a callable, it doesn't get bound to a Foo()
instance like the two functions bar
and wrapped_baz
. Since Python is a duck-typed language, it seems strange that the type of a given callable would play so heavily in the behavior of the object machinery.
Not that wrapping callables is necessarily a bad approach, are there other ways to have callables bound appropriately to Foo
instances? Is this a quirk of the CPython implementation, or is there a portion of the language spec that describes the observed behavior?
Upvotes: 5
Views: 114
Reputation: 152725
The reason for the difference is that functions implement the descriptor protocol, but your callable class does not. The descriptor protocol is part of the language specification.
When you look up an attribute on an instance or class it will check if the attribute on the class is a descriptor, i.e. if it has __get__
, __set__
, or __delete__
. If it's a descriptor then attribute-lookup (getting, setting, and deleting) will go through these methods. If you want to know more about how descriptors work, you can check the official Python documentation or other answers here on StackOverflow, for example "Understanding __get__
and __set__
and Python descriptors".
Functions have a __get__
and thus if you look them up, they return a bound method
. A bound method is a function where the instance is passed as first argument. I'm not sure this is part of the language specification (it probably is, but I couldn't find a reference).
So your bar
and wrapped_baz
functions are descriptors, but your Baz
class isn't. So the bar
(and the wrapped_baz
) function will be looked up as "bound method" where the instance is implicitly passed into the arguments when called. However the baz
instance is returned as-is, so there's no implicit argument when called.
Baz
class more method-likeDepending on what you want, you can make your Baz
act like a method by implementing a __get__
:
import types
# ...
class Baz:
def __get__(self, instance, cls):
"""Makes Baz a descriptor and when looked up on an instance returns a
"bound baz" similar to normal methods."""
if instance is None:
return self
return types.MethodType(self, instance)
def __call__(*args):
print(list(map(type, args)))
# ...
Foo().baz() # [<class '__main__.Baz'>, <class '__main__.Foo'>]
wrapped_baz
less method-likeOr if you don't want the Foo
(similar to your Baz
class) then just wrap the wrapped_baz
as staticmethod
:
# ...
class Baz:
def __call__(*args):
print(list(map(type, args)))
baz = Baz()
@staticmethod
def wrapped_baz(*args):
return baz(*args)
# ...
Foo().biz() # [<class '__main__.Baz'>]
Upvotes: 3