Class Internals
00:00
In the previous lesson, I introduced you to multiple inheritance. In this lesson, I’ll show you different bits about class internals and how you can use them to make more powerful code. In part one of this multi-part course, I showed you how the @property
and @.setter
decorators work to let you write code that makes methods behave like attributes.
00:20 This is part of something called the descriptor protocol. A protocol in Python is a loose collection of functions—or in this case, methods—that act as a promise.
00:30 If you implement their interface, Python will give you some functionality. Many things that seem like magic are actually implemented as protocols underneath, meaning you can write your own classes that behave the same way.
00:43 For example, both iteration and sequence functionality are built on protocols. The descriptor protocol is the one that makes methods act like attributes.
00:53
The @property
and @.setter
decorators are the simpler version of the descriptor protocol. They abstract away a deeper mechanism that uses dunder methods.
01:02
.__set_name__()
, .__get__()
, and .set()
are all used to implement the same features those decorators do, and you can override these methods and define your own behavior.
01:13 Lets go look at an example.
01:17 If you ever needed an integer that had a more positive outlook on life, well, let’s build one. One that doesn’t allow negative values. They clog up your aura.
01:27
You can write a class that abstracts a positive integer using the descriptor protocol. First off, .__set_name__()
is called when a descriptor is instantiated, and it’s passed the name of the variable it is being assigned to. For example, if you read a line of code that says radius = PositiveInteger
, instantiating a PositiveInteger
object and storing it in a reference called radius
, the name radius
gets passed into .__set_name__()
.
01:54
What you typically do with that value is store it for later use. I’m sticking it in ._name
, logically enough.
02:03
The core of what you do with the descriptor protocol is get and set values. .__get__()
is the get part. This method is past the object that the descriptor is attached to and the class of the descriptor itself.
02:16
That’d be the radius
and the Circle
class in the example I just mentioned. I’m not really sure why they bother passing in the class as you can always get it through self.__class__
, but it’s part of the protocol, so it has to be in the signature.
02:32
All the getter for PositiveInteger
needs to do is return the associated value. The value is on the instance object under the name associated with this descriptor.
02:43
.__dict__
is the dictionary associated with all Python objects where Python stores the objects attributes. It feels like there’s a lot going on in this line, but all it’s doing is getting the value from the associated instance object.
02:59
There are situations where this won’t work, but I’ll come back to that edge case in a later lesson. All right, you’ve got the getter. Now the setter. This signature takes the instance object associated with the value and the new value to set it to. The purpose of PositiveInteger
is to make sure that it can only store positive integer. This line enforces that.
03:23
Once the error checking’s out of the way, now I need to actually assign the value. This uses the same .__dict__
mechanism as the getter, but this time assigning the value to that dictionary.
03:36 Let me scroll down, and you’ll see how this gets used.
03:40
Circles are, so yesterday. Let’s define an Ellipse
. The ellipse has two radius-like things: a width and a height. Mathematically, there are a half dozen ways to specify an ellipse, but width and height are commonly used in drawing libraries, so let’s stick with that. Instead of specifying focal points, I want both my width and my height to be positive integers, so I use our newly minted class.
04:05
This is a little tricky, .width
and .height
here are class attributes, which means the instantiation of PositiveInteger
is associated with the Ellipse
’s class, not the objects.
04:18
This is how you register a descriptor. Inside Ellipse
’s .__init__()
, When I go to assign a value, the descriptor on the class is getting invoked, but because the descriptor’s code actually stores the contents on the object, the end result is a value associated with the object, not the class.
04:37 My brain hurts a little when I think about this. You don’t really need to understand this. When you use descriptors, you can treat it like magic. If you build a descriptor and assign it in the class, the end result is stored values with the same name on the object.
04:50 It isn’t magic though. There’s no extra special stuff going on here. From the interpreter’s point of view, it’s a class attribute that references a particular kind of instance object, which implements the descriptor, which when assigned to passes the value through the descriptor protocol to the owning instance object. Does it spoil the magic to know how the trick works?
05:13 Alright, magic or not, let’s see it in practice. Importing …
05:21
and I’ll create an ellipse
.
05:25 I’m gonna scroll the top window back up so that you can see the descriptor class. Since I put print statements in the descriptor, you can sort of see what’s going on.
05:35
The .__init__()
for Ellipse
assigns .width
and .height
. That assignment triggers .__set_name__()
for PositiveInteger
because it’s registered as a descriptor, as a class attribute. This gets called twice, once for .width
and once for .height
. Now, when I access the .width
attribute,
05:55
the print()
in the getter is issued and returns the value.
06:01
Same goes for .height
, and if I try to assign .height
,
06:08 I get the error checking in the setter enforcing our positive outlook. No negativity for you.
06:18
Dictionaries aren’t free. They take up more memory than just their contents. The attributes on an object are stored as a dictionary. By default, That dictionary is named .__dict__
.
06:31
If you don’t want the overhead cost of a dictionary associated with your class, you can slim it down using the special attribute .__slots__
.
06:40
What you put in .__slots__
is a tuple with the names of the attributes your class uses. Technically, it can be a list instead of a tuple, but that has extra overhead as well, and you’re never going to edit the contents. When .__slots__
is present, the underlying .__dict__
gets removed.
06:58
If you attempt to use .__dict__
, you’ll get an AttributeError
because it isn’t there, and of course, without .__dict__
, there is nowhere to put new attributes.
07:08
Attempting to add an attribute that isn’t in .__slots__
will also result in an error.
07:16
Consider this class containing an "x"
and a "y"
coordinate. By putting "x"
and "y"
in .__slots__
, I get rid of the underlying .__dict__
.
07:25 This saves a bit of memory, a little over 400 bytes per object. Remember earlier when I said I’ll save that for later in the lesson? Well, it’s later in the lesson.
07:36
The PositiveInteger
descriptor class worked by assuming the instance object it was associated with had a .__dict__
. If you use .__slots__
, that assumption isn’t valid.
07:48 I played around with this for a while trying to figure out if I could get the two things to work together. I found some code on the internet that showed a way to make it work, but it had two problems.
07:57
Problem one is it resulted in a new .__dict__
being created, which defeats the purpose of .__slots__
, and problem two was I couldn’t get it to work in Python 3.
08:06 Python 3 seems to be a bit more aggressive about the constraint. In theory, you could write a descriptor class that stored values in the descriptor class instead of on the instance object. As there can be multiple instance objects, you’d need to store that value in a nested dictionary with the top level keyed on the instance and the second level storing the values for that instance.
08:29
I haven’t tried this, but there’s no reason it wouldn’t work that I can think of. But if you get what I’m laying down, the short version of it is, I just said, reimplement .__dict__
, but somewhere else.
08:40
You’ll essentially be losing almost all the gains of .__slots__
. Not quite all, but close enough. The short version is don’t mix descriptors and slots.
08:49 It’s a recipe for disaster.
08:53 I hope you’re not tired of dunder stuff. More to come in the next lesson.
Become a Member to join the conversation.