pythonclassinstanceinstance-variablespython-attrs

Setting default/empty attributes for user classes in __init__


When I am creating a new class, should I set all instance attributes in __init__, even if they are None and in fact later assigned values in class methods?

See example below for the attribute results of MyClass:

class MyClass:
    def __init__(self,df):
          self.df = df
          self.results = None

    def results(df_results):
         #Imagine some calculations here or something
         self.results = df_results

I have found in other projects, class attributes can get buried when they only appear in class methods and there is a lot going.

So to an experienced professional programmer what is standard practice for this? Would you define all instance attributes in __init__ for readability?


Solution

  • Following considerable research and discussions with experienced programmers please see below what I believe is the most Pythonic solution to this question. I have included the updated code first and then a narrative:

    class MyClass:
        def __init__(self,df):
              self.df = df
              self._results = None
    
        @property
        def results(self):
            if self._results is None:
                raise Exception('df_client is None')
            return self._results
    
        def generate_results(self, df_results):
             #Imagine some calculations here or something
             self._results = df_results
    

    Description of what I learnt, changed and why:

    1. All class attributes should be included in the __init__ (initialiser) method. This is to ensure readability and aid debugging.

    2. The first issue is that you cannot create private attributes in Python. Everything is public, so any partially initialised attributes (such as results being set to None) can be accessed. Convention to indicate a private attribute is to place a lead underscore at the front, so in this case I changed it to self.results to self._results.

      Keep in mind this is only convention, and self._results can still be directly accessed. However, this is the Pythonic way to handle what are pseudo-private attributes.

    3. The second issue is having a partly initialised attribute which is set to None. As this is set to None, as @jferard below explains, we now have lost a fail-fast hint and have added a layer of obfuscation for debugging the code.

      To resolve this we add a getter method. This can be seen above as the function results() which has the @property decorator above.

      This is a function that when invoked checks if self._results is None. If so it will raise an exception (fail-safe hint), otherwise it will return the object. The @property decorator changes the invocation style from a function to an attribute, so all the user has to use on an instance of MyClass is .results just like any other attribute.

      (I changed the name of the method that sets the results to generate_results() to avoid confusion and free up .results for the getter method)

    4. If you then have other methods within the class that need to use self._results, but only when properly assigned, you can use self.results, and that way the fail-safe hint is baked in as above.

    I recommend also reading @jferard's answer to this question. He goes into depth about the problems and some of the solutions. The reason I added my answer is that I think for a lot of cases the above is all you need (and the Pythonic way of doing it).