Same name for classmethod and instancemethod?

2019-02-16 23:48发布

问题:

I'd like to do something like this:

class X:

    @classmethod
    def id(cls):
        return cls.__name__

    def id(self):
        return self.__class__.__name__

And now call id() for either the class or an instance of it:

>>> X.id()
'X'
>>> X().id()
'X'

Obviously this exact code doesn't work, but is there a similar way to make it work? Or any other workarounds to get such behavior without too much "hacky" stuff?

回答1:

Class and instance methods live in the same namespace and you cannot reuse names like that; the last definition of id will win in that case.

The class method will continue to work on instances however, there is no need to create a separate instance method; just use:

class X:
    @classmethod
    def id(cls):
        return cls.__name__

because the method continues to be bound to the class:

>>> class X:
...     @classmethod
...     def id(cls):
...         return cls.__name__
... 
>>> X.id()
'X'
>>> X().id()
'X'

This is explicitly documented:

It can be called either on the class (such as C.f()) or on an instance (such as C().f()). The instance is ignored except for its class.



回答2:

No idea what's your actual use case is, but you can do something like this using a descriptor:

class Desc(object):

    def __get__(self, ins, typ):
        if ins is None:
            print 'Called by a class.'
            return lambda : typ.__name__
        else:
            print 'Called by an instance.'
            return lambda : ins.__class__.__name__

class X(object):
    id = Desc()

x = X()
print x.id()
print X.id()

Output:

Called by an instance.
X
Called by a class.
X


回答3:

It can be done, quite succinctly, by binding the instance-bound version of your method explicitly to the instance (rather than to the class). Python will invoke the instance attribute found in Class().__dict__ when Class().foo() is called (because it searches the instance's __dict__ before the class'), and the class-bound method found in Class.__dict__ when Class.foo() is called.

This has a number of potential use-cases, though whether they are anti-patterns is open for debate:

class Test:
    def __init__(self):
        self.check = self.__check

    @staticmethod
    def check():
        print('Called as class')

    def __check(self):
        print('Called as instance, probably')

>>> Test.check()
Called as class
>>> Test().check()
Called as instance, probably

Or... let's say we want to be able to abuse stuff like map():

class Str(str):
    def __init__(self, *args):
        self.split = self.__split

    @staticmethod
    def split(sep=None, maxsplit=-1):
        return lambda string: string.split(sep, maxsplit)

    def __split(self, sep=None, maxsplit=-1):
        return super().split(sep, maxsplit)

>>> s = Str('w-o-w')
>>> s.split('-')
['w', 'o', 'w']
>>> Str.split('-')(s)
['w', 'o', 'w']
>>> list(map(Str.split('-'), [s]*3))
[['w', 'o', 'w'], ['w', 'o', 'w'], ['w', 'o', 'w']]


回答4:

In your example, you could simply delete the second method entirely, since both the staticmethod and the class method do the same thing.

If you wanted them to do different things:

class X:
    def id(self=None):
       if self is None:
           # It's being called as a static method
       else:
           # It's being called as an instance method


回答5:

"types" provides something quite interesting since Python 3.4: DynamicClassAttribute

It is not doing 100% of what you had in mind, but it seems to be closely related, and you might need to tweak a bit my metaclass but, rougly, you can have this;

from types import DynamicClassAttribute

class XMeta(type):
     def __getattr__(self, value):
         if value == 'id':
             return XMeta.id  # You may want to change a bit that line.
     @property
     def id(self):
         return "Class {}".format(self.__name__)

That would define your class attribute. For the instance attribute:

class X(metaclass=XMeta):
    @DynamicClassAttribute
    def id(self):
        return "Instance {}".format(self.__class__.__name__)

It might be a bit overkill especially if you want to stay away from metaclasses. It's a trick I'd like to explore on my side, so I just wanted to share this hidden jewel, in case you can polish it and make it shine!

>>> X().id
'Instance X'
>>> X.id
'Class X'

Voila...