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 asC().f()
). The instance is ignored except for its class.
If you do need distinguish between binding to the class and an instance
If you need a method to work differently based on where it is being used on; bound to a class when accessed on the class, bound to the instance when accessed on the instance, you’ll need to create a custom descriptor object.
The descriptor API is how Python causes functions to be bound as methods, and bind classmethod
objects to the class; see the descriptor howto.
You can provide your own descriptor for methods by creating an object that has a __get__
method. Here is a simple one that switches what the method is bound to based on context, if the first argument to __get__
is None
, then the descriptor is being bound to a class, otherwise it is being bound to an instance:
class class_or_instancemethod(classmethod):
def __get__(self, instance, type_):
descr_get = super().__get__ if instance is None else self.__func__.__get__
return descr_get(instance, type_)
This re-uses classmethod
and only re-defines how it handles binding, delegating the original implementation for instance is None
, and to the standard function __get__
implementation otherwise.
Note that in the method itself, you may then have to test, what it is bound to. isinstance(firstargument, type)
is a good test for this:
>>> class X:
... @class_or_instancemethod
... def foo(self_or_cls):
... if isinstance(self_or_cls, type):
... return f"bound to the class, {self_or_cls}"
... else:
... return f"bound to the instance, {self_or_cls"
...
>>> X.foo()
"bound to the class, <class '__main__.X'>"
>>> X().foo()
'bound to the instance, <__main__.X object at 0x10ac7d580>'
An alternative implementation could use two functions, one for when bound to a class, the other when bound to an instance:
class hybridmethod:
def __init__(self, fclass, finstance=None, doc=None):
self.fclass = fclass
self.finstance = finstance
self.__doc__ = doc or fclass.__doc__
# support use on abstract base classes
self.__isabstractmethod__ = bool(
getattr(fclass, '__isabstractmethod__', False)
)
def classmethod(self, fclass):
return type(self)(fclass, self.finstance, None)
def instancemethod(self, finstance):
return type(self)(self.fclass, finstance, self.__doc__)
def __get__(self, instance, cls):
if instance is None or self.finstance is None:
# either bound to the class, or no instance method available
return self.fclass.__get__(cls, None)
return self.finstance.__get__(instance, cls)
This then is a classmethod with an optional instance method. Use it like you’d use a property
object; decorate the instance method with @<name>.instancemethod
:
>>> class X:
... @hybridmethod
... def bar(cls):
... return f"bound to the class, {cls}"
... @bar.instancemethod
... def bar(self):
... return f"bound to the instance, {self}"
...
>>> X.bar()
"bound to the class, <class '__main__.X'>"
>>> X().bar()
'bound to the instance, <__main__.X object at 0x10a010f70>'
Personally, my advice is to be cautious about using this; the exact same method altering behaviour based on the context can be confusing to use. However, there are use-cases for this, such as SQLAlchemy’s differentiation between SQL objects and SQL values, where column objects in a model switch behaviour like this; see their Hybrid Attributes documentation. The implementation for this follows the exact same pattern as my hybridmethod
class above.