Reputation: 9532
Is there a way in Python to define a method that is both a class method and an instance method, such that both cls
and self
are receivers. In particular, I am looking to make a method that (1) knows which class it was called on when called on the class (Foo.method(value)
) and (2) knows which instance it was called on when called on an instance (foo.method()
).
For example, I would envision something working like this:
class Foo:
def __str__(self): return 'Foo()'
@classinstancemethod
def method(cls, self):
print(cls, self)
class Bar(Foo):
def __str__(self): return 'Bar()'
Foo().method() # <class '__main__.Foo'> Foo()
Bar().method() # <class '__main__.Bar'> Bar()
Foo.method(Foo()) # <class '__main__.Foo'> Foo()
Foo.method(Bar()) # <class '__main__.Foo'> Bar()
Bar.method(Foo()) # <class '__main__.Bar'> Foo()
Note that I am aware that undecorated methods can be called like Foo.foo(value)
, but this is not what I want because it doesn't get the cls
variable. And without the cls
variable, the method has no idea which class it was just called on. It may have been called as Bar.method(value)
, and there would be now way to know that if value
was an instance of Foo
. An undecorated method is more like both a static method and an instance method, not both a class method and an instance method.
Upvotes: 1
Views: 447
Reputation: 78546
You don't need a decorator for this. This is how methods already work; the instance is passed as first argument – implicitly when method is called from instance and explicitly from the class – with the instance available, you can retrieve the class by calling type
on the instance:
class Foo(object):
def __repr__(self): return 'Foo()'
def method(self):
print((type(self), self))
class Bar(Foo):
def __repr__(self): return 'Bar()'
On another note, in the __str__
(or __repr__
) special methods, you should return a string, not print.
I have used __repr__
since __str__
is not called for printing the instance when inside a container object (tuple here).
Update:
Considering the issues with class/instance printing in the above approach, you can use a descriptor instead to properly manage correct class and instance selection within the method:
class classinstancemethod():
def __get__(self, obj, cls):
def method(inst=None):
print(cls, inst if inst else obj)
return method
class Foo(object):
method = classinstancemethod()
def __str__(self): return 'Foo()'
class Bar(Foo):
def __str__(self): return 'Bar()'
Upvotes: 2
Reputation: 43136
This can be solved by implementing classinstancemethod
as a custom descriptor.
In a nutshell, the descriptor must define a __get__
method that will be called when it's accessed as an attribute (like Foo.method
or Foo().method
). This method will be passed the instance and the class as arguments and returns a bound method (i.e. it returns a method with the cls
and self
arguments baked in). When this bound method is called, it forwards the baked-in cls
and self
parameters to the actual method.
class classinstancemethod:
def __init__(self, method, instance=None, owner=None):
self.method = method
self.instance = instance
self.owner = owner
def __get__(self, instance, owner=None):
return type(self)(self.method, instance, owner)
def __call__(self, *args, **kwargs):
instance = self.instance
if instance is None:
if not args:
raise TypeError('missing required parameter "self"')
instance, args = args[0], args[1:]
cls = self.owner
return self.method(cls, instance, *args, **kwargs)
Results:
class Foo:
def __repr__(self): return 'Foo()'
@classinstancemethod
def method(cls, self):
print((cls, self))
class Bar(Foo):
def __repr__(self): return 'Bar()'
Foo().method() # (<class '__main__.Foo'>, 'Foo()')
Bar().method() # (<class '__main__.Bar'>, 'Bar()')
Foo.method(Foo()) # (<class '__main__.Foo'>, 'Foo()')
Foo.method(Bar()) # (<class '__main__.Foo'>, 'Bar()')
Bar.method(Foo()) # (<class '__main__.Bar'>, 'Foo()')
Upvotes: 2