10 кубиков синтаксического сахара

в 12:16, , рубрики: pythonic, синтаксис, синтаксический сахар, языковые конструкции

В питоне очень много полезного и интересного синтаксического сахара. Настолько много, что у неподготовленных пользователей может случиться сахарный диабет. Здесь вы увидите несколько уникального для питона синтаксического сахара, его примеры правильного и неправильного применения.

Разделители разрядов в числах

Длинные захардкоженные числа очень плохо воспринимаются на глаз.
На письме мы привыкли ставить между разрядами разделители (в России, например, принято писать пробелы, а в Америке - запятые). В коде тоже можно это делать.

Отличать длинные целые числа помогают знаки _, вставленные между разрядами.
Так, 10_000_000 и 8_800_555_35_35 являются обыкновенными целыми числами.

List comprehension

Прикольный способ записывать списки, словари, множества и генераторы в одну строчку

x = [выражение for i in итератор]

Этот код полностью эквивалентен следующему

def generator():
    for i in итератор:
        yield выражение

x = list(generator())

Так же можно добавить условие в это выражение. Тогда генератор будет возвращать только те значения, которые удовлетворяют условию

x = [выражение for i in итератор if условие]

Например, здесь все нечетные числа до 10 возводятся в квадрат

x = [i ** 2 for i in range(10) if i % 2 == 1]

Хотя, конкретно эту задачу можно решить вдвое быстрее, изменив шаг range

x = [i ** 2 for i in range(1, 10, 2)]

С таким же успехом создаются словари и множества. Надо просто поменять скобки

a_dict = {i: ord(i) for i in "abcdefghijklmnopqrstuvwxyz"}
a_set  = {isqrt(i) for i in range(100)}

Если поставить круглые скобки, то создастся обычный генератор. Кортежи и замороженные множества (неизвемяемые объекты) так создавать нельзя.

Вниманию оптимизаторов

Волшебная _, указаная в качестве переменной итерирования не экономит память! Выражения [0 for _ in l] и [0 for i in l] абсолютно одинаково ждут память эту память, хотя разница почти никакая.

JIT пугать выражением _ не надо, хотя бы потому, что в cpython JIT нет, а pypy воспримет _ как обычную переменную.

Хотите что-то оптимизировать? Оптимизируйте время, написав [0] * len(l).

Чем выше версия питона, тем лучше интерпретатор обрабатывает итераторы и тем меньше разница между обычным циклом и инициализацией через генераторы.

Распаковка итераторов

Допустим, у нас есть кортеж x = (1, 2, 3, 42, 999, 7). Я хочу распихать его значения по переменным, а именно: первое в a, второе в b, последнее в c, а все остальное в other.

Вместо громоздкого кода

a = x[0]
b = x[1]
c = x[-1]
other = x[2:-1]

Можно написать просто

a, b, *other, c = x

Более того, можно распаковывать вложенные кортежи абсолютно так же

y = (1, 2, 3, (10, 20, 30, 40, 50), (33, 77), 19, 29)
a, *b, c, (d, e, *f), (g, h), *i = y

Вместо кортежей могли бы быть любые итерируемые объекты

Такая распаковка работает везде: в циклах, списковых выражениях и т.д.

persons = [("Alice", 2), ("Bob", 9), ("Charlie", 11)]
for name, rank in persons:
    print(f"{name} -- {rank}")

Else в циклах

Обычно `else` используется для условного оператора, но в питоне для него есть дополнительная функциональность. Указав `else` после цикла, можно задать блок кода, который выполнится только если цикл завершился без `break`.

Например,

for i in range(2, isqrt(n)):
    if n % i == 0: break
else:
    print(f"{n} - простое число")

Эта классная конструкция делает код чище и избавляет программиста от объявления лишних флагов и танцев с бубнами. Сравните с

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

В питоне есть встроенная константа Ellipsis, имеющая псевдоним (литерал) ...
Это просто специальное значение, отличное от None, True/False и других констант.

Используется многоточие для упрощения жизни и для замены особых литералов.

Аннотации типов

Пусть нам надо указать тип переменной x - кортеж с целыми числами

x: tuple[int] = (1,)

Это объявление не то же самое, что list[int], ведь list[int] указывает тип для всех элементов списка, а tuple[int] - только тип первого элемента (и их количество - 1).

Для объявления кортежа с двумя элементами придется писать типы

x: tuple[int, int] = (1, 2)

А если длина кортежа неизвестна и может быть любой? Многоточие в помощь!

x: tuple[int, ...] = (1, 2, 3, 42, 999, 7)

Альтернатива None

Бывают ситуации, когда нужно запихать в функцию какое-то особое значение. Это не может быть None, ведь он используется как обычное значение. Здесь пригодится ...

Например, функция, возвращающая первое значение итератора

def first(iterable, default=...):
    """Возвращает первый элемент iterable"""

    for item in iterable:
        return item

    if default is ...:
        raise ValueError('first() вызвано с пустым iterable,'
                         'а значение по умолчанию не установленно')

    return default

Ellipsis - не замена pass

Теоритически можно написать ... вместо pass, но это будет семантически неверно. Код

def function():
    ...

полностью равносилен

def function():
    42

Нет никакого смысла помещать значащую константу в тело функции, цикла, условия и т.д., чтобы показать отсутствие действия. Логичнее и правильнее использовать pass.

pass означает, что кода нет. ... же подразумевает, что код есть, но я его просто не пишу. Поэтому единственная ситуация, где уместно использование ... в таком контексте - файлы .pyd. Ведь это объявления (прото)типов функций, классов и т.д., где код действительно есть, но его не видно (он ведь в другом файле).

Замена индекса

В обычном питоне такого функционала нет, но его добавляют сторонние библиотеки (например, numpy).

Идея основывается на том, что внутри срезов (объектов slice, используемых в индексации a[i:j]) могут стоять любые хешируемые объекты, в том числе и кортежи.

Пусть a - сильно многомерный массив (пусть будет 7-мерный). Вместо громоздкого a[0, :, :, :, :, :, 0] можно написать просто a[0, ..., 0].

Моржовый оператор

Специальная синтаксическая конструкция :=, которая позволяет присвоить значение переменной и сразу вернуть его. Используется, чтобы избежать громоздких выражений.

Например, проверка на соответствие регулярному выражению

if m := re.match(r"(.*)@([a-z.]+)", email):
    print(f"Почтовый ящик {m[1]} на сервисе {m[2]}")

Представим, что мы делаем свою «командную строку». Вместо дублирования кода

command = input("$ ")

while command != 'exit':
    ...
    command = input("$ ")

можно написать просто

while (command := input("$ ")) != 'exit':
    ... 

И еще оно классное применение моржа -- фиксация свидетелей any и контрпримеров в all. Функция any итерирует до первого истинного значения, а all - до первого ложного.

Перезаписывая какую-то переменную, мы сможем зафиксировать первое значение, для которого any стало истинным и all стало ложным.

Вот есть список

x = [1, 2, 3, 4, 10, 12, 7, 8]

И я проверяю, есть ли хотя бы одно число, большее 10

if any((a := i) > 10 for i in x):
    print(f'Есть хотя бы одно число, большее 10. Это {a}!')

И, соответственно, все ли числа меньше 10

if all((a := i) < 10 for i in x):
    print(f'Все числа меньше 10')
else:
    print(f'Не все числа меньше 10. Например, {a}')

Не забывайте про DRY, import this и самое главное - здравый смысл. Не надо пихать сахар там, где он хотя-бы визуально мешает и тем более там, где он вредит.

Автор: AKTOO

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js