30

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?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Markus Meskanen
  • 19,939
  • 18
  • 80
  • 119

6 Answers6

44

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)
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • 2
    The problem is that any argument in the constructor or other instance data cannot be accessed from the class method so your "solution" is very limited. – rwst Oct 17 '17 at 15:38
  • @rwst for what the op tried to do it was all that was needed. What usecase do you have that needs to have access to the instance some of the time? – Martijn Pieters Oct 17 '17 at 15:47
  • See comment on other answer. – rwst Oct 17 '17 at 15:57
  • @rwst: that comment doesn't tell me much at all. Why do you need `ClassObject.methodname()` and `instance.methodname()` to behave differently based on what it is bound to? – Martijn Pieters Oct 17 '17 at 16:04
  • Because in our case we encountered both class objects and instances. Both should react to the same call but differently. The complication was that the code of these objects lives in a different package and has to be added to the objects dynamically inside the metaclass that creates the class objects. See https://github.com/sympy/sympy/pull/13476 – rwst Oct 18 '17 at 18:45
  • @rwst: that's a rather esoteric use-case, and it's not clear from your pull request *why* `Class._sage_()` behaviour differs from `instance._sage_()`. At any rate, the use-case in the question did not require access to the instance, the basic problem there was that the OP did not understand how classmethods work. – Martijn Pieters Oct 19 '17 at 07:40
  • Here's a more concrete example. Say there's a Vector3D class. It is quite desirable that Vector3D.cross(a, b) and a = Vector3D(1,2,3); a.cross(b) both be available. We just implemented it using the "binding and hiding" approach in one of the answers below, but just... yuck? – dgorur Jan 28 '19 at 06:37
  • If you ever have time, could you add typing hints to help understand the different arguments? – Roelant May 24 '23 at 12:05
  • @Roelant: I'm not sure that'd help, the type hints required use 2 typevars, one of which is covariant, and a paramspec. For `class_or_instancemethod` the signatures are exactly the same as [those for the `classmethod` decorator](https://github.com/python/typeshed/blob/852ac3efb3da704267082cc611557214f2f87aac/stdlib/builtins.pyi#L143-L154) it inherits from. For hybridmethod add the [`Self` type](https://peps.python.org/pep-0673/) for the two decorator methods. The type checker will understand these but it all gets a lot more abstract. – Martijn Pieters Jun 08 '23 at 17:54
  • 1
    @Roelant: I've added type-hint annotated versions to the end of the answer now. – Martijn Pieters Jun 08 '23 at 18:14
26

I have 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
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Ashwini Chaudhary
  • 244,495
  • 58
  • 464
  • 504
  • 1
    Many thanks! That resolved an annoying problem with conversion of SymPy function objects to Sage objects (both Open Source math packages). – rwst Oct 17 '17 at 15:56
13

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']]
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
  • 1
    (Note that this is inferior to the descriptor solution) –  Oct 21 '18 at 18:05
  • 1
    can you please explain why this is inferior? It seems to me much more clear and requiring less boilerplate. – ipetrik Nov 09 '18 at 18:03
  • 3
    @ipetrik I mostly thought so because it's more-easily circumventable and also sloppier: it uses a hack-y technique to figure out whether it's an instance, whereas descriptors get passed that info by Python itself. It also requires you to do the `self.x = self.__x` assignment in `__init__` for *every single method* (although this could be offloaded to + automated by a metaclass or decorator, I think)... and lastly monkeypatching is generally just not so nice. But it was definitely just an opinion -- if you find this cleaner then have at it! –  Nov 27 '18 at 14:35
  • 1
    Personally I like this solution because it doesn't require the person reading the code or implementing it to understand descriptors. Maybe I'm missing something, but it doesn't feel like a situation where "if you're doing anything like this, you _ought_ to be _required_ to understand descriptors" applies strongly enough, so the advantage of simplicity gets substantial weight in my mind here. – mtraceur Jun 04 '21 at 21:23
  • 1
    P.S. But I haven't quite decided if it is ever the best design to do methods like this, or if so then what the thresholds are for when it becomes better to switch to implementing it with descriptors rather than this hack. – mtraceur Jun 04 '21 at 21:31
  • nice solution and you can still call the class method from the instance by doing self.__class__.methodname – ARR Jan 10 '23 at 22:18
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...

Saeshan
  • 49
  • 1
  • 3
  • Do you need the metaclass? From the docs, it looks like you can just modify `__getattr__`... – DylanYoung Aug 02 '19 at 14:29
  • @DylanYoung The metaclass is necessary because `DynamicClassAttribute` sends the attribute access to the _class_'s `__getattr__`, which is defined in its metaclass. – jirassimok Nov 16 '19 at 03:38
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
Tom Swirly
  • 2,740
  • 1
  • 28
  • 44
3

(Python 3 only) Elaborating on the idea of a pure-Python implementation of @classmethod, we can declare an @class_or_instance_method as a decorator, which is actually a class implementing the attribute descriptor protocol:

import inspect


class class_or_instance_method(object):

    def __init__(self, f):
        self.f = f

    def __get__(self, instance, owner):
        if instance is not None:
            class_or_instance = instance
        else:
            class_or_instance = owner

        def newfunc(*args, **kwargs):
            return self.f(class_or_instance, *args, **kwargs)
        return newfunc

class A:
    @class_or_instance_method
    def foo(self_or_cls, a, b, c=None):
        if inspect.isclass(self_or_cls):
            print("Called as a class method")
        else:
            print("Called as an instance method")
DomQ
  • 4,184
  • 38
  • 37
  • What is a "wrt"? I cannot for the life of me decode that abbreviation. – mtraceur Jun 04 '21 at 21:33
  • With Respect To (https://en.wiktionary.org/wiki/WRT#English). As in, the class or instance with respect to which we want to call the `@class_or_instance_method`. – DomQ Jun 10 '21 at 09:55
  • Oh, I had thought of "with regard to", but the "[the object] with regard/respect to [which we want to call the decorated function]" connection was very non-obvious to me. – mtraceur Jun 10 '21 at 19:44
  • I can see the appeal now that you've made the whole phrase explicit. Thank you for clarifying. Personally I would still just use something like `class_or_instance` as the variable name instead of `wrt` though, because readers like me will struggle to infer the phrasing which makes the variable name make sense. – mtraceur Jun 10 '21 at 19:51