Typefully

Object-Oriented Programming Series: 6. Special Methods (Dunder Methods)

Avatar

Share

 • 

3 years ago

 • 

View on X

Object-Oriented Python at the Hogwarts School of Codecraft and Algorithmancy --- Year 6: Special Methods (aka Dunder Methods) --- It's Year 6. Students will dive deeper into what's happening inside classes. This Year focusses on special methods Here are our classes so far
In previous years, you've seen how you can create classes and give them all the attributes you need But let's see at some things that are still missing. Let's create a couple of students and see whether we can use `>` in the same way we can on other data types, such as ints
You cannot use `>` with instances of `Student` Not yet, in any case And this shouldn't be surprising. After all, what does "is Hermione greater than Malfoy" mean? Height, age, skill, something else?
The answer: it's up to you, the person writing the class, to decide There's a special method you can define in a class that let's your program know what to do when you compare two objects using `>` This special method is `__gt__()` which stands for 'greater than'
Let's decide we're going to use their skill for comparison. This makes sense in the wizarding world So, shall we define the `__gt__()` special method for the `Student` class, then? Great, let's go ahead and do this…
But wait a moment. Do we want something similar for professors, too? Recall from Year 5 that both `Student` and `Professor` inherit from the parent class, `Wizard` Here's Year 5 again in case you need to repeat the Year twitter.com/s_gruppetta_ct/status/1643648452331700224
In that case, let's define this special method in `Wizard`. This makes it available to all child classes that inherit from `Wizard` This special method has two parameters. The first one is `self`, which you're used to by know as we've seen this is all methods so far
We often name the second one `other` Recall that a method is attached to an object. However, to compare that object to see if it's greater than…you need an 'other' object This method needs to return a Boolean. In this case, we compare the two instances' `.skill` attributes
The `.skill` data attribute contains a float. Therefore, it is possible to use the `>` symbol which works fine for floats So now, comparing two wizards using the `>` operator is the same as comparing their skills And since students are wizards, this applies to students, too
All `Wizard` objects are created with a default skill of 0.2 So let's give Hermione a higher skill. She deserves it!
You can complete the set with similar methods Here's the full list of these comparison and equality special methods: .__lt__() less than .__le__() less than or equal to .__eq__() equal to .__ne__() not equal to .__gt__() greater than .__ge__() greater than or equal to
There are a couple of shortcuts for not having to define all of these all the time, such as the `total_ordering` decorator in `functools` or using data classes if appropriate But that's a topic for after graduation from Hogwarts School of Codecraft and Algorithmancy
There are more special methods But first… You learned about methods in Year 3. But here I'm using the term "special methods" The special methods all have fixed names—you can't choose the name, unlike with other instance methods
And they all have double underscores at the beginning and double underscores at the end of their names For this reason, we often informally call them dunder methods because of the Double UNDERscores in their names
You'll occasionally see them referred to as "magic" methods—you may think this is appropriate given the theme of this series! However, there's nothing "magic" about these methods so I share the view many others have that this is not a great way of referring to them
Let's also define the `__eq__()` special method which determines when two objects are equal to each other Let's decide that two wizards are "equal" if they have the same name, patronus, and birth_year
Let's assume you create two instances for the same wizard. These are two separate instances. You can confirm this by using `is` which checks whether two objects are the exact same object However, the instances are equal, as you see when you use `==`
You can read more about the difference between `is` and `==` in the £5 note analogy: twitter.com/s_gruppetta_ct/status/1622914546783453185
I won't cover every possible special method in this thread (thankfully!) But let's look at a few more. Let's create a few more students, a house, and assign the students to the house
This is all well and good…until you get to the `for` loop at the end. There's a `TypeError` telling you that a `House` object is not iterable—you can't go through each "item" within it one at a time Of course it can't. How does it know what "item" you're referring to?
You look at the data attributes you defined in `House` You may be thinking: "Isn't it obvious I want to iterate through the list of house members—what else could it be?" Well, it may be obvious to you. But you need to tell your class about that… Enter `__iter__()`
This special method tells the class how to create an iterator that can be used whenever you need to iterate, such as in a `for` loop In this case, we can "cheat" and rely on the fact that `self.members` is a list and we can get its iterator, which is what we need
When you define `__iter__()`, you make the objects of the class iterable You can read more about iterables and iterators in the Data Structure Series—Days 1 and 6 talk about these structures twitter.com/s_gruppetta_ct/status/1628341581538549760
Let's run `making_magic.​py` again to see whether the `for` loop works now And yes—hurray! It works…
…well, sort of This brings up another issue Here's the output from the `for` loop: <hogwarts_magic.Student object at 0x103273110> <hogwarts_magic.Student object at 0x103273150> <hogwarts_magic.Student object at 0x103273210> Hmmm? Who's who?
Python doesn't know how to display the object when you print it. By default (in CPython), it shows the module and class name and the memory location So, you know those are three different objects since they have different memory addresses But that's not that useful!
We can define the `__str__()` dunder method to create a user-friendly representation of the object. What would you like the program's user to see when you print out the object? Let's make the addition in `Wizard` The `__str__()` special method should return a string
And now, running `making_magic.​py` again, without any changes, gives a more informative output
But, we've learnt we can override any method in a subclass Let's make this string representation more informative for students The `__str__()` method in `Student` now includes the house and year the student is in But, can you spot what the issue will be?
Here's the output We've fallen foul of Python not knowing how a `House` object should be displayed
But you know what to do… Add the `__str__()` method to `House`
I'll wrap up, but not before talking about another special method that also creates a string representation of an object—but this one is meant for programmers It provides more specific information When possible, this method returns a string you could use to re-create object
This is the `__repr__()` method Let's add it to `Wizard` first One place you'll see this representation rather than the one from `__str__()` is when you use the built-in `repr()` function But another is in the Console/REPL…
So, let's test this in a Console/REPL session When you just type `harry` and execute, you get the `__repr__()` string representation, unlike in the case of `print()` when you get the `__str__()` one
But, we've used the `Wizard` class for Harry. Let's see what happens when we use `Student` instead Hmmm! `Student` doesn't have its own `__repr__()` method so it reverts to the one in `Wizard`
So, although this is technically correct – Harry is a wizard – it misses the key fact that this is an instance of `Student`, the child class You _could_ define a new `__str__()` for `Student` (and another for `Professor` and any other class which inherits from `Wizard`)
But that's too much work… Instead we can use this solution `type(self)` finds the type of the object and the `.__name__` attribute of the `type` class is a string with the name of the class Therefore, this gets the name of whatever class the object is
These representations now show the correct class name However, if you try to use these outputs to create the objects, you'll see there's a missing argument in each one A good idea is to turn the "additional" arguments needed by `Student` and `Professor` into optional args
To read more about the `__str__()` and `__repr__()` special methods, have a look at the recent article I wrote for @realpython on this topic: realpython.com/python-repr-vs-str/
Year 6 was long—sorry --- Year 7: Final Year
Avatar

Stephen Gruppetta

@s_gruppetta_ct

Constantly looking for innovative ways to talk and write about Python • Mentoring learners • Writing about Python • Writing about technical writing