Python method available for both instantiated/uninstantiated class
Asked Answered
A

2

5

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?

Adamski answered 6/4, 2015 at 14:20 Comment(0)
A
10

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'
Alvarado answered 6/4, 2015 at 14:25 Comment(0)
G
0

If you want to have the class and instance method definitions separated, you can do something like:

class overriding_instance_method(object):
    """
    can be used as a decorator: see example below in __main__
    """
    def __init__(self, class_method_func, instance_method_func=None):
        self.class_method_func = class_method_func
        self.instance_method_func = instance_method_func

    def __call__(self, instance_method_func):
        return type(self)(self.class_method_func, 
                          instance_method_func=instance_method_func)

    def __get__(self, instance, cls=None):
        if instance is None:
            return classmethod(self.class_method_func).__get__(None, cls)
        return self.instance_method_func.__get__(instance, cls)

Use it like:

class OverridingClassMethodTest(object):

    def print_me(cls):
        print 'class: {}'.format(cls)

    @overriding_instance_method(print_me)
    def print_me(self):
        print 'instance: {}'.format(self)

OverridingClassMethodTest.print_me()
OverridingClassMethodTest().print_me()
Goalie answered 30/7, 2015 at 3:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.