This article shows you how to overload the arithmetic operators in Python with dunder methods.
Python lets you override the arithmetic operators like +
for addition or *
for multiplication through dunder methods.
Dunder methods are special methods whose name starts and ends with a double underscore (hence, “dunder”), and some dunder methods are specific to arithmetic operations.
In this Pydon't, you will learn:
-p
);+p
);abs(p)
); and~
).+
);-
);*
);/
);//
);%
);divmod
); andpow
).<<
);>>
);&
);|
);^
);NotImplemented
is and how it differs from NotImplementedError
; andWe will start by explaining how dunder methods work and we will give a couple of examples by implementing the unary operators.
Then, we introduce the mechanics behind the binary arithmetic operators, basing the examples on the binary operator +
.
After we introduce all the concepts and mechanics that Python uses to handle binary arithmetic operators, we will provide an example of a class that implements all the arithmetic dunder methods that have been mentioned above.
You can now get your free copy of the ebook “Pydon'ts – Write elegant Python code” [on Gumroad][gumroad-pydonts] to help support the series of “Pydon't” articles 💪.
The example we will be using throughout this article will be that of a Vector
.
A Vector
will be a class for geometrical vectors, like vectors in 2D, or 3D, and it will provide operations to deal with vectors.
For example, by the end of this article, you will have an implementation of Vector
that lets you do things like these:
>>> from vector import Vector
>>> v = Vector(3, 2)
>>> v + Vector(4, 10)
Vector(7, 12)
>>> 3 * v
(9, 6)
>>> -v
(-3, -2)
Let us go ahead and start!
This is the starting vector for our class Vector
:
# vector.py
class Vector:
def __init__(self, *coordinates):
self.coordinates = coordinates
def __repr__(self):
return f"Vector{self.coordinates}"
if __name__ == "__main__":
print(Vector(3, 2))
Running this code will show this output:
Vector(3, 2)
This starting vector also shows two dunder methods that we are using right off the bat:
__init__
to initialise our Vector
instance; and__repr__
to provide a string representation of our Vector
objects.This shows that dunder methods are not magical. They look funny because of the leading and trailing...
]]>Descriptors are not black magic and this article will show you that. In fact, you use descriptors every day and you don't even know it.
(If you are new here and have no idea what a Pydon't is, you may want to read the Pydon't Manifesto.)
Descriptors are a piece of the Python world that many will declare as “obscure”, “dark magic”, or “too complex to bother learning”. I will show you that this is not true.
While we can all agree that descriptors do not belong in an introductory course to Python, descriptors are far from black magic and with a couple of good examples you can understand how they work and what they are useful for.
In this Pydon't, you will
__get__
;__set__
; and__delete__
.__set_name__
with descriptors;property
;staticmethod
; andclassmethod
.You can now get your free copy of the ebook “Pydon'ts – Write elegant Python code” on Gumroad to help support the series of “Pydon't” articles 💪.
Consider the implementation of the class Colour
below, which contains a hex
attribute for the hexadecimal representation of the colour:
class Colour:
def __init__(self, hex):
self.hex = hex
This class doesn't do much (at least, for now...):
>>> colour = Colour("#f8f8f2")
>>> colour.hex
'#f8f8f2'
Now, suppose that you want to add the attributes r
, g
, and b
, respectively for the red, green, and blue, components of the colour.
These attributes depend on the value of the hexadecimal representation of the colour, and you know all about properties, so you understand that defining three properties is the way to go, here:
class Colour:
def __init__(self, hex_string):
self.hex = hex_string
@property
def r(self):
return int(self.hex[1:3], 16)
@property
def g(self):
return int(self.hex[3:5], 16)
@property
def b(self):
return int(self.hex[5:7], 16)
This works as expected:
>>> red = Colour("#ff0000")
>>> red.r
255
>>> red.g, red.b
(0, 0)
If you have an attentive eye, you will notice that the three properties are pretty much the same.
The only difference lies in the indices you use when slicing the parameter hex
.
Descriptors, like properties, let you customise attribute lookup. However, descriptors give you more freedom because you implement a descriptor as a class. This is different from properties that are usually implemented via their getter, setter, and deleter methods.
Later, you will understand that descriptors give you more freedom because properties are a specific descriptor. In a way, properties provide a type of descriptor blueprint for you to...
]]>Learn how to use properties to add dynamic behaviour to your attributes.
(If you are new here and have no idea what a Pydon't is, you may want to read the Pydon't Manifesto.)
Properties, defined via the property
built-in, are a Python feature that lets you add dynamic behaviour behind what is typically a static interface: an attribute.
Properties also have other benefits and use cases, and we will cover them here.
In this Pydon't, you will
property
;property
to implement read-only attributes;property
doesn't have to be used as a decorator;property
in the standard library.You can now get your free copy of the ebook “Pydon'ts – Write elegant Python code” on Gumroad to help support the series of “Pydon't” articles 💪.
A property is an attribute that is computed dynamically. That's it. And you will understand what this means in a jiffy!
This is a class Person
with three vanilla attributes:
class Person:
def __init__(self, first, last):
self.first = first
self.last = last
self.name = f"{self.first} {self.last}"
john = Person("John", "Doe")
However, there is an issue with the implementation. Can you see what is the issue with this implementation?
I'll give you a hint:
>>> john = Person("John", "Doe")
>>> john.name
'John Doe'
>>> john.last = "Smith"
>>> john.name
# ?
When you implement name
as a regular attribute, it can go out of sync when you change the attributes upon which name
depended on.
How do we fix this?
Well, we could provide methods that the user can use to set the first and last names of the Person
instance, and those methods could keep name
in sync:
class Person:
def __init__(self, first, last):
self.first = first
self.last = last
self.name = f"{self.first} {self.last}"
def set_first(self, first):
self.first = first
self.name = f"{self.first} {self.last}"
def set_last(self, last):
self.last = last
self.name = f"{self.first} {self.last}"
john = Person("John", "Doe")
This works:
>>> john = Person("John", "Doe")
>>> john.name
'John Doe'
>>> john.set_first("Charles")
>>> john.name
'Charles Doe'
However, we had to add two methods that look pretty much the same... And this would get worse if we introduced an attribute for middle names, for example... Essentially, this isn't a very Pythonic solution – it isn't very elegant. (Or, at least, we can do better!)
There is another alternative...
Instead of updating name
when the other attributes are changed, we could add a method that computes the name of the user on demand:
class Person:
def __init__(self, first, last):
self.first = first
self.last = last
def get_name(self):
return f"{self.first} {self.last}"
This also works:
>>> john = Person("John", "Doe")
>>> john.get_name()
'John Doe'
>>> john.first = "Charles"
>>> john.get_name()
'Charles Doe'
But this isn't very elegant, either. However, it...
]]>This is an introduction to dunder methods in Python, to help you understand what they are and what they are for.
(If you are new here and have no idea what a Pydon't is, you may want to read the Pydon't Manifesto.)
Python is a language that has a rich set of built-in functions and operators that work really well with the built-in types.
For example, the operator +
works on numbers, as addition, but it also works on strings, lists, and tuples, as concatenation:
>>> 1 + 2.3
3.3
>>> [1, 2, 3] + [4, 5, 6]
[1, 2, 3, 4, 5, 6]
But what is it that defines that +
is addition for numbers (integers and floats)
and concatenation for lists, tuples, strings?
What if I wanted +
to work on other types?
Can I do that?
The short answer is “yes”, and that happens through dunder methods, the object of study in this Pydon't. In this Pydon't, you will
You can now get your free copy of the ebook “Pydon'ts – Write elegant Python code” on Gumroad to help support the series of “Pydon't” articles 💪.
In Python, dunder methods are methods that allow instances of a class to interact with the built-in functions and operators of the language.
The word “dunder” comes from “double underscore”, because the names of dunder methods start and end with two underscores,
for example __str__
or __add__
.
Typically, dunder methods are not invoked directly by the programmer, making it look like they are called by magic.
That is why dunder methods are also referred to as “magic methods” sometimes.1
Dunder methods are not called magically, though. They are just called implicitly by the language, at specific times that are well-defined, and that depend on the dunder method in question.
If you have defined classes in Python, you are bound to have crossed paths with a dunder method: __init__
.
The dunder method __init__
is responsible for initialising your instance of the class,
which is why it is in there that you usually set a bunch of attributes related to arguments the class received.
For example, if you were creating an instance of a class Square
,
you would create the attribute for the side length in __init__
:
class Square:
def __init__(self, side_length):
"""__init__ is the dunder method that INITialises the instance.
To create a square, we need to know the length of its side,
so that will be passed as an argument later, e.g. with Square(1).
To make sure the instance knows its own side length,
we save it with self.side_length = side_length.
"""
print("Inside init!")
self.side_length =...
]]>
Let me tell you why it is impossible to truly master Python, but also show you how to get as close to it as possible.
It has been said that you need 10,000 hours to master a skill. I won't dispute if that's true or not. What I'll tell you is that, even if that's true, I'm not sure it applies to Python!
In this Pydon't, I'll explain why I think you can't really master Python, but I'll also tell you why I think that's ok: I'll give you a series of practical tips that you can use to make sure you keep improving your Python knowledge.
Finally, by the end of the Pydon't, I'll share a little anecdote from my own personal experience with Python, to support my claims.
You can now get your free copy of the ebook “Pydon'ts – Write elegant Python code” on Gumroad to help support the series of “Pydon't” articles 💪.
Here's the dictionary definition of the verb “to master”:
“master”, verb – to learn or understand something completely
From my personal experience, there are two levels at which I believe one cannot master Python; I'll lay both of them down now.
The Python language is an evolving language: it isn't a finished product. As such, it keeps growing:
Therefore, I can never know everything about it! As soon as I think I just learned all the things there are to learn, new things pop up.
This is something I believe in, but it is also almost a philosophical point of view. There is also a practical side to this argument.
Not only does the language keep changing, one can argue that the Python language is already too big for you to be able to master it.
For example, most of us are familiar with the list methods .append
or .pop
.
But, from my experience, most people aren't familiar with the list methods .copy
, or .extend
, for example.
In fact, let's do an experiment: can you name the 11 existing list methods?
Scroll to the bottom of the page and write them down as a comment. If not the 11, write down as many as you can remember.
Here are they:
>>> [name for name in dir(list) if not name.startswith("__")]
['append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
No idea what dir
is? Just scroll down.
Maybe you even knew about all of them, but being able to name them is hard, right?
Let's do a similar thing for strings! First, jot down as many string methods that you can remember.
Done?
Great. Now count them. How many did you get?
Now, how many string methods do you think there are?
There are 47 (!) string methods!
Probably, you never even heard about some...
]]>This article compares the three main string formatting methods in Python and suggests which methods to use in each situation.
(If you are new here and have no idea what a Pydon't is, you may want to read the Pydon't Manifesto.)
The Zen of Python says that
“There should be one – and preferably only one – obvious way to do it.”
And yet, there are three main ways of doing string formatting in Python. This Pydon't will settle the score, comparing these three methods and helping you decide which one is the obvious one to use in each situation.
In this Pydon't, you will:
.format
;You can now get your free copy of the ebook “Pydon'ts – Write elegant Python code” on Gumroad.
Let's pretend, for a second, that Python had zero ways of doing string formatting.
Now, I have a task for you: write a function that accepts a programming language name and returns a string saying that said programming language rocks. Can you do it? Again, without any string formatting whatsoever!
Here is a possible solution:
def language_rocks(language):
return language + " rocks!"
# ---
>>> language_rocks("Python")
'Python rocks!'
Great job!
Now, write a function that accepts a programming language name and its (estimated) number of users, and returns a string saying something along the lines of “<insert language> rocks! Did you know that <insert language> has around <insert number> users?”.
Can you do it? Recall that you are not supposed to use any string formatting facilities, whatsoever!
Here is a possible solution:
def language_info(language, users_estimate):
return (
language + " rocks! Did you know that " + language +
" has around " + str(users_estimate) + " users?!"
)
# ---
>>> language_info("Python", 10)
'Python rocks! Did you know that Python has around 10 users?!'
Notice how that escalated quite quickly: the purpose of our function is still very simple, and yet we have a bunch of string concatenations happening all over the place, just because we have some pieces of information that we want to merge into the string.
This is what string formatting is for: it's meant to make your life easier when you need to put information inside strings.
Now that we've established that string formatting is useful, let's take a look at the three main ways of doing string formatting in Python.
First, here is how you would refactor the function above:
# Using C-style string formatting:
def language_info_cstyle(language, users_estimate):
return (
"%s rocks! Did you know that %s has around %d users?!" %
(language, language, users_estimate)
)
# Using the Python 3 `.format` method from strings:
def language_info_format(language, users_estimate):
return "{} rocks! Did you know...
]]>
When you call a function in Python and give it some arguments... Are they passed by value? No! By reference? No! They're passed by assignment.
(If you are new here and have no idea what a Pydon't is, you may want to read the Pydon't Manifesto.)
Many traditional programming languages employ either one of two models when passing arguments to functions:
Having said that, it is important to know the model that Python uses, because that influences the way your code behaves.
In this Pydon't, you will:
id
;copy
to do both types of object copies.You can now get your free copy of the ebook “Pydon'ts – Write elegant Python code” on Gumroad.
In the pass-by-value model, when you call a function with a set of arguments, the data is copied into the function. This means that you can modify the arguments however you please and that you won't be able to alter the state of the program outside the function. This is not what Python does, Python does not use the pass-by-value model.
Looking at the snippet of code that follows, it might look like Python uses pass-by-value:
def foo(x):
x = 4
a = 3
foo(a)
print(a)
# 3
This looks like the pass-by-value model because we gave it a 3,
changed it to a 4,
and the change wasn't reflected on the outside
(a
is still 3).
But, in fact, Python is not copying the data into the function.
To prove this, I'll show you a different function:
def clearly_not_pass_by_value(my_list):
my_list[0] = 42
l = [1, 2, 3]
clearly_not_pass_by_value(l)
print(l)
# [42, 2, 3]
As we can see, the list l
, that was defined outside of the function,
changed after calling the function clearly_not_pass_by_value
.
Hence, Python does not use a pass-by-value model.
In a true pass-by-reference model, the called function gets access to the variables of the callee! Sometimes, it can look like that's what Python does, but Python does not use the pass-by-reference model.
I'll do my best to explain why that's not what Python does:
def not_pass_by_reference(my_list):
my_list = [42, 73, 0]
l = [1, 2, 3]
not_pass_by_reference(l)
print(l)
# [1, 2, 3]
If Python used a pass-by-reference model,
the function would've managed to completely change the value of l
outside the function, but that's not what happened, as we can see.
Let me show you an actual pass-by-reference situation.
Here's some Pascal code:
program callByReference;
var
x: integer;...
]]>
This Pydon't will teach you how to use Python's conditional expressions.
(If you are new here and have no idea what a Pydon't is, you may want to read the Pydon't Manifesto.)
Conditional expressions are what Python has closest to what is called a “ternary operator” in other languages.
In this Pydon't, you will:
if: ... elif: ... else:
statements and conditional expressions;You can now get your free copy of the ebook “Pydon'ts – Write elegant Python code” on Gumroad to help support the series of “Pydon't” articles 💪.
A conditional expression in Python is an expression (in other words, a piece of code that evaluates to a result) whose value depends on a condition.
To make it clearer, here is an example of a Python expression:
>>> 3 + 4 * 5
23
The code 3 + 4 * 5
is an expression, and that expression evaluates to 23.
Some pieces of code are not expressions.
For example, pass
is not an expression because it does not evaluate to a result.
pass
is just a statement, it does not “have” or “evaluate to” any result.
This might be odd (or not!) but to help you figure out if something is an expression or not,
try sticking it inside a print
function.
Expressions can be used inside other expressions, and function calls are expressions.
Therefore, if it can go inside a print
call, it is an expression:
>>> print(3 + 4 * 5)
23
>>> print(pass)
File "<stdin>", line 1
print(pass)
^
SyntaxError: invalid syntax
The syntactic error here is that the statement pass
cannot go inside the print
function,
because the print
function wants to print something,
and pass
gives nothing.
We are very used to using if
statements to run pieces of code when certain conditions are met.
Rewording that, a condition can dictate what piece(s) of code run.
In conditional expressions, we will use a condition to change the value to which the expression evaluates.
Wait, isn't this the same as an if
statement?
No!
Statements and expressions are not the same thing.
Instead of beating around the bush, let me just show you the anatomy of a conditional expression:
expr_if_true if condition else expr_if_false
A conditional expression is composed of three sub-expressions and the keywords if
and else
.
None of these components are optional.
All of them have to be present.
How does this work?
First, condition
is evaluated.
Then, depending on whether condition
evaluates to Truthy or Falsy,
the expression evaluates expr_if_true
or expr_if_false
, respectively.
As you may be guessing from the names,
expr_if_true
and expr_if_false
can themselves be expressions.
This means they can be simple literal values like...
This Pydon't will teach you the basics of list comprehensions in Python.
(If you are new here and have no idea what a Pydon't is, you may want to read the Pydon't Manifesto.)
List comprehensions are, hands down, one of my favourite Python features.
It's not THE favourite feature, but that's because Python has a lot of things I really like! List comprehensions being one of those.
This article (the first in a short series) will cover the basics of list comprehensions.
This Pydon't will teach you the following:
for
loops and list comprehensions;I also summarised the contents of this article in a cheatsheet that you can get for free from here.
You can now get your free copy of the ebook “Pydon'ts – Write elegant Python code” on Gumroad to help support the series of “Pydon't” articles 💪.
A list comprehension is a Python expression that builds a list. That's it, really.
In other words, list comprehensions are great because they provide a very convenient syntax to create lists.
If you come from a functional language, have a strong mathematical background, or if you are just very comfortable with maths notation, you may have an easier time learning about list comprehensions from first principles.
For people who already know Python, list comprehensions are best understood when compared to a for
loop, so let me show you that.
Consider the loop below.
It builds a list called squares
which contains the first square numbers:
squares = []
for num in range(10):
squares.append(num ** 2)
This loop exhibits a very common pattern:
given an iterable (in this case, range(10)
), do something with each of its elements, one by one, and append the result to a new list (in this case, squares
).
The key idea behind list comprehensions is that many lists can be built out of other, simpler iterables (lists, tuples, strings, range
, ...) by transforming the data that we get from those iterables.
In those cases, we want to focus on the data transformation that we are doing.
So, in the case of the loop above, the equivalent list comprehension would look like like this:
squares = [num ** 2 for num in range(10)]
What we can see, by comparing the two, is that the list comprehension extracts the most important bits out of the loop and then drops the fluff:
In short, the version with the list comprehension drops:
squares = []
); andThis Pydon't will teach you how to use the set
and frozenset
Python built-in types.
(If you are new here and have no idea what a Pydon't is, you may want to read the Pydon't Manifesto.)
Python contains a handful of built-in types, among which you can find integers, lists, strings, etc...
Python also provides two built-in types to handle sets,
the set
and the frozenset
.
In this Pydon't, you will:
set
built-in and the mathematical concept of “set”;set
and frozenset
built-ins are;set
and frozenset
are;set
(and frozenset
) in Python code;You can now get your free copy of the ebook “Pydon'ts – Write elegant Python code” on Gumroad to help support the series of “Pydon't” articles 💪.
A set is simply a collection of unique items where order doesn't matter. Whenever I have to think of sets, I think of shopping carts.
If you go shopping, and you take a shopping cart with you, the order in which you put the items in the shopping cart doesn't matter. The only thing that actually matters is the items that are in the shopping cart.
If you buy milk, chocolate, and cheese, it doesn't matter the order in which those items are registered. What matters is that you bought milk, chocolate, and cheese.
In that sense, you could say that the groceries you bought form a set:
the set containing milk, chocolate, and cheese.
Both in maths and in Python, we use {}
to denote a set,
so here's how you would define the groceries set in Python:
>>> groceries = {"milk", "cheese", "chocolate"}
>>> groceries
{'cheese', 'milk', 'chocolate'}
>>> type(groceries).__name__
'set'
We can check that we created a set
indeed by checking the __name__
of
the type
of groceries
.
If you don't understand why we typed type(groceries).__name__
instead
of just doing type(groceries)
, then I advise you to skim through
the Pydon't about the dunder attribute __name__
.
(P.S. doing isinstance(groceries, set))
would also work here!)
To make sure that order really doesn't matter in sets, we can try comparing this set with other sets containing the same elements, but written in a different order:
>>> groceries = {"milk", "cheese", "chocolate"}
>>> groceries == {"cheese", "milk", "chocolate"}
True
>>> groceries == {"chocolate", "milk", "cheese"}
True
Another key property of (mathematical) sets is that there are no duplicate elements. It's more or less as if someone told you to go buy cheese, and when you get back home, that person screams from another room:
“Did you buy cheese?”
This is a yes/no question: you either bought cheese or you didn't.
For sets, the same thing happens: the element...
]]>