16.2. Python’s Dunder Methods

Python has several special method names that begin and end with double underscores, abbreviated as dunder. These methods are called dunder methods, special methods, or magic methods. You’re already familiar with the __init__() dunder method name, but Python has several more. We often use them for operator overloading—that is, adding custom behaviors that allow us to use objects of our classes with Python operators, such as + or >= . Other dunder methods let objects of our classes work with Python’s built-in func- tions, such as len() or repr() .

As with __init__() or the getter, setter, and deleter methods for prop- erties, you almost never call dunder methods directly. Python calls them behind the scenes when you use the objects with operators or built-in func- tions. For example, if you create a method named len() or repr() for your class, they’ll be called behind the scenes when an object of that class is passed to the len() or repr() function, respectively. These methods are doc- umented online in the official Python documentation at https://docs.python .org/3/reference/datamodel.html.

As we explore the many different types of dunder methods, we’ll expand our WizCoin class to take advantage of them. ## String Representation Dunder Methods You can use the __repr_() and __str__() dunder methods to create string representations of objects that Python typically doesn’t know how to han- dle. Usually, Python creates string representations of objects in two ways. The repr (pronounced “repper”) string is a string of Python code that, when run, creates a copy of the object. The str (pronounced “stir”) string is a human-readable string that provides clear, useful information about the object. The repr and str strings are returned by the repr() and str() built-in functions, respectively. For example, enter the following into the interactive shell to see a datetime.date object’s repr and str strings:

>>> import datetime
>>> newyears = datetime.date(2021, 1, 1)
>>> repr(newyears)
'datetime.date(2021, 1, 1)'
>>> str(newyears)
'2021-01-01'
>>> newyears
datetime.date(2021, 1, 1)

In this example, the ‘datetime.date(2021, 1, 1)’ repr string of the date time.date object 2 is literally a string of Python code that creates a copy of that object 1. This copy provides a precise representation of the object. On the other hand, the ‘2021-01-01’ str string of the datetime.date object 3 is a string representing the object’s value in a way that’s easy for humans to read. If we simply enter the object into the interactive shell 4, it displays the repr string. An object’s str string is often displayed to users, whereas an object’s repr string is used in technical contexts, such as error messages and logfiles.

Python knows how to display objects of its built-in types, such as inte- gers and strings. But it can’t know how to display objects of the classes we create. If repr() doesn’t know how to create a repr or str string for an object, by convention the string will be enclosed in angle brackets and contain the object’s memory address and class name: ‘<wizcoin.WizCoin object at 0x00000212B4148EE0>’ . To create this kind of string for a WizCoin object, enter the following into the interactive shell:

>>> import wizcoin
>>> purse = wizcoin.WizCoin(2, 5, 10)
>>> str(purse)
'<wizcoin.WizCoin object at 0x00000212B4148EE0>'
>>> repr(purse)
'<wizcoin.WizCoin object at 0x00000212B4148EE0>'
>>> purse
<wizcoin.WizCoin object at 0x00000212B4148EE0>

These strings aren’t very readable or useful, so we can tell Python what strings to use by implementing the __repr__() and __str() dunder methods. The __repr() method specifies what string Python should return when the object is passed to the repr() built-in function, and the __str__() method specifies what string Python should return when the object is passed to the str() built-in function. Add the following to the end of the wizcoin.py file:

--snip-- def __repr__(self): """Returns a string of an expression that re-creates this object."""

return f'{self.__class__.__qualname__}({self.galleons}, {self.sickles}, {self.knuts})'

def __str__(self): """Returns a human-readable string representation of this object."""

return f'{self.galleons}g, {self.sickles}s, {self.knuts}k'

When we pass purse to repr() and str() , Python calls the __repr__() and __str__() dunder methods. We don’t call the dunder methods in our code. Note that f-strings that include the object in braces will implicitly call str() to get an object’s str string. For example, enter the following into the interactive shell:

>>> import wizcoin
>>> purse = wizcoin.WizCoin(2, 5, 10)
>>> repr(purse) # Calls WizCoin's __repr__() behind the scenes.
'WizCoin(2, 5, 10)'
>>> str(purse) # Calls WizCoin's __str__() behind the scenes.
'2g, 5s, 10k'
>>> print(f'My purse contains {purse}.') # Calls WizCoin's __str__().
My purse contains 2g, 5s, 10k.

When we pass the WizCoin object in purse to the repr() and str() func- tions, behind the scenes Python calls the WizCoin class’s __repr__() and __str__ () methods. We programmed these methods to return more readable and useful strings. If you entered the text of the ‘WizCoin(2, 5, 10)’ repr string into the interactive shell, it would create a WizCoin object that has the same attributes as the object in purse . The str string is a more human-readable rep- resentation of the object’s value: ‘2g, 5s, 10k’ . If you use a WizCoin object in an f-string, Python uses the object’s str string.

If WizCoin objects were so complex that it would be impossible to create a copy of them with a single constructor function call, we would enclose the repr string in angle brackets to denote that it’s not meant to be Python code. This is what the generic representation strings, such as ‘<wizcoin. WizCoin object at 0x00000212B4148EE0>’ , do. Typing this string into the inter- active shell would raise a SyntaxError , so it couldn’t possibly be confused for Python code that creates a copy of the object.

Inside the __repr__() method, we use self._:raw-latex:class.__qualname instead of hardcoding the string ‘WizCoin’ ; so if we subclass WizCoin , the inherited __repr() method will use the subclass’s name instead of ‘WizCoin’ . In addi- tion, if we rename the WizCoin class, the __repr() method will automatically use the updated name.

But the WizCoin object’s str string shows us the attribute values in a neat, concise form. I highly recommended you implement __repr__() and __str__() in all your classes.

16.2.1. SE NSITI V E INFORM ATION IN RE PR S TRINGS

As mentioned earlier, we usually display the str string to users, and we use the repr string in technical contexts, such as logfiles. But the repr string can cause security issues if the object you’re creating contains sensitive information, such as passwords, medical details, or personally identifiable information. If this is the case, make sure the __repr__() method doesn’t include this information in the string it returns. When software crashes, it’s frequently set up to include the contents of variables in a logfile to aid in debugging. Often, these logfiles aren’t treated as sensitive information. In several security incidents, publicly shared logfiles have inadvertently included passwords, credit card numbers, home addresses, and other sensitive information. Keep this in mind when you’re writing __repr__() methods for your class. ## Numeric Dunder Methods The numeric dunder methods, also called the math dunder methods, overload Python’s mathematical operators, such as + , - , * , / , and so on. Currently, we can’t perform an operation like adding two WizCoin objects together with the + operator. If we try to do so, Python will raise a TypeError exception, because it doesn’t know how to add WizCoin objects. To see this error, enter the following into the interactive shell:

>>> import wizcoin
>>> purse = wizcoin.WizCoin(2, 5, 10)
>>> tipJar = wizcoin.WizCoin(0, 0, 37)
>>> purse + tipJar
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'WizCoin' and 'WizCoin'

Instead of writing an addWizCoin() method for the WizCoin class, you can use the __add__() dunder method so WizCoin objects work with the + opera- tor. Add the following to the end of the wizcoin.py file:

--snip-- def __add__(self, other): """Adds the coin amounts in two WizCoin objects together.""" 2 if not isinstance(other, WizCoin): return NotImplemented 1 3 return WizCoin(other.galleons + self.galleons, other.sickles + self.sickles, other.knuts + self.knuts)

When a WizCoin object is on the left side of the + operator, Python calls the __add__() method 1 and passes in the value on the right side of the + operator for the other parameter. (The parameter can be named anything, but other is the convention.)

Keep in mind that you can pass any type of object to the __add__() method, so the method must include type checks 2. For example, it doesn’t make sense to add an integer or a float to a WizCoin object, because we don’t know whether it should be added to the galleons , sickles , or knuts amount.

The __add__() method creates a new WizCoin object with amounts equal to the sum of the galleons , sickles , and knuts attributes of self and other 3. Because these three attributes contain integers, we can use the + operator on them. Now that we’ve overloaded the + operator for the WizCoin class, we can use the + operator on WizCoin objects.

Overloading the + operator like this allows us to write more readable code. For example, enter the following into the interactive shell:

>>> import wizcoin
>>> purse = wizcoin.WizCoin(2, 5, 10) # Create a WizCoin object.
>>> tipJar = wizcoin.WizCoin(0, 0, 37) # Create another WizCoin object.
>>> purse + tipJar # Creates a new WizCoin object with the sum amount.
WizCoin(2, 5, 47)

If the wrong type of object is passed for other , the dunder method shouldn’t raise an exception but rather return the built-in value NotImplemented . For example, in the following code, other is an integer:

>>> import wizcoin
>>> purse = wizcoin.WizCoin(2, 5, 10)
>>> purse + 42 # WizCoin objects and integers can't be added together.
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'WizCoin' and 'int'

Returning NotImplemented signals Python to try calling other methods to perform this operation. (See “Reflected Numeric Dunder Methods” later in this chapter for more details.) Behind the scenes, Python calls the __add__() method with 42 for the other parameter, which also returns NotImplemented , causing Python to raise a TypeError .

Although we shouldn’t be able to add integers to or subtract them from WizCoin objects, it would make sense to allow code to multiply WizCoin objects by positive integer amounts by defining a __mul__() dunder method. Add the following to the end of wizcoin.py:

--snip-- def __mul__(self, other): """Multiplies the coin amounts by a non-negative integer.""" if not isinstance(other, int): return NotImplemented if other < 0: # Multiplying by a negative int results in negative # amounts of coins, which is invalid. raise WizCoinException('cannot multiply with negative integers') return WizCoin(self.galleons * other, self.sickles * other, self.knuts * other)

This __mul__() method lets you multiply WizCoin objects by positive inte- gers. If other is an integer, it’s the data type the __mul__() method is expect- ing and we shouldn’t return NotImplemented . But if this integer is negative, multiplying the WizCoin object by it would result in negative amounts of coins in our WizCoin object. Because this goes against our design for this class, we raise a WizCoinException with a descriptive error message. ### NOTE You shouldn’t change the self object in a numeric dunder method. Rather, the method should always create and return a new object. The + and other numeric operators are always expected to evaluate to a new object rather than modifying an object’s value in-place.

Enter the following into the interactive shell to see the__mul__() dunder method in action:

>>> import wizcoin
>>> purse = wizcoin.WizCoin(2, 5, 10) # Create a WizCoin object.
>>> purse * 10 # Multiply the WizCoin object by an integer.
WizCoin(20, 50, 100)
>>> purse * -2 # Multiplying by a negative integer causes an error.
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "C:\Users\Al\Desktop\wizcoin.py", line 86, in __mul__
raise WizCoinException('cannot multiply with negative integers')
wizcoin.WizCoinException: cannot multiply with negative integers

Table 17-1 shows the full list of numeric dunder methods. You don’t always need to implement all of them for your class. It’s up to you to decide which methods are relevant.

Table 17-1: Numeric Dunder Methods

Dunder method

Operation

Operator or built-in function

__add__()

Addition

__sub__()

Subtraction

__mul__()

Multiplication

*

__matmul__()

Matrix multiplication (new in Python 3.5 )

@

__truediv__()

Division

/

__floordiv__()

Integer division

//

__mod__()

Modulus

%

__divmod__()

Division and modulus

divmod()

__pow__()

Exponentiation

**, pow()

__lshift__()

Left shift

>>

__rshift__()

Right shift

<<

__and__()

Bitwise and

&

__or__()

Bitwise or

__xor__()

Bitwise exclusive or

^

__neg__()

Negation

Unary -, as in -42

__pos__()

Identity

Unary +, as in +42

__abs__()

Absolute value

abs()

__invert__()

Bitwise inversion

~

__complex__()

Complex number form

complex()

__int__()

Integer number form

int()

__float__()

Floating-point number form

float()

__bool__()

Boolean form

bool()

__round__()

Rounding

round()

__trunc__()

Truncation

math.trunc()

__floor__()

Rounding down

math.floor()

__ceil__()

Rounding up

math.ceil()

Some of these methods are relevant to our WizCoin class. Try writing your own implementation of the __sub__() , __pow() , __int() , __float() , and __bool() methods. You can see an example of an implementation at https://autbor.com/wizcoinfull. The full documentation for the numeric dunder methods is in the Python documentation at https://docs.python.org/3/ reference/datamodel.html#emulating-numeric-types.

The numeric dunder methods allow objects of your classes to use Python’s built-in math operators. If you’re writing methods with names like multiplyBy() , convertToInt() , or something similar that describes a task typically done by an existing operator or built-in function, use the numeric dunder methods (as well as the reflected and in-place dunder methods described in the next two sections). ## Reflected Numeric Dunder Methods Python calls the numeric dunder methods when the object is on the left side of a math operator. But it calls the reflected numeric dunder methods (also called the reverse or right-hand dunder methods) when the object is on the right side of a math operator.

Reflected numeric dunder methods are useful because programmers using your class won’t always write the object on the left side of the opera- tor, which could lead to unexpected behavior. For example, let’s consider what happens when purse contains a WizCoin object, and Python evaluates the expression 2 * purse , where purse is on the right side of the operator: 1. Because 2 is an integer, the int class’s __mul__() method is called with purse passed for the other parameter. 2. The int class’s __mul() method doesn’t know how to handle WizCoin objects, so it returns NotImplemented . 3. Python doesn’t raise a TypeError just yet. Because purse contains a WizCoin object, the WizCoin class’s __rmul() method is called with 2 passed for the other parameter. 4. If __rmul__() returns NotImplemented , Python raises a TypeError .

Otherwise, the returned object from __rmul__() is what the 2 * purse expression evaluates to.

But the expression purse * 2 , where purse is on the left side of the opera- tor, works differently: 1. Because purse contains a WizCoin object, the WizCoin class’s __mul__() method is called with 2 passed for the other parameter. 2. The __mul__() method creates a new WizCoin object and returns it. 3. This returned object is what the purse * 2 expression evaluates to.

Numeric dunder methods and reflected numeric dunder methods have identical code if they are commutative. Commutative operations, like addi- tion, have the same result backward and forward: 3 + 2 is the same as 2 + 3. But other operations aren’t commutative: 3 – 2 is not the same as 2 – 3. Any commutative operation can just call the original numeric dunder method whenever the reflected numeric dunder method is called. For example, add the following to the end of the wizcoin.py file to define a reflected numeric dunder method for the multiplication operation:

--snip-- def __rmul__(self, other): """Multiplies the coin amounts by a non-negative integer.""" return self.__mul__(other)

Multiplying an integer and a WizCoin object is commutative: 2 * purse is the same as purse * 2 . Instead of copying and pasting the code from __mul__() , we just call self.__mul__() and pass it the other parameter. After updating wizcoin.py, practice using the reflected multiplication dunder method by entering the following into the interactive shell:

>>> import wizcoin
>>> purse = wizcoin.WizCoin(2, 5, 10)
>>> purse * 10 # Calls __mul__() with 10 for the `other` parameter.
WizCoin(20, 50, 100)
>>> 10 * purse # Calls __rmul__() with 10 for the `other` parameter.
WizCoin(20, 50, 100)

Keep in mind that in the expression 10 * purse , Python first calls the int class’s __mul__() method to see whether integers can be multiplied with WizCoin objects. Of course, Python’s built-in int class doesn’t know anything about the classes we create, so it returns NotImplemented . This signals to Python to next call WizCoin class’s __rmul() , and if it exists, to handle this opera- tion. If the calls to the int class’s __mul() and WizCoin class’s__rmul__() both return NotImplemented , Python raises a TypeError exception.

Only WizCoin objects can be added to each other. This guarantees that the first WizCoin object’s__add__() method will handle the operation, so we don’t need to implement__radd() . For example, in the expression purse + tipJar , the__add() method for the purse object is called with tipJar passed for the other parameter. Because this call won’t return NotImplemented , Python doesn’t try to call the tipJar object’s __radd__() method with purse as the other parameter.

Table 17-2 contains a full listing of the available reflected dunder methods.

Table 17-2: Reflected Numeric Dunder Methods

Dunder method

Operation

Operator or built-in function

__radd__()

Addition

__rsub__()

Subtraction

__rmul__()

Multiplication

*

__rmatmul__()

Matrix multiplication (new in Python 3.5)

@

__rtruediv__()

Division

/

__rfloordiv__()

Integer division

//

__rmod__()

Modulus

%

__rdivmod__()

Division and modulus

divmod()

__rpow__()

Exponentiation

**, pow()

__rlshift__()

Left shift

>>

__rrshift__()

Right shift

<<

__rand__()

Bitwise and

&

__ror__()

Bitwise or

__rxor__()

Bitwise exclusive or

^

The full documentation for the reflected dunder methods is in the Python documentation at https://docs.python.org/3/reference/datamodel.html #emulating-numeric-types. ## In-Place Augmented Assignment Dunder Methods The numeric and reflected dunder methods always create new objects rather than modifying the object in-place. The in-place dunder methods, called by the augmented assignment operators, such as += and * = , modify the object in-place rather than creating new objects. (There is an exception to this, which I’ll explain at the end of this section.) These dunder method names begin with an i, such as __iadd__() and __imul__() for the += and *= operators, respectively.

For example, when Python runs the code purse *= 2 , the expected behav- ior isn’t that the WizCoin class’s __imul__() method creates and returns a new WizCoin object with twice as many coins, and then assigns it the purse variable. Instead, the __imul__() method modifies the existing WizCoin object in purse so it has twice as many coins. This is a subtle but important difference if you want your classes to overload the augmented assignment operators.

Our WizCoin objects already overload the + and * operators, so let’s define the __iadd__() and __imul() dunder methods so they overload the += and = operators as well. In the expressions purse += tipJar and purse= 2 , we call the __iadd() and imul() methods, respectively, with tipJar and 2 passed for the other parameter, respectively. Add the following to the end of the wizcoin.py file:

--snip-- def __iadd__(self, other): """Add the amounts in another WizCoin object to this object.""" if not isinstance(other, WizCoin): return NotImplemented # We modify the self object in-place: self.galleons += other.galleons self.sickles += other.sickles self.knuts += other.knuts return self # In-place dunder methods almost always return self. def __imul__(self, other): """Multiply the amount of galleons, sickles, and knuts in this object by a non-negative integer amount.""" if not isinstance(other, int): return NotImplemented if other < 0: raise WizCoinException('cannot multiply with negative integers') # The WizCoin class creates mutable objects, so do NOT create a # new object like this commented-out code: #return WizCoin(self.galleons * other, self.sickles * other, self.knuts * other) # We modify the self object in-place: self.galleons *= other self.sickles *= other self.knuts *= other return self # In-place dunder methods almost always return self.

The WizCoin objects can use the += operator with other WizCoin objects and the *= operator with positive integers. Notice that after ensuring that the other parameter is valid, the in-place methods modify the self object in-place rather than creating a new WizCoin object. Enter the following into the interactive shell to see how the augmented assignment operators mod- ify the WizCoin objects in-place:

>>> import wizcoin
>>> purse = wizcoin.WizCoin(2, 5, 10)
>>> tipJar = wizcoin.WizCoin(0, 0, 37)
>>> purse + tipJar
WizCoin(2, 5, 46)
>>> purse
WizCoin(2, 5, 10)
>>> purse += tipJar
>>> purse
WizCoin(2, 5, 47)
>>> purse *= 10
>>> purse
WizCoin(20, 50, 470)

The + operator 1 calls the __add__() or __radd__() dunder methods to create and return new objects 2. The original objects operated on by the + operator remain unmodified. The in-place dunder methods 3 4 should modify the object in-place as long as the object is mutable (that is, it’s an object whose value can change). The exception is for immutable objects: because an immutable object can’t be modified, it’s impossible to modify it in-place. In that case, the in-place dunder methods should create and return a new object, just like the numeric and reflected numeric dunder methods.

We didn’t make the galleons , sickles , and knuts attributes read-only, which means they can change. So WizCoin objects are mutable. Most of the classes you write will create mutable objects, so you should design your in- place dunder methods to modify the object in-place.

If you don’t implement an in-place dunder method, Python will instead call the numeric dunder method. For example, if the WizCoin class had no __imul__() method, the expression purse *= 10 will call __mul__() instead and assign its return value to purse. Because WizCoin objects are mutable, this is unexpected behavior that could lead to subtle bugs. ## Comparison Dunder Methods Python’s sort() method and sorted() function contain an efficient sorting algorithm that you can access with a simple call. But if you want to compare and sort objects of the classes you make, you’ll need to tell Python how to compare two of these objects by implementing the comparison dunder methods. Python calls the comparison dunder methods behind the scenes whenever your objects are used in an expression with the < , > , <= , >= , == , and != comparison operators.

Before we explore the comparison dunder methods, let’s examine six functions in the operator module that perform the same operations as the six comparison operators. Our comparison dunder methods will be calling these functions. Enter the following into the interactive shell.

>>> import operator
>>> operator.eq(42, 42)# "EQual", same as 42 == 42
True
>>> operator.ne('cat', 'dog')# "Not Equal", same as 'cat' != 'dog'
True
>>> operator.gt(10, 20)# "Greater Than ", same as 10 > 20
False
>>> operator.ge(10, 10)# "Greater than or Equal", same as 10 >= 10
True
>>> operator.lt(10, 20)# "Less Than", same as 10 < 20
True
>>> operator.le(10, 20)# "Less than or Equal", same as 10 <= 20
True

The operator module gives us function versions of the comparison oper- ators. Their implementations are simple. For example, we could write our own operator.eq() function in two lines:

>>> def eq(a, b):
>>>     return a == b

It’s useful to have a function form of the comparison operators because, unlike operators, functions can be passed as arguments to function calls. We’ll be doing this to implement a helper method for our comparison dunder methods.

First, add the following to the start of wizcoin.py. These imports give us access to the functions in the operator module and allow us to check whether the other argument in our method is a sequence by comparing it to collections.abc.Sequence :

>>> import collections.abc
>>> import operator

Then add the following to the end of the wizcoin.py file:

--snip-- def _comparisonOperatorHelper(self, operatorFunc, other): """A helper method for our comparison dunder methods.""" if isinstance(other, WizCoin): return operatorFunc(self.total, other.total) elif isinstance(other, (int, float)): return operatorFunc(self.total, other) elif isinstance(other, collections.abc.Sequence): otherValue = (other[0] * 17 * 29) + (other[1] * 29) + other[2] return operatorFunc(self.total, otherValue) elif operatorFunc == operator.eq: return False elif operatorFunc == operator.ne: return True else: return NotImplemented def __eq__(self, other): # eq is "EQual" return self._comparisonOperatorHelper(operator.eq, other) def __ne__(self, other): # ne is "Not Equal" return self._comparisonOperatorHelper(operator.ne, other) def __lt__(self, other): # lt is "Less Than" return self._comparisonOperatorHelper(operator.lt, other) def __le__(self, other): # le is "Less than or Equal" return self._comparisonOperatorHelper(operator.le, other) def __gt__(self, other): # gt is "Greater Than" return self._comparisonOperatorHelper(operator.gt, other) a def __ge__(self, other): # ge is "Greater than or Equal" return self._comparisonOperatorHelper(operator.ge, other)

Our comparison dunder methods call the _comparisonOperatorHelper() method 1 and pass the appropriate function from the operator module for the operatorFunc parameter. When we call operatorFunc() , we’re calling the function that was passed for the operatorFunc parameter— eq() 5, ne() 6, lt() 7, le() 8, gt() 9, or ge() a—from the operator module. Otherwise, we’d have to duplicate the code in _comparisonOperatorHelper() in each of our six comparison dunder methods. ### NOTE Functions (or methods) like _comparisonOperatorHelper() that accept other functions as arguments are called higher-order functions.

Our WizCoin objects can now be compared with other WizCoin objects 2, integers and floats 3, and sequence values of three number values that represent the galleons, sickles, and knuts 4. Enter the following into the interactive shell to see this in action:

>>> import wizcoin
>>> purse = wizcoin.WizCoin(2, 5, 10) # Create a WizCoin object.
>>> tipJar = wizcoin.WizCoin(0, 0, 37) # Create another WizCoin object.
>>> purse.total, tipJar.total # Examine the values in knuts.
(1141, 37)
>>> purse > tipJar # Compare WizCoin objects with a comparison operator.
True
>>> purse < tipJar
False
>>> purse > 1000 # Compare with an int.
True
>>> purse <= 1000
False
>>> purse == 1141
True
>>> purse == 1141.0 # Compare with a float.
True
>>> purse == '1141' # The WizCoin is not equal to any string value.
False
>>> bagOfKnuts = wizcoin.WizCoin(0, 0, 1141)
>>> purse == bagOfKnuts
True
>>> purse == (2, 5, 10) # We can compare with a 3-integer tuple.
True
>>> purse >= [2, 5, 10] # We can compare with a 3-integer list.
True
>>> purse >= ['cat', 'dog'] # This should cause an error.
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "C:\Users\Al\Desktop\wizcoin.py", line 265, in __ge__
return self._comparisonOperatorHelper(operator.ge, other)
File "C:\Users\Al\Desktop\wizcoin.py", line 237, in _
comparisonOperatorHelper
otherValue = (other[0] * 17 * 29) + (other[1] * 29) + other[2]
IndexError: list index out of range

Our helper method calls isinstance(other, collections.abc.Sequence) to see whether other is a sequence data type, such as a tuple or list. By making WizCoin objects comparable with sequences, we can write code such as purse >= [2, 5, 10] for a quick comparison.

16.2.2. SEQUE NCE COMPA RISONS

When comparing two objects of the built-in sequence types, such as strings, lists, or tuples, Python puts more significance on the earlier items in the sequence. That is, it won’t compare the later items unless the earlier items have equal values. For example, enter the following into the interactive shell:

>>> 'Azriel' < 'Zelda'
True
>>> (1, 2, 3) > (0, 8888, 9999)
True

The string ‘Azriel’ comes before (in other words, is less than) ‘Zelda’ because ‘A’ comes before ‘Z’ . The tuple (1, 2, 3) comes after (in other words, is greater than) (0, 8888, 9999) because 1 is greater than 0 . On the other hand, enter the following into the interactive shell:

>>> 'Azriel' < 'Aaron'
False
>>> (1, 0, 0) > (1, 0, 9999)
False

The string ‘Azriel’ doesn’t come before ‘Aaron’ because even though the ‘A’ in ‘Azriel’ is equal to the ‘A’ in ‘Aaron’ , the subsequent ‘z’ in ‘Azriel’ doesn’t come before the ‘a’ in ‘Aaron’ . The same applies to the tuples (1, 0, 0) and (1, 0, 9999) : the first two items in each tuple are equal, so it’s the third items ( 0 and 9999 , respectively) that determine that (1, 0, 0) comes before (1, 0, 9999).

This forces us to make a design decision about our WizCoin class. Should WizCoin(0, 0, 9999) come before or after WizCoin(1, 0, 0) ? If the number of galleons is more significant than the number of sickles or knuts, WizCoin(0, 0,9999) should come before WizCoin(1, 0, 0) . Or if we compare objects based on their values in knuts, WizCoin(0, 0, 9999) (worth 9,999 knuts) comes after WizCoin(1, 0, 0) (worth 493 knuts). In wizcoin.py, I decided to use the object’s value in knuts because it makes the behavior consistent with how WizCoin objects compare with integers and floats. These are the kinds of decisions you’ll have to make when designing your own classes.

There are no reflected comparison dunder methods, such as __req__() or __rne() , that you’ll need to implement. Instead, __lt() and __gt() reflect each other, __le() and __ge() reflect each other, and __eq() and __ne__() reflect themselves. The reason is that the following relationships hold true no matter what the values on the left or right side of the operator are:

  • purse > [2, 5, 10] is the same as [2, 5, 10] < purse

  • purse >= [2, 5, 10] is the same as [2, 5, 10] <= purse

  • purse == [2, 5, 10] is the same as [2, 5, 10] == purse

  • purse != [2, 5, 10] is the same as [2, 5, 10] != purse

Once you’ve implemented the comparison dunder methods, Python’s sort() function will automatically use them to sort your objects. Enter the following into the interactive shell:

>>> import wizcoin
>>> oneGalleon = wizcoin.WizCoin(1, 0, 0) # Worth 493 knuts.
>>> oneSickle = wizcoin.WizCoin(0, 1, 0) # Worth 29 knuts.
>>> oneKnut = wizcoin.WizCoin(0, 0, 1)
# Worth 1 knut.
>>> coins = [oneSickle, oneKnut, oneGalleon, 100]
>>> coins.sort() # Sort them from lowest value to highest.
>>> coins
[WizCoin(0, 0, 1), WizCoin(0, 1, 0), 100, WizCoin(1, 0, 0)]

Table 17-3 contains a full listing of the available comparison dunder methods and operator functions.

Table 17-3: Comparison Dunder Methods and operator Module Functions

Dunder method

Operation

Comparison operator

Function in operator module

__eq__()

EQual

==

operator.eq()

__ne__()

Not Equal

!=

operator.ne()

__lt__()

Less Than

<

operator.lt()

__le__()

Less than or Equal

<=

operator.le()

__gt__()

Greater Than

>

operator.gt()

__ge__()

Greater than or Equal

>=

operator.ge()

You can see the implementation for these methods at https://autbor.com/ wizcoinfull. The full documentation for the comparison dunder methods is in the Python documentation at https://docs.python.org/3/reference/datamodel .html#object.__lt__.

The comparison dunder methods let objects of your classes use Python’s comparison operators rather than forcing you to create your own meth- ods. If you’re creating methods named equals() or isGreaterThan() , they’re not Pythonic, and they’re a sign that you should use comparison dunder methods.