Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

This lesson is for members only. Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Hint: You can adjust the default video playback speed in your account settings.
Hint: You can set your subtitle preferences in your account settings.
Sorry! Looks like there’s an issue with video playback 🙁 This might be due to a temporary outage or because of a configuration issue with your browser. Please see our video player troubleshooting guide to resolve the issue.

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.