I have a decorator called Special
that turns a function into two versions of itself: one that can be called directly and prefixes the result with 'regular '
and one that can be called with .special
and prefixes the results with 'special '
:
class Special:
def __init__(self, func):
self.func = func
def __get__(self, instance, owner=None):
if instance is None:
return self
return Special(self.func.__get__(instance, owner))
def special(self, *args, **kwargs):
return 'special ' + self.func(*args, **kwargs)
def __call__(self, *args, **kwargs):
return 'regular ' + self.func(*args, **kwargs)
It works fine with regular methods and static methods - but .special
does not work with class methods:
class Foo:
@Special
def bar(self):
return 'bar'
@staticmethod
@Special
def baz():
return 'baz'
@classmethod
@Special
def qux(cls):
return 'qux'
assert Foo().bar() == 'regular bar'
assert Foo().bar.special() == 'special bar'
assert Foo.baz() == 'regular baz'
assert Foo.baz.special() == 'special baz'
assert Foo.qux() == 'regular qux'
assert Foo.qux.special() == 'special qux' # TypeError: qux() missing 1 required positional argument: 'cls'
Foo().bar
is invoking __get__
, which binds the underlying function and passes the bound method to a new instance of Special
- which is why both Foo().bar()
and Foo().bar.special()
work.
Foo.baz
is simply returning the original Special
instance - where both the regular and the special calls are simple.
Foo.qux
is binding without calling my __get__
.
Foo.qux()
works.Foo.qux.special
is simply calling the .special
of the underlying function (classmethod
does not know how to bind it) - so Foo.qux.special()
is invoking an unbound function, hence the TypeError
.Is there any way for Foo.qux.special
to know it's being called from a classmethod
? Or some other way around this problem?
The problem is that classmethod.__get__
doesn't call the wrapped function's __get__
—because that's basically the whole point of @classmethod
. You can look at the pure-Python equivalent to classmethod
in the Descriptors HOWTO, or the actual CPython C source in funcobject.c
, for details, but you'll see that there's really no way around this.
Of course if you just @Special
a @classmethod
, instead of the other way around, everything will work fine when called on an instance:
class Foo:
@Special
@classmethod
def spam(cls):
return 'spam'
assert Foo().spam() == 'regular spam'
assert Foo().spam.special() == 'special spam'
… but now it won't work when called on the class:
assert Foo.spam() == 'regular spam'
assert Foo.spam.special() == 'special spam'
… because you're trying to call a classmethod
object, which isn't callable.
But this problem, unlike the previous one, is fixable. In fact, the only reason this fails is this part:
if instance is None:
return self
When you try to bind a Special
instance to a class, it just returns self
instead of binding its wrapped object. Which means it ends up as just a wrapper around a classmethod
object rather than a wrapper around a bound class method, and of course you can't call a classmethod
object.
But if you just leave that out, it'll let the underlying classmethod
bind the same way a normal function does, which does exactly the right thing, and now everything works:
class Special:
def __init__(self, func):
self.func = func
def __get__(self, instance, owner=None):
return Special(self.func.__get__(instance, owner))
def special(self, *args, **kwargs):
return 'special ' + self.func(*args, **kwargs)
def __call__(self, *args, **kwargs):
return 'regular ' + self.func(*args, **kwargs)
class Foo:
@Special
def bar(self):
return 'bar'
@Special
@staticmethod
def baz():
return 'baz'
@Special
@classmethod
def qux(cls):
return 'qux'
assert Foo().bar() == 'regular bar'
assert Foo().bar.special() == 'special bar'
assert Foo.baz() == 'regular baz'
assert Foo.baz.special() == 'special baz'
assert Foo().baz() == 'regular baz'
assert Foo().baz.special() == 'special baz'
assert Foo.qux() == 'regular qux'
assert Foo.qux.special() == 'special qux'
assert Foo().qux() == 'regular qux'
assert Foo().qux.special() == 'special qux'
Of course this will cause problems with wrapping unbound method objects in Python 2.7, but I think your design already breaks for normal methods in 2.7, and hopefully you only care about 3.x here anyway.
classmethod
is a descriptor that returns a bound method. It doesn't invoke your __get__
method in this process because it can't do so without breaking some contracts of the descriptor protocol. (Namely, the fact that instance
should be an instance, not a class.) So your __get__
method not being called is completely expected.
So how do you make it work? Well, think about it: You want both some_instance.bar
and SomeClass.bar
to return a Special
instance. In order to achieve that, you simply apply the @Special
decorator last:
class Foo:
@Special
@staticmethod
def baz():
return 'baz'
@Special
@classmethod
def qux(cls):
return 'qux'
This gives you full control over if/when/how the decorated function's descriptor protocol is invoked. Now you just need to remove the if instance is None:
special case in your __get__
method, because it prevents classmethods from working correctly. (The reason is that classmethod objects are not callable; you have to invoke the descriptor protocol to turn the classmethod object into a function that can be called.) In other words, the Special.__get__
method has to unconditionally call the decorated function's __get__
method, like this:
def __get__(self, instance=None, owner=None):
return Special(self.func.__get__(instance, owner))
And now all your assertions will pass.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With