Search code examples
pythonpython-3.xclassgetter-setter

The pythonic way to construct a multimethod setter


We can use a @property to construct a getter and setter. This is a short example how we can do this:

class A:

    def __init__(self,x):
        self.x = x

    @property
    def x(self):
        return self.__x

    @x.setter
    def x(self, x):
        if x < 0:
            self.__x = 0
        elif x > 100:
            self.__x = 100
        else:
            self.__x = x

My case seems to be more complicated.

class A:

    def __init__(self, x):
        self.__x = x
        self.x1()
        self.x2()
        self.x3()

    def x1(self):
        self.__x1 = self.__x + 1
        return self.__x1

    def x2(self):
        self.__x2 = self.__x1 + 2
        return self.__x2

    def x3(self):
        self.__x3 = self.__x2 + 3
        return self.__x3


if __name__ == "__main__":
    a = A(3)
    print(a.x3)

Methods x1, x2 and x3 are oversimplified. The self.__x3 variable is set only once, when the __init__ method is called. Now, I need a getter method to get self.__x3 by calling a.x3. How to achieve that in the pythonic way?


Solution

  • Attempting an answer based on the assumption that you want the __x# variables modified only during __init__, and never again, but also want the accessors to follow the same code path (possibly because the read is also programmatically complex):

    In this case, you can have the implementing function take an additional, defaulted argument. When accessed in attribute form, it will receive the defaulted argument, but if the fget member of the property is explicitly accessed, it can be called with the non-default argument. A simple example addressing x1 only:

    class A:
    
        def __init__(self, x):
            self.__x = x
            # Access the property itself off the class, bypassing execution,
            # then call it directly with the non-default argument
            type(self).x1.fget(self, True)
    
        @property
        def x1(self, doset=False):
            if doset:
                self.__x1 = self.__x + 1
            return self.__x1
    

    Alternatively, to simplify the usage in __init__, you can use a separate name for the underlying function vs. the property to achieve the same effect:

    class A:
    
        def __init__(self, x):
            self.__x = x
            # Call the implementing function directly with the non-default argument
            self._x1(True)
    
        # Implementing function named with single underscore prefix to indicate it's
        # for internal/protected use only
        def _x1(self, doset=False):
            if doset:
                self.__x1 = self.__x + 1
            return self.__x1
        # Define property x1 based on x1 for outside use
        x1 = property(_x1)
    

    Of course, if you don't have a complicated getter path, then the real solution is to separate _x1 from x1 completely, where _x1 is pure setter helper function for __init__, and x1 is pure getter:

    class A:
    
        def __init__(self, x):
            self.__x = x
            # Call the init helper
            self._init_x1()
    
        # Implementing function named with single underscore prefix to indicate it's
        # for internal/protected use only
        def _init_x1(self):
            self.__x1 = self.__x + 1
    
        @property:
        def x1(self):
            return self.__x1
    

    To be clear, only the last of these is "Pythonic" in any meaningful sense. The second option has some limited use cases (where you have a function that demands existence, and is highly configurable, but has a reasonable set of defaults that a property could use), but in that case, it's usually a function that has public utility just like the property. Option #1 is the least Pythonic, as it's inconvenient to use (needing to elevate to the class type, extract the fget member, and explicitly pass self), and makes it quite clear that there is no expected use case outside of __init__ (because it's such a pain to use that no one would bother).