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?
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:
All class attributes should be included in the __init__
(initialiser) method. This is to ensure readability and aid debugging.
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.
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)
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).