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

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

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

Отличать длинные целые числа помогают знаки _, вставленные между разрядами.
Так, 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_compose = False

for i in range(2, isqrt(n)):
    if n % i == 0: 
        is_compose = True
        break

if is_compose:
    print(f"{n} - составное число")
else:
    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 и самое главное - здравый смысл. Не надо пихать сахар там, где он хотя-бы визуально мешает и тем более там, где он вредит.

Комментарии (18)


  1. dopusteam
    29.08.2024 12:26

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

    А нельзя сделать так?

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

    Мне вообще кажется, тут с примером что то не то


    1. navferty
      29.08.2024 12:26
      +2

      Да, похоже что наоборот, выводится "простое" вместо "не простое".


    1. AKTOO Автор
      29.08.2024 12:26

      Исправил код. Смысл примера не в конкретно этом примере (на простоту можно и по-другому проверить), а в том, что отпадает необходимость в флагах.


    1. event1
      29.08.2024 12:26

      а ещё лучше вообще сделать так:

      i, lim = 2, isqrt(n)
      
      while i < lim and n % i == 0 :
          i +=  1
      
      type = 'простое' if i == lim else 'составное'
      printf(f'{n} — {type} число')
      


      1. navferty
        29.08.2024 12:26

        Я не питонист, но мне кажется Вы тоже немного перемудрили с циклом - получается, что Вы проверяете чтобы оно делилось нацело на каждое из чисел в интервале от 2 до корня.

        https://www.online-python.com/daMsAQ3HXO


        1. event1
          29.08.2024 12:26

          да, конечно вы правы, там должно быть !=


  1. CrazyElf
    29.08.2024 12:26
    +10

    Переменная с именем _ используется не для оптимизаций, а чтобы показать, что хотя нам и пришлось тут написать какую-то переменную, потому что по правилам языка без этого нельзя обойтись, но значение этой переменной нас не интересует, мы его нигде не используем. Это такой хинт для того, кто будет читать этот код.


    1. AKTOO Автор
      29.08.2024 12:26
      +1

      Абсолютно верно. Но, к сожалению, на моем обыте было очень много людей, которые думали, что _ память все-таки экономит.
      Написать в подзаголовке "вниманию горе-оптимизаторов" у меня рука не повернулась


  1. Spiritschaser
    29.08.2024 12:26
    +3

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

    А, так вот как эту бабуйню сделать читаемой!


    1. domix32
      29.08.2024 12:26

      Какую конкретно? Сопостовления или генераторы?


  1. Spiritschaser
    29.08.2024 12:26

    генераторы. У меня с ними проблем нет, но хочется иногда чтобы код почти блок-схемой выглядел.


    1. domix32
      29.08.2024 12:26

      в смысле как у всяких джавистов-растоманов типа

      something()
       .something_else1()
       .something_else2()
       .something_else3()

      Или что вы блок-схемой зовёте? Не так же код писать

                        | something |
                              |
                             /\
                            /  \
                  ---YES---/cond\---NO---
                  |        \    /       |
      | something_else2 |   \  /     | something_else2 |
                             \/


      1. Spiritschaser
        29.08.2024 12:26

        Ну, почти так - в стиле процедур Паскаля с подробными конструкциями. Ну или как у растаманов, да.


  1. leshabirukov
    29.08.2024 12:26

    import sys
    print( sys.version )
    y = (1, 2, 3, (10, 20, 30, 40, 50), (33, 77), 19, 29)
    a, *b, c, (d, e, *f), (g, h), *i = y
    3.9.6 (tags/v3.9.6:db3ff76, Jun 28 2021, 15:26:21) [MSC v.1929 64 bit (AMD64)]
      File "C:\Users\User\AppData\Local\Temp\ipykernel_17596\1470887669.py", line 4
        a, *b, c, (d, e, *f), (g, h), *i = y
        ^
    SyntaxError: multiple starred expressions in assignment

    Это 3.9 недостаточно умный или у автора ошибка?


    1. AKTOO Автор
      29.08.2024 12:26

      естественно, у меня ошибка.
      две и больше * на обном уровне распаковки быть не может - иначе непонятно (ни интерпретатору, ни программисту), в какие переменные класть значения и сколько их класть


  1. anonymous
    29.08.2024 12:26

    НЛО прилетело и опубликовало эту надпись здесь


    1. AKTOO Автор
      29.08.2024 12:26

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


  1. qandak
    29.08.2024 12:26

    Генераторы...

    Так, где генераторы? Один единственный пример, и то в виде функции, еще и обернутый перед назначением переменной в list(), чтоб показать эквивалентность результата. Зачем?

    Однострочные генераторы (generator expression) пишутся в круглых скобочках.

    x = (выражение for i in итератор)

    Тогда генератор будет возвращать только те значения...

    Опять же, не надо путать генератор со списком. List comprehension создает список, не генератор. Переменная получает список как результат, никакой ленивой семантики в примере нет.