Properties and Descriptors
00:00 In the previous lesson, you learned more about Python attributes. In this lesson, you’ll learn other ways of accessing an object’s content. Say you wanted to have a method, but you wanted it to look like an attribute.
00:13
Well, you can do that by wrapping the method with the @property
decorator. This allows you to do a calculation each time an attribute is referenced.
00:22 If you like, let me go show you an example.
00:27 New REPL, new class. This time I’m building a person.
00:32
.__init__()
takes two arguments, the first and last name of my person. And
00:42
there’s the boilerplate for storing .first
and .last
on the actual object.
00:53
The .full_name()
method is wrapped with the @property
decorator, which means you can access it like an attribute. Properties must not take any arguments besides self
.
01:04 This makes sense. If you’re accessing it as an attribute, you have no way of passing arguments in.
01:11
Inside the method, whatever I return will be the property’s value. Here I’m concatenating the first and last name together. Note how I’m accessing those attributes using self
inside the f-string.
01:24 This method is horribly Western-centric. The whole concept of first and last name is very European. Google falsehoods programmers believe about names for an education on these biases, or look up the RadioLab podcast episode called “Null” about the problems people with that last name have in the modern world. Anyhow, I’ll step off my soapbox.
01:47 That’s our class. Let’s create a person.
01:55
See what I mean? It isn’t like "of Rivia"
is actually his last name. Let’s access the first name attribute, then the last name, and now the full name.
02:10 It’s just like any other attribute. The property hides that this is a method from the user. Remember in a previous lesson when I commented on Asciimatic’s use of private values?
02:23 Well, that codebase uses properties as the public interface. Almost all attributes have underscores, while the property exposes the actual value. In some cases, this might just be a developer quirk. Hey, you all have them.
02:38 Notice I said you. I have no quirks at all. My foibles are severe enough to be promoted to full-blown aberrations. What was I saying? Right, developer quirk.
02:48
One reason for doing this has to do with caching. Say you had a value that had some complications to calculate. You could initialize it to None
, and then the first time someone used it, do the calculation.
03:01 Then you cache an internal copy, and on subsequent accesses, use that cached copy. You can sort of do this with a method, but as a property, you’re hiding away this implementation detail.
03:14
The @property
decorator is part of a deeper mechanism called the descriptor protocol. So far, you’ve just seen the simplest portion: using @property
to access a value.
03:25
You can use a similar mechanism for trapping the setting of a value. To do this, you decorate the method with the name of the attribute, then .setter
. This mechanism allows you to perform side effects when a value is set, for example. You can perform other calculations, update that cache I was talking about, or do error checks on the value.
03:48 All of these things could equally be done with a method, and in fact, in a lot of other languages, having getter and setter methods is common. This really is just syntactical sugar, but it can make your code a little more readable.
04:01
Rather than having .get_radius()
and .set_radius()
, you just have .radius
, and your code hides away what checking you need to do when the value is assigned.
04:11
A common pattern you’ll find in code is to pass a value to .__init__()
that gets stored in the class. Have that value stored as a non-public attribute, and then have a property with the same name but without the underscore and then a corresponding setter for that value. Inside the setter, you’re actually setting the non-public attribute as well as doing any error checking or other work, but from the perspective of the programmer using the object, it just looks like an attribute.
04:41 I’ve already told a couple of anecdotes relating to this. I’ll wait until at least the next lesson before I start repeating myself. Let’s go use a setter.
04:51
Here’s a new version of my Circle
class using ._radius
as a non-public attribute and a property named radius
. This would be that pattern I just mentioned.
05:08
And this is what a setter decorator looks like. Note that it uses the same name as the property that you’re setting and then the .setter
. Remember, the property method takes no additional arguments. Well, the setter does need an argument: the new value being set.
05:26 If you’re used to Python, you might notice there’s something a little weird going on here with the name. Both the property and the setter methods are named radius. Normally, this wouldn’t be allowed in Python.
05:37 Python doesn’t support method overloading in the same class. But decorators are kind of a special case. Whenever you see a decorator, it’s actually wrapping the thing you’re decorating with a function.
05:48
Although it says @radius
here, from the compiler’s perspective, it’s seeing something completely different. As weird as this is, it has become a common pattern.
05:57
It sort of hits home the fact that this is a setter for radius. You can call the method anything. Because it’s wrapped, it doesn’t really matter. Inside the method, I’m going to use the setter to do a little bit of an error checking. Here, I’m checking whether or not the contents of .value
is either an int
or a float
and is a positive number.
06:20
If you haven’t come across the notation with the pipe operator before, it was added in Python 3.10. It saves some typing. In the old days, you’d have to or
together two calls to isinstance()
. Isn’t progress grand?
06:34
Well, if the value being set isn’t an int
or a float
or it’s less than zero, I raise a ValueError
to tell them they can’t do that. If everything is okay, then I store the value on the non-public attribute. Let’s create a circle.
06:53 Once more, a small circle.
06:57 And there’s my radius. From the outside, it looks no different than the original code, but remember, it’s actually using a property here.
07:08 Just like a regular attribute, I can assign my radius,
07:12 and it takes on the new value. Of course, in this case, assigning the new value actually called the setter. How do I prove that? Well, by triggering the error check, of course.
07:28
Again, you can do this with methods, but this is so very clean. Programmers using Circle
don’t have to think about it. They can just use the radius naturally, and yet you still get all the advantages of a method that checks for illegal values. Okay, you’ve seen a bunch having to do with attributes.
Christopher Trudeau RP Team on Sept. 7, 2023
Hi Erik,
Without seeing the code you’re trying, I can only guess, but this one is a bit tricky. Make sure you have the leading underscore on self._radius
in __init__
, otherwise you’ll be calling the setter.
>>> class Circle:
... def __init__(self, radius):
... self._radius = radius # underscore is important here!
... @property
... def radius(self):
... return self._radius # value created in __init__
... @radius.setter
... def radius(self, value):
... if not isinstance(value, int | float) or value <= 0:
... raise ValueError("Radius must be a positive number")
... self._radius = value
There are two ways of using the descriptor protocol, the first calls the setter in __init__
and the second (the one shown here) uses a different variable referenced by both __init__
and the getter/setter methods.
In fact there is a drawback to the method shown here – it only checks for a valid radius in the setter, not in the initializer. You could do Circle('x')
and it would allow it.
IIRC, the first method is covered in part 2 of the course, soon to be released.
If this isn’t the problem you’re encountering, paste your code in response, and I can see what I can figure out.
Erick Kendall on Sept. 7, 2023
Yes, this was the code I was referring to. I completely missed the underscore in __init__
. Thank you!
Christopher Trudeau RP Team on Sept. 8, 2023
Yep, easy enough mistake. In fact when reading one of the other authors’ code when building the course I reached out to him telling him he missed it… not realizing until afterwards he was trying to do the other structure which explicitly uses the setter. This can be sneaky.
Khaled on Nov. 27, 2023
Is there a way to use the @property decorator against class variables as well?
I tried using getter and setter via @property decorator method for a class variable that stores a dictionary but it kept failing to update the class variable accordingly using the setter…
Become a Member to join the conversation.
Erick Kendall on Sept. 7, 2023
I’m initializing the class with
def __init__(self, radius)
yet I havedef radius(self,value)
- I’m not clear what’s happening when I create the classsmall = Circle(3)
- it’s as if I have two competing initializers