Search code examples
pythondecoratorclass-method

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.


Solution

  • 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()')