The following python code shows two decorators: @decorator
and @broken_decorator
. The second one is broken as it looses the reference to self
. In fact, to use a decorated method with @broken_decorator
one should give the self object as first parameter, eg. writing three.plus_(MyClass(4), 4)
, printing 4 + 4 = 8 even the object to which plus is applied is three
.
from dataclasses import dataclass
from typing import Callable
def decorator(func: Callable[["MyClass", int], None]):
def wrapper(my_class_self: "MyClass", y: int):
func(my_class_self, y)
return wrapper
def broken_decorator(func: Callable[["MyClass", int], None]):
class Wrapper:
def __call__(self: "Wrapper", my_class_self: "MyClass", y: int):
func(my_class_self, y)
return Wrapper()
@dataclass
class MyClass:
x: int
@decorator
def plus(self: "MyClass", y: int) -> None:
print(f"{self.x} + {y} = {self.x + y}")
@broken_decorator
def plus_(self: "MyClass", y: int) -> None:
print(f"{self.x} + {y} = {self.x + y}")
three = MyClass(3)
three.plus(4) # 3 + 4 = 7
# with @broken_decorator
three.plus_(MyClass(4), 4) # 4 + 4 = 8, as self is the new object MyClass(4)
Specifically, the difference between the two decorators is in the returned wrapper, the first one returns a function the second a class equipped with the __call__
method.
Is this behaviour correct?
Yes, it's correct. Plain functions implement the descriptor protocol, which is what allows them to implicitly bind self
as part of the lookup-and-call process. Your class does not implement the descriptor protocol, so it doesn't get this benefit.
You could manually implement __get__
on your class so it also implemented the descriptor protocol appropriately, but unless there's a really good reason to do so, you're usually better off using functions (possibly with persistent state tracked via closure state) since they get all this functionality for free by default (and it's implemented more efficiently than anything you can hand-write in Python to boot). The equivalent implementation even for something as simple as simulating method binding is tricky; you'd need to have __get__
return an instance of a different class that knew both the function to call and the self
to provide, you'd need to make sure __get__
didn't do this when it received None
for the instance (in that case it should return itself unmodified), etc. It's a royal pain, and unless you have a strong motivation to write a custom class for this purpose, closure-scoped functions cover 99% of desired use cases and involve far less rigmarole, so use them.