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.
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.
Here are the type-hinted versions of the above, per request. These require that your project has typing_extensions
installed:
from typing import Generic, Callable, TypeVar, overload
from typing_extensions import Concatenate, ParamSpec, Self
_T = TypeVar("_T")
_R_co = TypeVar("_R_co", covariant=True)
_R1_co = TypeVar("_R1_co", covariant=True)
_R2_co = TypeVar("_R2_co", covariant=True)
_P = ParamSpec("_P")
class class_or_instancemethod(classmethod[_T, _P, _R_co]):
def __get__(
self, instance: _T, type_: type[_T] | None = None
) -> Callable[_P, _R_co]:
descr_get = super().__get__ if instance is None else self.__func__.__get__
return descr_get(instance, type_)
class hybridmethod(Generic[_T, _P, _R1_co, _R2_co]):
fclass: Callable[Concatenate[type[_T], _P], _R1_co]
finstance: Callable[Concatenate[_T, _P], _R2_co] | None
__doc__: str | None
__isabstractmethod__: bool
def __init__(
self,
fclass: Callable[Concatenate[type[_T], _P], _R1_co],
finstance: Callable[Concatenate[_T, _P], _R2_co] | None = None,
doc: str | None = 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: Callable[Concatenate[type[_T], _P], _R1_co]) -> Self:
return type(self)(fclass, self.finstance, None)
def instancemethod(self, finstance: Callable[Concatenate[_T, _P], _R2_co]) -> Self:
return type(self)(self.fclass, finstance, self.__doc__)
@overload
def __get__(self, instance: None, cls: type[_T]) -> Callable[_P, _R1_co]: ...
@overload
def __get__(self, instance: _T, cls: type[_T] | None = ...) -> Callable[_P, _R1_co] | Callable[_P, _R2_co]: ...
def __get__(self, instance: _T, cls: type[_T] | None = None) -> Callable[_P, _R1_co] | Callable[_P, _R2_co]:
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)