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.

3
  • 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 Nonein __init__ does not help the spec. In this case you can spec based on an instance instead of the class. Commented Apr 24, 2020 at 18:03
  • 1
    If it is just for a better reading of the involved class members, C looks OK. Commented Apr 24, 2020 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. Commented Apr 27, 2020 at 16:08

2 Answers 2

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.

Sign up to request clarification or add additional context in comments.

2 Comments

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

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.