Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Function to behave differently on class vs on instance

I'd like a particular function to be callable as a classmethod, and to behave differently when it's called on an instance.

For example, if I have a class Thing, I want Thing.get_other_thing() to work, but also thing = Thing(); thing.get_other_thing() to behave differently.

I think overwriting the get_other_thing method on initialization should work (see below), but that seems a bit hacky. Is there a better way?

class Thing:

    def __init__(self):
        self.get_other_thing = self._get_other_thing_inst()

    @classmethod
    def get_other_thing(cls):
        # do something...

    def _get_other_thing_inst(self):
        # do something else
like image 213
DilithiumMatrix Avatar asked Mar 05 '23 22:03

DilithiumMatrix


2 Answers

Great question! What you seek can be easily done using descriptors.

Descriptors are Python objects which implement the descriptor protocol, usually starting with __get__().

They exist, mostly, to be set as a class attribute on different classes. Upon accessing them, their __get__() method is called, with the instance and owner class passed in.

class DifferentFunc:
    """Deploys a different function accroding to attribute access

    I am a descriptor.
    """

    def __init__(self, clsfunc, instfunc):
        # Set our functions
        self.clsfunc = clsfunc
        self.instfunc = instfunc

    def __get__(self, inst, owner):
        # Accessed from class
        if inst is None:
            return self.clsfunc.__get__(None, owner)

        # Accessed from instance
        return self.instfunc.__get__(inst, owner)


class Test:
    @classmethod
    def _get_other_thing(cls):
        print("Accessed through class")

    def _get_other_thing_inst(inst):
        print("Accessed through instance")

    get_other_thing = DifferentFunc(_get_other_thing,
                                    _get_other_thing_inst)

And now for the result:

>>> Test.get_other_thing()
Accessed through class
>>> Test().get_other_thing()
Accessed through instance

That was easy!

By the way, did you notice me using __get__ on the class and instance function? Guess what? Functions are also descriptors, and that's the way they work!

>>> def func(self):
...   pass
...
>>> func.__get__(object(), object)
<bound method func of <object object at 0x000000000046E100>>

Upon accessing a function attribute, it's __get__ is called, and that's how you get function binding.

For more information, I highly suggest reading the Python manual and the "How-To" linked above. Descriptors are one of Python's most powerful features and are barely even known.


Why not set the function on instantiation?

Or Why not set self.func = self._func inside __init__?

Setting the function on instantiation comes with quite a few problems:

  1. self.func = self._funccauses a circular reference. The instance is stored inside the function object returned by self._func. This on the other hand is stored upon the instance during the assignment. The end result is that the instance references itself and will clean up in a much slower and heavier manner.
  2. Other code interacting with your class might attempt to take the function straight out of the class, and use __get__(), which is the usual expected method, to bind it. They will receive the wrong function.
  3. Will not work with __slots__.
  4. Although with descriptors you need to understand the mechanism, setting it on __init__ isn't as clean and requires setting multiple functions on __init__.
  5. Takes more memory. Instead of storing one single function, you store a bound function for each and every instance.
  6. Will not work with properties.

There are many more that I didn't add as the list goes on and on.

like image 101
Bharel Avatar answered Mar 25 '23 01:03

Bharel


Here is a bit hacky solution:

class Thing(object):
    @staticmethod
    def get_other_thing():
        return 1

    def __getattribute__(self, name):
        if name == 'get_other_thing':
            return lambda: 2
        return super(Thing, self).__getattribute__(name)

print Thing.get_other_thing()  # 1
print Thing().get_other_thing()  # 2

If we are on class, staticmethod is executed. If we are on instance, __getattribute__ is first to be executed, so we can return not Thing.get_other_thing but some other function (lambda in my case)

like image 37
Eugene Primako Avatar answered Mar 25 '23 00:03

Eugene Primako