10 cubes of syntactic sugar
Python has a lot of useful and interesting syntactic sugar. So much that unprepared users can get diabetes. Here you will see some unique syntactic sugar for Python, examples of its correct and incorrect use.
Thousand separators in numbers
Long hardcoded numbers are very difficult to perceive by the eye.
In writing, we are used to putting separators between digits (in Russia, for example, it is customary to write spaces, and in America – commas). You can do this in code too.
Signs help to distinguish long integers _
inserted between the digits.
So, 10_000_000
And 8_800_555_35_35
are ordinary integers.
List comprehension
A cool way to write lists, dictionaries, sets and generators in one line
x = [выражение for i in итератор]
This code is completely equivalent to the following
def generator():
for i in итератор:
yield выражение
x = list(generator())
You can also add a condition to this expression. Then the generator will return only those values that satisfy the condition.
x = [выражение for i in итератор if условие]
For example, here all odd numbers up to 10 are squared
x = [i ** 2 for i in range(10) if i % 2 == 1]
Although, this particular problem can be solved twice as fast by changing the step range
x = [i ** 2 for i in range(1, 10, 2)]
Dictionaries and sets are created with the same success. You just need to change the brackets
a_dict = {i: ord(i) for i in "abcdefghijklmnopqrstuvwxyz"}
a_set = {isqrt(i) for i in range(100)}
If you put parentheses, you will create a regular generator. Tuples and frozen sets (unknown objects) cannot be created this way.
Attention optimizers
Magical _
specified as an iteration variable does not save memory! Expressions [0 for _ in l]
And [0 for i in l]
they are waiting for this memory in absolutely the same way, although there is almost no difference.
JIT scare expression _
no need, at least because cpython doesn't have JIT, but pypy will accept it _
as a normal variable.
Want to optimize something? Optimize your time by writing [0] * len(l)
.
The higher the version of Python, the better the interpreter handles iterators and the smaller the difference between a regular loop and initialization via generators.
Unpacking iterators
Let's say we have a tuple x = (1, 2, 3, 42, 999, 7)
. I want to push its values into variables, namely: the first in a
the second in b
the last one in c
and everything else in other
.
Instead of bulky code
a = x[0]
b = x[1]
c = x[-1]
other = x[2:-1]
You can write it simply
a, b, *other, c = x
Moreover, you can unpack nested tuples in exactly the same way
y = (1, 2, 3, (10, 20, 30, 40, 50), (33, 77), 19, 29)
a, *b, c, (d, e, *f), (g, h), *i = y
Instead of tuples there could be any iterable objects
This unpacking works everywhere: in cycles, list expressions, etc.
persons = [("Alice", 2), ("Bob", 9), ("Charlie", 11)]
for name, rank in persons:
print(f"{name} -- {rank}")
Else in loops
Typically, `else` is used for a conditional statement, but Python has additional functionality for it. By specifying `else` after a loop, you can specify a block of code that will only be executed if the loop exits without a `break`.
For example,
for i in range(2, isqrt(n)):
if n % i == 0: break
else:
print(f"{n} - простое число")
This cool construction makes the code cleaner and saves the programmer from declaring unnecessary flags and dancing with tambourines. Compare with
is_prime = False
for i in range(2, isqrt(n)):
if n % i == 0:
is_prime = True
break
if is_prime:
print(f"{n} - простое число")
Ellipsis object
Python has a built-in constant Ellipsis
having an alias (literal) ...
It's just a special meaning different from None
, True
/False
and other constants.
Ellipses are used to make life easier and to replace special literals.
Type Annotations
Let's say we need to specify the type of variable x
– tuple of integers
x: tuple[int] = (1,)
This ad is not the same as list[int]
after all list[int]
specifies the type for all elements of the list, and tuple[int]
– only the type of the first element (and their number – 1).
To declare a tuple with two elements, you will have to write types
x: tuple[int, int] = (1, 2)
But what if the length of the tuple is unknown and can be anything? Ellipsis to the rescue!
x: tuple[int, ...] = (1, 2, 3, 42, 999, 7)
Alternative None
There are situations when you need to push some special value into a function. This cannot be None
because it is used as a normal value. It will come in handy here ...
For example, a function that returns the first value of an iterator
def first(iterable, default=...):
"""Возвращает первый элемент iterable"""
for item in iterable:
return item
if default is ...:
raise ValueError('first() вызвано с пустым iterable,'
'а значение по умолчанию не установленно')
return default
Ellipsis is not a replacement for pass
Theoretically, it is possible to write ...
instead of pass
but this would be semantically incorrect. Code
def function():
...
completely equivalent
def function():
42
There is no point in placing a meaningful constant in the body of a function, loop, condition, etc. to indicate no action. It is more logical and correct to use pass
.
pass
means there is no code. ...
implies that the code exists, but I just don't write it. Therefore, the only situation where it is appropriate to use ...
in this context – .pyd files. After all, these are declarations of (proto)types of functions, classes, etc., where the code actually exists, but it is not visible (after all, it is in another file).
Replace index
There is no such functionality in regular Python, but third-party libraries (for example, numpy) add it.
The idea is based on the fact that inside the slices (objects slice
used in indexing a[i:j]
) can contain any hashable objects, including tuples.
Let a
– a highly multidimensional array (let it be 7-dimensional). Instead of a bulky a[0, :, :, :, :, :, 0]
you can write simply a[0, ..., 0]
.
Walrus Operator
Special syntactic construction :=
which allows you to assign a value to a variable and immediately return it. It is used to avoid cumbersome expressions.
For example, checking for a regular expression
if m := re.match(r"(.*)@([a-z\.]+)", email):
print(f"Почтовый ящик {m[1]} на сервисе {m[2]}")
Let's imagine that we are making our own “command line”. Instead of duplicating the code
command = input("$ ")
while command != 'exit':
...
command = input("$ ")
you can write simply
while (command := input("$ ")) != 'exit':
...
And another cool use of the walrus is to record witnesses. any
and counterexamples in all
. Function any
iterates to the first true value, and all
– until the first false one.
By overwriting some variable, we can fix the first value for which any
became true and all
became false.
Here is a list
x = [1, 2, 3, 4, 10, 12, 7, 8]
And I check if there is at least one number greater than 10
if any((a := i) > 10 for i in x):
print(f'Есть хотя бы одно число, большее 10. Это {a}!')
And, accordingly, are all numbers less than 10?
if all((a := i) < 10 for i in x):
print(f'Все числа меньше 10')
else:
print(f'Не все числа меньше 10. Например, {a}')
Don't forget about DRY, import this
and most importantly – common sense. Don't push sugar where it even visually interferes and especially where it does harm.