Search code examples
pythonfunctionclassmethodspython-descriptors

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

Solution

  • 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.