Python method where first argument is either class or self

I want to write some Python code that when called from the context of a type behaves like an @classmethod, however when called on an instance of an object the first argument is behaves as normal, with self referencing the current instance. I.e. I want to write:

class Foo(object):
    @something # <- does this exist? How can I even write it?
    def bar(self_or_class):
        print(repr(self_or_class))

Foo.bar() # Should print: <class '__main__.Foo'>
inst = Foo()
inst.bar() # Should print:  <__main__.Foo at 0x....>

Upvotes: 1

Views: 617

Answers (2)

Gribouillis
Gribouillis

Reputation: 2220

I once wrote a code snippet in daniweb about this

from __future__ import print_function
from functools import partial

class mixedmethod(object):
    """This decorator mutates a function defined in a class into a 'mixed' class and instance method.

    Usage:

        class Spam:

            @mixedmethod
            def egg(self, cls, *args, **kwargs):
                if self is None:
                    pass # executed if egg was called as a class method (eg. Spam.egg())
                else:
                    pass # executed if egg was called as an instance method (eg. instance.egg())
    The decorated methods need 2 implicit arguments: self and cls, the former being None when
    there is no instance in the call. This follows the same rule as __get__ methods in python's
    descriptor protocol.
    """
    def __init__(self, func):
        self.func = func
    def __get__(self, instance, cls):
        return partial(self.func, instance, cls)

if __name__ == '__main__':

    class Spam(object):

        @mixedmethod
        def ham(self, cls, *args):
            if self is None:
                print("Spam.ham() was called as a class method with {0}.".format((self, cls)+ args))
            else:
                print("Spam.ham() was called as an instance method with {0}.".format((self, cls) + args))

        def __repr__(self):
            return '<Spam instance>'
    egg = Spam()
    egg.ham(5)
    Spam.ham(5)

Upvotes: 2

After some research I managed to do this. The first thing I found is that you can actually implement @classmethod in pure Python, as referenced in the documentation.

With that knowledge it's fairly simple to adapt the code to test if obj exists:

class something(object):
    ''' 
    This works like @classmethod, except the first argument is either class or self if it's available
    '''
    def __init__(self, f):
        self.f = f

    def __get__(self, obj, klass=None):
        if klass is None:
            klass = type(obj)
        def newfunc(*args, **kwargs):
            return self.f(klass if obj is None else obj, *args, **kwargs)
        return newfunc

All we have to do over and above the example in the documentation is check if obj is None and favour obj over klass if it is not.

Upvotes: 1

Related Questions