Throughout the years, I've heard several opinions on what an class attribute is in Python. It seems as though everyone has their own subjective thought. However, there really is one accurate definition. Attributes come in two forms, data and methods. Data attributes are values that vary widely but strictly pertain to the class object. On the other hand, methods are just attributes that are callable.
Properties are also a third dimension that pertain to a class object. They're used to allow users to view public data attributes, yet simultaneously avoid any attempt to change them. Properties can be used through accessor methods such as a get or set method.
However, the beauty of Python doesn't stop there. Aside from properties, python exhibits an API for controlling attribute access. Special methods such as __getattr__ and __setattr__ are called by the interpreter when a user attempts to access attributes. Luckily, we can re-write these methods in a way that is convenient for our application.
class Garage(object):
def __init__(self, capacity):
self.fullspots = 0
self.capacity = capacity
def __getattr__(self, name):
if name == 'open':
return self.capacity - self.fullspots
else:
raise AttributeError ('%s not found', name)
def parkCar(self):
self.fullspots += 1
Above, I've defined a class that represents a garage. The data attributes here are fullspots and capacity. The attribute method is parkCar() and the accessor method is __getattr__().
The __getattr__ method is only called by the interpreter when the attribute attempting to being accessed is not found as a pre-defined data attribute ( like fullspots and capacity ). So if I attempt to access the attribute open, it is computed on the fly through the __getattr__ method and returned. Take a look below for a clear explanation. Remember, the __getattr__ is only called when the usual process fails to retrieve an attribute from the instance, class or superclass.
>>> garage = Garage(50)
>>> garage.parkCar()
>>> garage.fullspots
1
>>> garage.capacity
50
>>> garage.open
49
>>> garage.parkCar()
>>> garage.open
48
Even though the open isn't a class data attribute, it's still computed and can be accessed by __getattr__. The interpreter calls these special methods such as __get/setattr__ using dot notation as seen above.
>>> garage.open = 40
>>> garage.fullspots
2
Notice the error above. When setting open to 40, fullspots should equal 10. This error has occurred because we haven't defined the __setattr__ method that handles data attribute assignments. Defining it would look something like below
def __setattr__(self, name, value):
if name == 'open':
self.fullspots = self.capacity - value
else:
object.__setattr__(self, name, value)
Now lets test it after adding this function.
>>> garage = Garage(50)
>>> garage.fullspots
0
>>> garage.open = 40
>>> garage.fullspots
10
The problem with this is that now users can (intentionally or unintentionally) change the values of fullspots and capacity. This is a crucial problem for most applications.
>>> garage = Garage(50)
>>> garage.fullspots
0
>>> garage.open = 40
>>> garage.fullspots
10
>>> garage.fullspots = 30
>>> garage.open
20
>>> garage.capacity = 100
>>> garage.open
70
As the author of this code, I want to make sure that the capacity of the garage cannot change. Also, I want to make sure that the only way a spot is filled is if a car is parked ( through the parkCar method). Luckily, python makes this much easier for us. All you have to do is name your data attributes beginning with a double underscore.
class Garage(object):
def __init__(self, capacity):
self.__fullspots = 0
self.__capacity = capacity
Now when you attempt to change it
>>> garage.fullspots = 10
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "CarGarage.py", line 128, in __setattr__
object.__setattr__(self, name, value)
AttributeError: can't set attribute
>>> garage.open = 10
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "CarGarage.py", line 125, in __setattr__
self.fullspots = self.capacity - value
File "CarGarage.py", line 67, in __getattr__
raise AttributeError ('%s not found', name)
AttributeError: ('%s not found', 'capacity')
The second error occurred because in the __setattr__ method, it attempts to set 'open' on self.fullspots and self.capacity, which are now replaced with self.__fullspots and self.__capacity.
The leverage we've gained here is not optimized because now we cannot access these values to even read only.
>>> garage.fullspots
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "CarGarage.py", line 67, in __getattr__
raise AttributeError ('%s not found', name)
AttributeError: ('%s not found', 'fullspots')
>>> garage.open
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "CarGarage.py", line 65, in __getattr__
return self.capacity - self.fullspots
File "CarGarage.py", line 67, in __getattr__
raise AttributeError ('%s not found', name)
AttributeError: ('%s not found', 'capacity')
>>> garage.capacity
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "CarGarage.py", line 67, in __getattr__
raise AttributeError ('%s not found', name)
AttributeError: ('%s not found', 'capacity')
Python offers a solution that will allow us to read only these data attributes. This is possible through the property decorator.
@Property
The property decorator is used to declare a property of the class. Simply put, its purpose is to allow users to read data attributes of the class while avoiding the capability of changing it.
class Garage(object):
def __init__(self, capacity):
self.__fullspots = 0
self.__capacity = capacity
@property
def fullspots(self):
return self.__fullspots
@property
def capacity(self):
return self.__capacity
Now when we call Garage.fullspots or Garage.capacity, the interpreter calls the methods fullspots and capacity, which return the data attributes self.__fullspots and self.__capacity respectively. Now the assignment to Garage.fullspots will raise a AttributeError and the assignment of Garage.__fullspots is meaningless. It won't effect the class attribute. Same goes for Garage.capacity and Garage.__capacity.
>>> garage = Garage(50)
>>> garage.fullspots
0
>>> garage.capacity
50
>>> garage.__fullspots
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "CarGarage.py", line 67, in __getattr__
raise AttributeError ('%s not found', name)
AttributeError: ('%s not found', '__fullspots')
>>> garage.__capacity
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "CarGarage.py", line 67, in __getattr__
raise AttributeError ('%s not found', name)
AttributeError: ('%s not found', '__capacity')
>>> garage.fullspots = 10
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "CarGarage.py", line 128, in __setattr__
object.__setattr__(self, name, value)
AttributeError: can't set attribute
>>> garage.fullspots
0
>>> garage.__fullspots = 10
>>> garage.fullspots
0
>>> garage.parkCar()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "CarGarage.py", line 77, in parkCar
self.fullspots += 1
File "CarGarage.py", line 128, in __setattr__
object.__setattr__(self, name, value)
AttributeError: can't set attribute
The error in parkCar() occurs because we still haven't changed it to increment self.__fullspots instead of self.fullspots. The implementation of parkCar below is calling on the property function fullspots, the one we just proved unable to set or change the class data attribute.
def parkCar(self):
self.fullspots += 1
The above should be altered to
def parkCar(self):
self.__fullspots += 1
Now,
>>> garage = Garage(50)
>>> garage.parkCar()
>>> garage.fullspots
1
This looks much better.
What about the variables in __setattr__ and __getattr__ ? The methods that compute 'open' on the fly. Do we keep the variables set to the class methods self.fullspots and self.capacity or do we change them to the data attributes self.__fullspots and self.__capacity ?
def __getattr__(self, name):
if name == 'open':
return self.capacity - self.fullspots
else:
raise AttributeError ('%s not found', name)
def __setattr__(self, name, value):
if name == 'open':
self.fullspots = self.capacity - value
else:
object.__setattr__(self, name, value)
Well think about it. In __getattr__ it really doesn't matter. Since 1) your only returning the difference between two values and your not changing anything and 2) self.capcity and self.fullspots return their respective data attributes without manipulating them. I will say though, some applications may require that you use one over the other, so i'll leave that up to you.
In __setattr__, you're referring to the method attribute fullspots, which cannot be changed, hence any assignment to 'open' in this case will raise an error. If you want to allow the setting simply change the above __setattr__ to as it is below.
def __setattr__(self, name, value):
if name == 'open':
self.__fullspots = self.capacity - value
else:
object.__setattr__(self, name, value)
Now you're referring to the data attribute fullspots. This application will not raise an error. self.capacity does not have to change to self.__capacity since you're not attempting to change it. You're only extracting it's value to use it in an operation.
The choice on which variable to use, the data attribute or the method attribute is completely subjective to your application the scope you intend to allow to your users. I just laid out the options. You should think very carefully as to what you should and should not allow.
If you have any questions, please comment below or shoot me an email! Thanks! are called by the interpreter when a user attempts to access attributes. Luckily, we can re-write these methods in a way that is convenient for the our application.
self.fullspots += 1
The above should be altered to
def parkCar(self):
self.__fullspots += 1
Now,
>>> garage = Garage(50)
>>> garage.parkCar()
>>> garage.fullspots
1
This looks much better.
What about the variables in __setattr__ and __getattr__ ? The methods that compute 'open' on the fly. Do we keep the variables set to the class methods self.fullspots and self.capacity or do we change them to the data attributes self.__fullspots and self.__capacity ?
def __getattr__(self, name):
if name == 'open':
return self.capacity - self.fullspots
else:
raise AttributeError ('%s not found', name)
def __setattr__(self, name, value):
if name == 'open':
self.fullspots = self.capacity - value
else:
object.__setattr__(self, name, value)
Well think about it. In __getattr__ it really doesn't matter. Since 1) your only returning the difference between two values and your not changing anything and 2) self.capcity and self.fullspots return their respective data attributes without manipulating them. I will say though, some applications may require that you use one over the other, so i'll leave that up to you.
In __setattr__, you're referring to the method attribute fullspots, which cannot be changed, hence any assignment to 'open' in this case will raise an error. If you want to allow the setting simply change the above __setattr__ to as it is below.
def __setattr__(self, name, value):
if name == 'open':
self.__fullspots = self.capacity - value
else:
object.__setattr__(self, name, value)
Now you're referring to the data attribute fullspots. This application will not raise an error. self.capacity does not have to change to self.__capacity since you're not attempting to change it. You're only extracting it's value to use it in an operation.
The choice on which variable to use, the data attribute or the method attribute is completely subjective to your application the scope you intend to allow to your users. I just laid out the options. You should think very carefully as to what you should and should not allow.
If you have any questions, please comment below or shoot me an email! Thanks! are called by the interpreter when a user attempts to access attributes. Luckily, we can re-write these methods in a way that is convenient for the our application.