Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python method available for both instantiated/uninstantiated class

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?

like image 535
mgoffin Avatar asked Apr 06 '15 14:04

mgoffin


1 Answers

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'
like image 127
Martijn Pieters Avatar answered Nov 03 '22 06:11

Martijn Pieters