Special Methods
This apparent oddity is the tip of an iceberg that, when properly understood, is the key to everything we call Pythonic. The iceberg is called the Python data model, and it describes the API that you can use to make your own objects play well with the most idiomatic language features. - Luciano Ramalho (Fluent Python: clear, concise, and effective programming)
What's so special about the Python data model one may ask. Rather than giving a personal answer, why don't we do a little dive in and see what we can accomplish by understanding the data model. Data model simply speaks about how data is represented. In Python, data is represented by objects or to sound a bit technical, objects are Python's abstraction for data. The data model provides us with an API that allows our objects to play well with the 'under the hood' of Python programming.
In our little dive into Python data model, we are going to specifically focus on the special methods. Special methods are class functions with special names that are invoked by special syntax. Defining these special methods in our class definitions can give our class instances some really cool Python powers like iteration, operator overloading, working well with context managers (the 'with' keyword), proper string representation and formatting, and many more. To show you how you could implement these special functions into your classes, we will consider two examples of situations where using these special functions would make our codes clearer and more Pythonic.
The first example is a little bit outside-the-box solution I came up with for creating a simple game of Rock-Paper-Scissors in Python and the second is going to be a bit mathematical in nature but I'm going to walk you through each line of code
A Simple Game Of Rock Paper Scissors
Just in case you are not familiar with the Rock-Paper-Scissors game, it is originally a hand game usually played among two people that involves making signs of either Rock or paper or scissors. Knowing the whole history of the game doesn't really matter what is important is knowing how to determine the winner. In a conventional setting, a hand sign of rock would always win against scissors but will lose against paper, a hand sign of scissors would win against paper and lose to rock and obviously, paper would lose to scissors and win against rock. we can summarize this as shown below
For our Python emulation of this game, we will limit the number of players to just two, one player would be the computer and the other would be the user. Also, this is not a machine learning article or a write-up about computer vision, our users would still have to type in an option between rock, paper, and scissors on the terminal for our program to work.
Before we go into the actual coding, it's good that we take a step back and consider how we want our Python script to be. For my solution to this challenge, I will use the random module to enable the computer select a random option of either rock, paper, or scissors. To implement how our code evaluates the winner, I'm going to make the following assumptions:
I'm also going to take an OOP approach; our rock, paper, and scissors will be treated as objects and not string variables. Rather than creating three separate classes for each, I'll create only one that can represent any of them. This approach would also allow me to show you how special methods make life easier. Now to the fun aspect!
Class Definition
Naming our class RPS may sound a bit odd, but I found the name 'RPS' to be a good fit cause each letter comes from the initials, R for Rock, P for Paper, and S for Scissors. What's important to note here is that creating an instance of our class requires two arguments: pick and name. We already stated that the users of our script would have to type in their selected option on the terminal, instead of making our users type in 'Paper' (which could be so stressful for them) why don't we just allow our user to type in 'P' (or 'p') to select 'Paper', that's what the pick stands for. The name property is the actual name e.g 'Paper'. So now that we know what each parameters is for, we can now inspect our class by creating an instance
>>> p = RPS('P', 'Paper') # create an instance
>>> p.name
# return : Paper
>>> p.pick
# return : P
>>> print(p)
# return : <__main__.RPS object at 0x...>
Our class instance was created and has the right attributes but notice what we get when we try to print the contents of the variable holding our class instance. Before getting into the technical details of how our class instance returns the odd-looking string, let's update our class definition by adding a single special function and see the difference.
Now let's create an instance and try printing our class instance again
>>> p = RPS('P', 'Paper')
>>> print(p)
# return : RPS(P, Paper)
As we can see, by defining the 'repr' method we can achieve a better looking result. Let's make one more change to our class definition.
Now let's create an instance and test it again.
>>> p = RPS('P', 'Paper')
>>> p
# return : RPS(P, Paper)
>>> print(p)
# return : Paper
>>> str(p)
# return : 'Paper'
>>> repr(p)
# return : 'RPS(P, Paper)'
To know what's going on here, we need to know a little about the print function. The print function converts all non-keyword arguments(like our p variable) to string using the built-in Python class str
. If calling str()
on our variable fails, python falls back on the built-in repr
function. When str
is called on our object, it looks for a __str__
method, if it finds none, it fails and then searches for a __repr__
method. Both the __str__
and the __repr__
methods are special methods used for string representation of our object. The __repr__
method gives the official string representation of our object while the __str__
method gives a friendly string representation of our object. I usually say that the __repr__
method is like talking to another developer and it usually shows how to call our class and the __str__
is like talking to a user of our program (like the player in this case), you would usually just want to return a simple string like "Paper" to show the user what he picked.
Although I stated the __repr__
and __str__
as the two special functions in our class definition, there's actually a third special method, and yes it is the most common one, the __init__
function. It is used for initializing our class and called by the __new__
special method just before returning our class instance. Did I just mention another special method we haven't defined? yes, I did. It may also interest you to know that Python automatically adds some other special methods to our class. You can check them out by calling the built-in function dir
on our class instance like this
>>> dir(p)
# returns : ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'name', 'pick']
Special functions or methods can be identified by the way they are named, they always begin with double underscores '' and end with double underscores '' Because of this special way of naming these methods, they are commonly called daunder methods (Double + UNDERscores = DUNDER). So if a method name begins with double underscores, it is most likely but not certainly a special method. Why not certainly? this is simply because Python does not stop us from defining our own methods using the dunder syntax. Alright back to our game script.
All that's left for us now is for us to let our script know how to determine a winner. As stated earlier, I will use comparison to evaluate a winner.
# comparison logic
Rock > Scissors
Scissors > Paper
Paper > Rock
To implement this solution, I will add a dictionary and use the daunder greater_than method. The dictionary key would be the initials of Rock, Paper and Scissors. The value of each key would be the only other element that the key can defeat.
Notice the new lines of code, first the options dictionary and then the __gt__
method definition. With these new lines of code, let's see what new functionality our code now has.
# create a rock instance
>>> r = RPS('R', 'Rock')
# create a paper instance
>>> p = RPS('P', 'Paper')
# create a scissors instance
>>> s = RPS('S', 'Scissors')
>>> print(r,p,s)
# return : Rock Paper Scissors
>>> p > r # paper wins against rock
# return : True
>>> r > s # rock wins against scissors
# return : True
>>> s > p # scissors wins against paper
# return : True
>>> p < s # paper lose to scissors
# return : True
>>> p < r # paper lose to rock
# return : False
>>> p < s < r# paper lose to scissors which lose to rock
# return : True
>>> p >= r paper wins or tie to rock
# return : Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: '>=' not supported between instances of 'RPS' and 'RPS'
Just by adding to __gt__
special method, our class instances have gained magical powers (daunder methods are sometimes called magic methods and we can see why). By implementing the daunder gt method, our class instance now relates well with the >
and <
symbols but not the ≥
and ≤
symbols. The reason is that <
is just the negation of >
. The special method for <
is __lt__(self, x)
which can just be the negation of calling __gt__(self,x)
.
For the ≥ symbol, its special method is the __ge__(self, x)
and it must be defined for our object to work well with the ≥
sign. But in this program, we can do without it.
Another missing piece would be to check if two separate instances of Paper are equal.
>>> p1 = RPS("P", "Paper")
>>> p2 = RPS("P", "Paper")
>>> p3 = p1
>>> p1 == p2
# return : False
>>> p1 == p3
# return : True
>>> id(p1)
# return : 140197926465008
>>> id(p2)
# return : 140197925989440
>>> id(p3)
# return : 140197926465008
>>> id(p1) == id(p3)
# return : True
>>> id(p2) == id(p1)
# return False
The default operation of the equal comparison sign is to compare the id of the object. p1 and p2 are different class instances that happen to have the same attributes but their id differs and therefore are not equal. When we assign a variable to a class instance, we make that variable point to the address of the instance which is what we observe for p3 which has the same id as p1. We have the option of overriding how the equality comparison works on our object by defining and implementing our own __eq__(self, other)
method. But for this script, I will compare two instances using their pick attribute. Now that we have defined our class and know how it works, we are now ready to see the full implementation of the Python script
Putting It All Together
Let me walk you through the code. We are already familiar with the RPS class definition. If you recall, our code is meant to allow the computer to select choices at random and that's what the random module is for. The random module makes available the choice
function which allows the 'random' selection of an element from an iterable object e.g. a list in Python. The list in this case is the option_list
. Because our class is made to work with uppercase letters for comparison, it is necessary that we always initialize our objects with uppercase for the pick attribute. This is why we first convert the user's input to upper case (line 34) with the .upper()
. It is also possible that our user types in an unexpected character like 'Q' so we have to validate our user input by checking if the uppercase character is part of the valid options in option_list
. The mapping dictionary allows us to quickly convert the user's input to a corresponding instance of RPS after being validated. The evaluate_winner function makes use of the comparison symbol to determine the winner. Because we want the code to run in a loop until a winner is found, we make use of a while loop and when a winner is found, the evaluate_winner function returns True which will then break the loop and exit the game.
Here is one of the various results of running the code
Our Python code runs as expected, although there could be a couple of improvements or new features to add. The most important thing is that we see how using special methods in our class definition gives our code a more Pythonic feel. Assuming we were to take a different approach such as using nested if statements, our evaluate_winner method would look something like this
def evaluate_winner(user_choice, comp_choice):
# check if user choice is 'R'
if user_choice == 'R':
# check if comp_choice is 'R'
if comp_choice == 'R':
# it is a tie
...
elif comp_choice == 'S':
# user wins
...
else:
# computer wins
...
if ...
# do the same for when user_choice is 'S' and then for
# when user_choice is 'P'
A problem with this approach other than the lengthy code is that if we desire to add a new element, diamond which can beat both rock and scissors but not paper (for an unknown reason), our if statements would begin to look really awkward. Whereas in our OOP approach, all we have to do is to modify the options dict like so
options = {"R" : ["S"], "P" : ["R"], "S" : ["P"], "D" : ["R", "S"]}
and then we change the if statement in __gt__
to be
def __gt__(self,x):
if x.pick in self.options[self.pick]:
return True
else:
return False
we can make the statement shorter
def __gt__(self, x):
return True if x.pick in self.options[self.pick] else False
To conclude, here are some things you should note about using special methods:
You hardly (or never) call them directly yourself, let Python do the calling for you
When defining functions that use the dunder naming syntax, you should consider that Python could one day define such a function and give it a different meaning. This could break your code or make it behave in unexpected ways
You certainly don't have to implement every special method there is. Just a couple that you are really sure you need. Remember, simple is better than complex. If there's a simpler way you should use that instead
To learn more about the Python data model I recommend the official Python documentation.
I'll also recommend reading Fluent Python by Luciano Ramalho
This is the first part of the topic, in the next part, we are going to be dealing with operator overloading and making iterable objects
Hope you enjoyed this article!!!
Top comments (0)