drhagen
drhagen

Reputation: 9532

Make a method that is both a class method and an instance method

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

Answers (2)

Moses Koledoye
Moses Koledoye

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

Aran-Fey
Aran-Fey

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

Related Questions