I have a class which fetches details and populates the class with information if it's instantiated already with an id
using a details
method. If it's not instantiated yet I want it to instead use an argument passed into details
as the id
and return a new instantiated object. Something like the following:
f = Foo()
f.id = '123'
f.details()
but also allow for:
f = Foo.details(id='123')
Can I use the same details
method to accomplish this? Or do I need to create two separate methods and make one a @classmethod
? Can they have the same name if I declare one as a @classmethod
and the other not?
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'
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With