Pythonic Code
In this chapter, we will explore the way ideas are expressed in Python, with its own particularities. If you are familiar with the standard ways of accomplishing some tasks in programming (such as getting the last element of a list, iterating, searching, and so on), or if you come from more traditional programming languages (like C, C++, and Java), then you will find that, in general, Python provides its own mechanism for most common tasks.
In programming, an idiom is a particular way of writing code in order to perform a specific task. It is something common that repeats and follows the same structure every time. Some could even argue and call them a pattern, but be careful because they are not designed patterns (which we will explore later on). The main difference is that design patterns are high-level ideas, independent from the language (sort of), but they do...
Indexes and slices
In Python, as in other languages, some data structures or types support accessing its elements by index. Another thing it has in common with most programming languages is that the first element is placed in the index number zero. However, unlike those languages, when we want to access the elements in a different order than usual, Python provides extra features.
For example, how would you access the last element of an array in C? This is something I did the first time I tried Python. Thinking the same way as in C, I would get the element in the position of the length of the array minus one. This could work, but we could also use a negative index number, which will start counting from the last, as shown in the following commands:
>>> my_numbers = (4, 5, 3, 9)
>>> my_numbers[-1]
9
>>> my_numbers[-3]
5
In addition to getting just one element...
Context managers
Context managers are a distinctively useful feature that Python provides. The reason why they are so useful is that they correctly respond to a pattern. The pattern is actually every situation where we want to run some code, and has preconditions and postconditions, meaning that we want to run things before and after a certain main action.
Most of the time, we see context managers around resource management. For example, on situations when we open files, we want to make sure that they are closed after processing (so we do not leak file descriptors), or if we open a connection to a service (or even a socket), we also want to be sure to close it accordingly, or when removing temporary files, and so on.
In all of these cases, you would normally have to remember to free all of the resources that were allocated and that is just thinking about the best case—...
Properties, attributes, and different types of methods for objects
All of the properties and functions of an object are public in Python, which is different from other languages where properties can be public, private, or protected. That is, there is no point in preventing caller objects from invoking any attributes an object has. This is another difference with respect to other programming languages in which you can mark some attributes as private or protected.
There is no strict enforcement, but there are some conventions. An attribute that starts with an underscore is meant to be private to that object, and we expect that no external agent calls it (but again, there is nothing preventing this).
Before jumping into the details of properties, it's worth mentioning some traits of underscores in Python, understanding the convention, and the scope of attributes.
...Iterable objects
In Python, we have objects that can be iterated by default. For example, lists, tuples, sets, and dictionaries can not only hold data in the structure we want but also be iterated over a for loop to get those values repeatedly.
However, the built-in iterable objects are not the only kind that we can have in a for loop. We could also create our own iterable, with the logic we define for iteration.
In order to achieve this, we rely on, once again, magic methods.
Iteration works in Python by its own protocol (namely the iteration protocol). When you try to iterate an object in the form for e in myobject:..., what Python checks at a very high level are the following two things, in order:
- If the object contains one of the iterator methods—__next__ or __iter__
- If the object is a sequence and has __len__ and __getitem__
Therefore, as a fallback mechanism, sequences...
Container objects
Containers are objects that implement a __contains__ method (that usually returns a Boolean value). This method is called in the presence of the in keyword of Python.
Something like the following:
element in container
When used in Python becomes this:
container.__contains__(element)
You can imagine how much more readable (and Pythonic!) the code can be when this method is properly implemented.
Let's say we have to mark some points on a map of a game that has two-dimensional coordinates. We might expect to find a function like the following:
def mark_coordinate(grid, coord):
if 0 <= coord.x < grid.width and 0 <= coord.y < grid.height:
grid[coord] = MARKED
Now, the part that checks the condition of the first if statement seems convoluted; it doesn't reveal the intention of the code, it's not expressive, and worst of all it calls...
Dynamic attributes for objects
It is possible to control the way attributes are obtained from objects by means of the __getattr__ magic method. When we call something like <myobject>.<myattribute>, Python will look for <myattribute> in the dictionary of the object, calling __getattribute__ on it. If this is not found (namely, the object does not have the attribute we are looking for), then the extra method, __getattr__, is called, passing the name of the attribute (myattribute) as a parameter. By receiving this value, we can control the way things should be returned to our objects. We can even create new attributes, and so on.
In the following listing, the __getattr__ method is demonstrated:
class DynamicAttributes:
def __init__(self, attribute):
self.attribute = attribute
def __getattr__(self, attr):
if attr.startswith("fallback_"...
Callable objects
It is possible (and often convenient) to define objects that can act as functions. One of the most common applications for this is to create better decorators, but it's not limited to that.
The magic method __call__ will be called when we try to execute our object as if it were a regular function. Every argument passed to it will be passed along to the __call__ method.
The main advantage of implementing functions this way, through objects, is that objects have states, so we can save and maintain information across calls.
When we have an object, a statement like this object(*args, **kwargs) is translated in Python to object.__call__(*args, **kwargs).
This method is useful when we want to create callable objects that will work as parametrized functions, or in some cases functions with memory.
The following listing uses this method to construct an object that...
Summary of magic methods
We can summarize the concepts we described in the previous sections in the form of a cheat sheet like the one presented as follows. For each action in Python, the magic method involved is presented, along with the concept that it represents:
Statement
|
Magic method
|
Python concept
|
obj[key] obj[i:j] obj[i:j:k] |
__getitem__(key) |
Subscriptable object |
with obj: ... | __enter__ / __exit__ |
Context manager |
for i in obj: ... |
__iter__ / __next__ __len__ / __getitem__ |
Iterable object Sequence |
obj.<attribute> |
__getattr__ |
Dynamic attribute retrieval |
obj(*args, **kwargs) |
__call__(*args, **kwargs) |
Callable object
|
Caveats in Python
Besides understanding the main features of the language, being able to write idiomatic code is also about being aware of the potential problems of some idioms, and how to avoid them. In this section, we will explore common issues that might cause you long debugging sessions if they catch you off guard.
Most of the points discussed in this section are things to avoid entirely, and I will dare to say that there is almost no possible scenario that justifies the presence of the anti-pattern (or idiom, in this case). Therefore, if you find this on the code base you are working on, feel free to refactor it in the way that is suggested. If you find these traits while doing a code review, this is a clear indication that something needs to change.
Mutable default arguments...
Summary
In this chapter, we have explored the main features of Python, with the goal of understanding its most distinctive features, those that make Python a peculiar language compared to the rest. On this path, we have explored different methods of Python, protocols, and their internal mechanics.
As opposed to the previous chapter, this one is more Python-focused. A key takeaway of the topics of this book is that clean code goes beyond following the formatting rules (which, of course, are essential to a good code base). They are a necessary condition, but not sufficient. Over the next few chapters, we will see ideas and principles that relate more to the code, with the goal of achieving a better design and implementation of our software solution.
With the concepts and the ideas of this chapter, we explored the core of Python: its protocols and magic methods. It should be clear...
References
The reader will find more information about some of the topics that we have covered in this chapter in the following references. The decision of how indices work in Python is based on (EWD831), which analyzes several alternatives for ranges in math and programming languages:
- EWD831: Why numbering should start at zero (https://www.cs.utexas.edu/users/EWD/transcriptions//EWD831.html)
- PEP-343: The "with" Statement (https://www.python.org/dev/peps/pep-0343/)
- CC08: The book written by Robert C. Martin named Clean Code: A Handbook of Agile Software Craftsmanship
- Python documentation, the iter() function (https://docs.python.org/3/library/functions.html#iter)
- Differences between PyPy and CPython (https://pypy.readthedocs.io/en/latest/cpython_differences.html#subclasses-of-built-in-types)