ISISComputingGroup / ISISComputingGroup/lewis
Guidelines for Class Attribute vs Instance Attribute usage
- Dominant language
- Python
- Stars
- 24
- Forks
- 19
- PR merge metrics
- No merged PRs in 30d
Description
Class attributes can be a bit dangerous in Python. They have behaviour that **appears** very similar to instance attributes **most** of the time, but work very differently under the hood and can suddenly produce radically different results under some conditions (which may be triggered far away from what is affected).
To illustrate:
```python
class Foobar(object):
_value = 'Foo'
@property
def value(self):
return self._value # Doesn't really exist until setter is called!
@value.setter
def value(self, v):
self._value = v
>>> foo = Foobar()
>>> foo.value # Returns Foobar._value (foo._value doesn't exist yet!)
'Foo'
>>> foo.value = 'Foo' # Defines foo._value for the first time
>>> foo.value # Returns foo._value now
'Foo' # Looks the same, but isn't
>>> bar = Foobar()
>>> bar.value # Foobar._value
'Foo'
>>> Foobar._value = 'Bar'
>>> foo.value # foo._value
'Foo'
>>> bar.value # Foobar._value still
'Bar'
>>> bar.value = bar.value # Side-effects can't be predicted by looking at property code alone (!!!)
>>> Foobar._value = 'Baz'
>>> bar.value # Now really bar._value
'Bar'
```
---
Even worse:
```python
class Foobar(object):
values = []
def add(self, value):
self.values.append(value)
def get(self):
return self.values
>>> foo = Foobar()
>>> foo.add('hello')
>>> foo.get()
['hello']
>>> bar = Foobar()
>>> bar.add('world')
>>> bar.get() # ?!
['hello', 'world']
>>> foo.get() # !!
['hello', 'world']
>>> foo.values = ['goodbye', 'cruel']
>>> foo.get()
['goodbye', 'cruel']
>>> foo.add('world')
>>> foo.get()
['goodbye', 'cruel', 'world']
>>> bar.get() # ?!?!
['hello', 'world']
```
This is probably not intended behaviour. Again, this is caused by `self.values` not actually existing (and defaulting to Foobar.values). Here with the added twist that, since the value of `values` is mutable, changes to an instance affect all other instances. That is, until I "assign" something to `foo.values` (really, create `foo.values` for the first time and point it at a different list). Now `foo` has its own `values` list, while `bar` still falls back on whatever `Foobar.values` points to.
---
In C++ terms, Python class attributes appear to behave like `static`, `public` data members that automagically become non-`static` as soon as you assign something to them via `this->attribute = ...` or `instance_variable.attribute = ...`. But only for that one instance. And not if you modify them any other way apart from assignment (such as calling member functions). And not if you assign to `ClassName::attribute` instead.
This issue is to do the following:
- Come up with general rule(s) for when using class attributes is appropriate / permitted. For example:
- Are there any plausible scenarios where you or another developer may be tempted to write `self.attribute = value` or `myinstance.attribute = value`?
If yes, `attribute` must be an instance attribute.
If no, `attribute` may be a class attribute.
... or ...
- "Only use a class attribute where you would use a class constant in C++."
- Document rule(s) in our contribution guidelines somewhere.
- Go through codebase to make sure it complies with new rule(s).
`SimulatedJulabo`, in particular, has a lot of class attributes that are used like instance attributes.
Contributor guide
Assessment
This issue has not been assessed yet.