Transcript
How can we make a read-only attribute in Python?
A property is like an auto-updating attribute
We have a class called Square
which accepts a length
and an optional color
and stores those as length
and color
attributes:
class Square:
def __init__(self, length, color=None):
self.length = length
self.color = color
def __repr__(self):
return f"Square({self.length!r}, {self.color!r})"
@property
def area(self):
return self.length**2
If we make a Square
object (with just a length in this case):
>>> s1 = Square(4)
We can see the length and color in the string representation of this Square
object:
>>> s1
Square(4, None)
This class also has an area
property.
@property
def area(self):
return self.length**2
A property is a kind of like a virtual attribute.
Every time we access the area
attribute, a function will be executed and its return value will be given back to us:
>>> s1.area
16
That function call happens automatically, simply by accessing the area
attribute.
So if we change the length
of a Square
object:
>>> s1.length = 3
The area
will seem to change automatically:
>>> s1.area
9
Properties can make read-only attributes
What if we wanted to make it so the color
of our Square
objects can be changed but the length
and area
can't be changed?
>>> s1.color = 'purple'
>>> s1.color
'purple'
We want to make the length
and the area
attributes read-only.
It turns out the area
attribute is already read-only:
>>> s1.area = 100
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
We can't assign to area
because properties are read-only by default.
We could try to make our length
attribute into a property, by adding this to our class definition:
@property
def length(self):
return self.length
We're using the property
decorator to create a property named length
and this property returns the length
attribute when accessed.
This length
property doesn't quite work though:
>>> s1 = Square(4)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/trey/shapes.py", line 3, in __init__
self.length = length
AttributeError: can't set attribute
It doesn't work because in our initializer method we assigned to length
:
def __init__(self, length, color=None):
self.length = length
self.color = color
But length
is a property that doesn't have a setter, so it's read-only (all properties are read-only by default).
Our property is accessing data that's stored in the same places as our property. That's not possible! There's nothing to distinguish our property name from an attribute of the same name, so we have a name collision.
We need our property name to be different from the attribute where we actually store our data. We don't want to rename our property, so let's rename our attribute.
Using an underscore prefix for internal attributes
We need an attribute that has a different name than length
.
When you have an attribute that represents internal data that shouldn't be accessed directly (by code outside of your class) it's common in Python to prefix a single underscore (_
) before that attribute name.
PEP 8 notes this convention as well: "Use one leading underscore only for non-public methods and instance variables."
So we'll rename the attribute that stores our length to _length
: So, we'll say:
def __init__(self, length, color=None):
self._length = length
self.color = color
And our property will access self._length
now to get the length:
@property
def length(self):
return self._length
Here's the full class:
class Square:
def __init__(self, length, color=None):
self._length = length
self.color = color
def __repr__(self):
return f"Square({self.length!r}, {self.color!r})"
@property
def area(self):
return self.length**2
@property
def length(self):
return self._length
Now when we make a Square
object, we'll see that there is a length
:
>>> s1 = Square(4)
>>> s1.length
4
But we can't assign to it because it's a property:
>>> s1.length = 3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
The single underscore prefix is just a convention
Note that this single underscore prefix is just a convention. So we can still access the _length
attribute if we want to:
>>> s1._length
4
And in fact we can even change it:
>>> s1._length = 5
Which changes the length
on our Square
object:
>>> s1.length
5
But when other Python programmers see an assignment to an underscore-prefixed attribute, they'll look at that code and think something strange is going on: we're changing some internal details of an object. An underscore-prefixed attribute is supposed to be private by convention, so it's uncommon to see an assignment to an underscore-prefixed attribute.
Summary
If you need to make a read-only attribute in Python, you can turn your attribute into a property that delegates to an attribute with almost the same name, but with an underscore prefixed before the its name to note that it's private convention.
from Planet Python
via read more
No comments:
Post a Comment