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 athe second in bthe last one in cand 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 Ellipsishaving 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 Nonebecause 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 passbut 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 sliceused 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.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *