Using Python descriptors
Descriptors are used to manage access to an attribute. They can be used to protect an attribute from changes or automatically update the value of a dependent attribute, as we'll see in this guide.
Descriptors are used to manage access to an attribute. They can be used to protect an attribute from changes or automatically update the value of a dependent attribute, as we'll see in this guide.
A descriptor is simply a way for the developer to manage access to an attribute. Descriptors determine what happens when an attribute is referenced on a model. Descriptors give the developer the ability to manage access to three actions: get, set, and delete.
get
print(user.name)
set
user.name = "Adam Smith"
delete
del(user.name)
No, Python doesn't offer a way to privatize class variables. Why not?
"Python makes a lot of assumptions. One being that you're not an idiot." Internet
But there are some use cases for private variables - one being to avoid name clashes with subclasses. But also, there are occasions where a variable really shouldn't be made accessible to the user because it needs to be validated either upon being set or retrieved.
Keep in mind, these variables are in no way private. It is only a convention. When you prefix a variable with an underscore, it let's other developers know that this variable is "private" or "for internal use". So if you're using a class, and you're trying to set a "private" variable directly you might find that something breaks.
A single underscore prefix specifies "weak internal use". This is contrasted with a double underscore prefix which, when used, becomes mangled. This means that it becomes prefixed with the class name to avoid clashing with subclass variables.
Like this:
In [1]: class foo: __bar = None
In [2]: print(dir(foo))
['__class__', ..., '_foo__bar']
But for the sake of this guide, we're going to use single underscores for our private variables.
As an example we'll create a temperature class. The point of this class is to keep track of the user-specified temperature and display the output in either celsius or fahrenheit. So the user should be able to set either fahrenheit or celsius and retrieve the temperature in either fahrenheit or celsius.
The wrongest way.
class Temperature(object):
def __init__(self, fahrenheit=32):
self.fahrenheit = fahrenheit
self.celsius = self.convert_to_celsius(fahrenheit)
def convert_to_celsius(self, f):
return (f - 32) * (5 / 9)
def convert_to_fahrenheit(self, c):
return (c * (9 / 5)) + 32
So we're setting the initial temperature to 32 degrees F upon instantiation. Then you can access the temperature in either F or C. But after that, if the user sets the temperature, it will not change the value of the other measurement.
Let's see this in action:
In [1]: from temperature import Temperature
In [2]: t = Temperature()
In [3]: t.celsius
Out[3]: 0.0
In [4]: t.fahrenheit
Out[4]: 32
In [5]: t.celsius = 100
In [6]: t.fahrenheit
Out[6]: 32
You can see that setting celsius to 100 has no impact on the fahrenheit value.
A way that works.
If you just want to make this work you can create a set method for the user.
class Temperature(object):
def __init__(self, fahrenheit=32):
self.fahrenheit = fahrenheit
self.celsius = self.convert_to_celsius(fahrenheit)
def convert_to_celsius(self, f):
return (f - 32) * (5 / 9)
def convert_to_fahrenheit(self, c):
return (c * (9 / 5)) + 32
def set_fahrenheit(self, value):
self.fahrenheit = value
self.celsius = self.convert_to_celsius(value)
def set_celsius(self, value):
self.fahrenheit = self.convert_to_fahrenheit(value)
self.celsius = value
Now we have a set method that we want our user to use. Assuming that the user knows exactly what we expect of them, this can work.
In [1]: from temperature import Temperature
In [2]: t = Temperature()
In [3]: t.set_fahrenheit(212)
In [4]: t.fahrenheit
Out[4]: 212
In [5]: t.celsius
Out[5]: 100.0
So using our setter actually does what we'd like. But Python provides a way to keep the setters and getters hidden by way of descriptors with properties. I believe this is cleaner and more intuitive for the user. This method also introduces Python's conventional way of creating "private" variables.
class Temperature(object):
def __init__(self, fahrenheit=32):
self._fahrenheit = fahrenheit
self._celsius = self.convert_to_celsius(fahrenheit)
def convert_to_celsius(self, f):
return (f - 32) * (5 / 9)
def convert_to_fahrenheit(self, c):
return (c * (9 / 5)) + 32
def set_fahrenheit(self, value):
self._fahrenheit = value
self._celsius = self.convert_to_celsius(value)
def set_celsius(self, value):
self._fahrenheit = self.convert_to_fahrenheit(value)
self._celsius = value
def get_fahrenheit(self):
return int(self._fahrenheit)
def get_celsius(self):
return int(self._celsius)
fahrenheit = property(get_fahrenheit, set_fahrenheit)
celsius = property(get_celsius, set_celsius)
Notice we have created two private variables on init: _fahrenheit and _celsius. And we have a getter and setter for each of these variables. Then at the bottom of the class we create the actual properties themselves passing in the getter and setter.
This gives us the functionality that makes the most sense.
In [1]: from temperature import Temperature
In [2]: t = Temperature()
In [3]: t.fahrenheit = 212
In [4]: t.celsius
Out[4]: 100
In [5]: t.celsius = 0
In [6]: t.fahrenheit
Out[6]: 32
In [7]: t.celsius = 200
In [8]: t.fahrenheit
Out[8]: 392
Of course, the user still has the ability to access your "private" variables, but this is where the "don't be an idiot" rule comes in to play.
We can't all have a proper beard, and many don't want one.