You'll have to create your own descriptor to handle this; it'll have to bind to the class if no instance is available, otherwise to the instance:
class class_or_instance_method(object):
def __init__(self, func, doc=None):
self.func = func
self.cmdescriptor = classmethod(func)
if doc is None:
doc = func.__doc__
self.__doc__ = doc
def __get__(self, instance, cls=None):
if instance is None:
return self.cmdescriptor.__get__(None, cls)
return self.func.__get__(instance, cls)
This descriptor delegates to a classmethod()
object if no instance is available, to produce the right binding.
Use it like this:
class Foo(object):
@class_or_instance_method
def details(cls_or_self, id=None):
if isinstance(cls_or_self, type):
# called on a class
else:
# called on an instance
You can could make it more fancy by returning your own method-like wrapper object that passes in keyword arguments for the binding instead.
Demo:
>>> class Foo(object):
... @class_or_instance_method
... def details(cls_or_self, id=None):
... if isinstance(cls_or_self, type):
... return 'Class method with id {}'.format(id)
... else:
... return 'Instance method with id {}'.format(cls_or_self.id)
...
>>> Foo.details(42)
'Class method with id 42'
>>> f = Foo()
>>> f.id = 42
>>> f.details()
'Instance method with id 42'
The test in the function itself is a little tedious; you could take a leaf from how property
objects operate and attach a separate function to handle the class-bound case:
class class_or_instance_method(object):
def __init__(self, instf, clsf=None, doc=None):
self.instf = instf
self.clsf = clsf
self.cmdescriptor = classmethod(clsf or instf)
if doc is None:
doc = instf.__doc__
self.__doc__ = doc
def __get__(self, instance, cls=None):
if instance is None:
return self.cmdescriptor.__get__(None, cls)
return self.instf.__get__(instance, cls)
def classmethod(self, clsf):
return type(self)(self.instf, clsf, doc=self.__doc__)
def instancemethod(self, instf):
return type(self)(instf, self.clsf, doc=self.__doc__)
This will call the initial decorated function for both classes or instances (just like the implementation of the descriptor above), but it lets you register an optional, separate function to handle binding to a class when you use the @methodname.classmethod
decorator:
class Foo(object):
@class_or_instance_method
def details(self):
# called on an instance
@details.classmethod
def details(cls, id):
# called on a class, takes mandatory id argument
This has the added advantage that now you can give the two method implementations distinct parameters; Foo.details()
takes an id
argument in the above, whereas instance.details()
does not:
>>> class Foo(object):
... @class_or_instance_method
... def details(self):
... return 'Instance method with id {}'.format(self.id)
... @details.classmethod
... def details(self, id):
... return 'Class method with id {}'.format(id)
...
>>> Foo.details(42)
'Class method with id 42'
>>> f = Foo()
>>> f.id = 42
>>> f.details()
'Instance method with id 42'