2

Sometimes I want to write a class with instance variables which should on the one hand be initialized inside __init__, but on the other hand also later be updatable via different functions (function_1 - function_3) either from the inside after some event happened or from the outside.

The update functions depend all on the same input parameter, but work the same in initialization and later update. They may be either members of the class (@staticmethod or not) or utility functions imported from some package.

For the later update, the "meta" update function (update_member_variables) clearly should be a procedure, i. e. return nothing and only modify the member variables as a side effect. However, for initialization it should better be a pure function and return the variables' values so that they can be assigned to the variables inside __init__.

This conflict always puts me through the following cycle of duplicated code, declarations outside __init__ and None-initializations, but never leads to a satisfying solution:

from some_utils import function_1, function_2, function_3


# A: duplicate code in update_member_variables
class Class:
    def __init__(self, parameter):
        self._variable_1 = function_1(parameter)
        self._variable_2 = function_2(parameter)
        self._variable_3 = function_3(parameter)

    def update_member_variables(self, parameter):
        self._variable_1 = function_1(parameter)
        self._variable_2 = function_2(parameter)
        self._variable_3 = function_3(parameter)


# B: instance variables declared outside __init__
class Class:
    def __init__(self, parameter):
        self.update_member_variables(parameter)

    def update_member_variables(self, parameter):
        self._variable_1 = function_1(parameter)
        self._variable_2 = function_2(parameter)
        self._variable_3 = function_3(parameter)


# C: boilerplate None-assignments
class Class:
    def __init__(self, parameter):
        self._variable_1 = None
        self._variable_2 = None
        self._variable_3 = None
        self.update_member_variables(parameter)

    def update_member_variables(self, parameter):
        self._variable_1 = function_1(parameter)
        self._variable_2 = function_2(parameter)
        self._variable_3 = function_3(parameter)


# D: back to duplicated code in update_member_variables
class Class:
    def __init__(self, parameter):
        (
            self._variable_1,
            self._variable_2,
            self._variable_3
        ) = self._derive_values(parameter)

    def _derive_values(self, parameter):
        return (
            function_1(parameter),
            function_2(parameter),
            function_3(parameter),
        )

    def update_member_variables(self, parameter):
        (
            self._variable_1,
            self._variable_2,
            self._variable_3
        ) = self._derive_values(parameter)

It is tempting to choose B, but because of all the warnings against member variable declarations outside of __init__, I usually stick with C or D although they seem bloated and bulky.

Isn't there any better way to solve this situation? Maybe outside the box? Or is the most "elegant" or clean one already among A-D?

Related questions:

In https://stackoverflow.com/a/51607441/3863847 a similar question was answered, but update_number is only suitable for initialization. The accepted answer's code is like my D, but without update_member_variables.

In https://stackoverflow.com/a/20661498/3863847 another related question was answered. In general, Simeon Visser states, that it is the developer's responsibility to ensure a consistent object after initialization, but also that it is not strictly necessary clinging to this rule no matter what. Is my case such a case in which it is okay to choose B? The instanciated objects of B would be consistent at least.

S818
  • 391
  • 3
  • 15
  • 1
    what is the reason of "all the warnings"? Something that I remember related to this has to do with specced mocks when writing unit tests. But assigning `None`in `__init__` does not help the spec. In this case you can spec based on an instance instead of the class. – progmatico Apr 24 '20 at 18:03
  • 1
    If it is just for a better reading of the involved class members, C looks OK. – progmatico Apr 24 '20 at 18:05
  • @progmatico: The reason for all the warnings seems to be PEP 8, although I didn't find the exact passage yet. Since I found another recommendation for None-assignments like you suggested as well, I am going to consider C the best version. Thank you for that. And also thank you for directing me to autospec. – S818 Apr 27 '20 at 16:08

2 Answers2

2
class A:
    def __init__(self, parameter):
        self._variable_1 = function_1(parameter)
        self._variable_2 = function_2(parameter)
        self._variable_3 = function_3(parameter)

    @property
    def variable_1(self):
        return self._variable_1

    @variable_1.setter
    def variable_1(self, value):
        self._variable_1 = function_1(value)

    ... so on and so forth for other variables ...

 a = A(parameter1)
 # update based on parameters
 a.variable_1 = parameter2

I feel using property you can update your variables in a better way.

S818
  • 391
  • 3
  • 15
MSS
  • 3,306
  • 1
  • 19
  • 50
  • 1
    Thank you for your approach, but this is not what I am looking for, I'm afraid. Generally, it is a good idea to hide some functionality inside a setter, but I would have to call the setter for every variable separately this way, although the exact same parameter goes into each `function_n()`. I could bundle the variables inside another `Variables` class with a `Variables.update()` which gets called by the `Class.update()` and have an instance of it as a member for `Class`, but this would just move the problem to the `Variables` class. – S818 Apr 23 '20 at 13:07
  • 1
    also for @S818, and still about specs You may not be able to use autospec when mocking if there are properties or descriptors that can trigger code execution. Search for "This isn’t without caveats" and read from there on [here](https://docs.python.org/3/library/unittest.mock.html#auto-speccing) – progmatico Apr 24 '20 at 18:14
0

In https://stackoverflow.com/a/19292653/3863847 readability is given by sthenault as a reason for why instance variables should not be declared outside __init__.

As far as I know, this is rooted in PEP 8 and that is why pylint complains about violations - and I never choose B.

sthenault also suggests None-assignments in __init__, just like progmatico did in the comment below my question, which corresponds to my version C.

Although I was hoping for an elegant trick to somehow circumvent this situation, I am going to consider C as "most pythonic" for the time being. If anyone later comes up with this kind of magical solution I am looking for, I will switch the accepted answer.

S818
  • 391
  • 3
  • 15