В питоне очень много полезного и интересного синтаксического сахара. Настолько много, что у неподготовленных пользователей может случиться сахарный диабет. Здесь вы увидите несколько уникального для питона синтаксического сахара, его примеры правильного и неправильного применения.
Разделители разрядов в числах
Длинные захардкоженные числа очень плохо воспринимаются на глаз.
На письме мы привыкли ставить между разрядами разделители (в России, например, принято писать пробелы, а в Америке - запятые). В коде тоже можно это делать.
Отличать длинные целые числа помогают знаки _
, вставленные между разрядами.
Так, 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)
CrazyElf
29.08.2024 12:26+10Переменная с именем
_
используется не для оптимизаций, а чтобы показать, что хотя нам и пришлось тут написать какую-то переменную, потому что по правилам языка без этого нельзя обойтись, но значение этой переменной нас не интересует, мы его нигде не используем. Это такой хинт для того, кто будет читать этот код.AKTOO Автор
29.08.2024 12:26+1Абсолютно верно. Но, к сожалению, на моем обыте было очень много людей, которые думали, что _ память все-таки экономит.
Написать в подзаголовке "вниманию горе-оптимизаторов" у меня рука не повернулась
Spiritschaser
29.08.2024 12:26+3Этот код полностью эквивалентен следующему
А, так вот как эту бабуйню сделать читаемой!
Spiritschaser
29.08.2024 12:26генераторы. У меня с ними проблем нет, но хочется иногда чтобы код почти блок-схемой выглядел.
domix32
29.08.2024 12:26в смысле как у всяких джавистов-растоманов типа
something() .something_else1() .something_else2() .something_else3()
Или что вы блок-схемой зовёте? Не так же код писать
| something | | /\ / \ ---YES---/cond\---NO--- | \ / | | something_else2 | \ / | something_else2 | \/
Spiritschaser
29.08.2024 12:26Ну, почти так - в стиле процедур Паскаля с подробными конструкциями. Ну или как у растаманов, да.
leshabirukov
29.08.2024 12:26import 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 недостаточно умный или у автора ошибка?
AKTOO Автор
29.08.2024 12:26естественно, у меня ошибка.
две и больше * на обном уровне распаковки быть не может - иначе непонятно (ни интерпретатору, ни программисту), в какие переменные класть значения и сколько их класть
anonymous
29.08.2024 12:26НЛО прилетело и опубликовало эту надпись здесь
AKTOO Автор
29.08.2024 12:26Всегда, где есть сложные выражения или логические конструкции. Например, если проверяется больше 1 условия, или если кроме вычислений идут какие-то другие действия.
Например для задач возвести все нечетные числа в квадрат или запустить корутины списковые выражения подойдут. Для более сложных, например подключиться к нескольким сокетам и собирать данные в кортежи, стоит написать генератор
qandak
29.08.2024 12:26Генераторы...
Так, где генераторы? Один единственный пример, и то в виде функции, еще и обернутый перед назначением переменной в list(), чтоб показать эквивалентность результата. Зачем?
Однострочные генераторы (generator expression) пишутся в круглых скобочках.
x = (выражение for i in итератор)
Тогда генератор будет возвращать только те значения...
Опять же, не надо путать генератор со списком. List comprehension создает список, не генератор. Переменная получает список как результат, никакой ленивой семантики в примере нет.
dopusteam
А нельзя сделать так?
Мне вообще кажется, тут с примером что то не то
navferty
Да, похоже что наоборот, выводится "простое" вместо "не простое".
AKTOO Автор
Исправил код. Смысл примера не в конкретно этом примере (на простоту можно и по-другому проверить), а в том, что отпадает необходимость в флагах.
event1
а ещё лучше вообще сделать так:
navferty
Я не питонист, но мне кажется Вы тоже немного перемудрили с циклом - получается, что Вы проверяете чтобы оно делилось нацело на каждое из чисел в интервале от 2 до корня.
https://www.online-python.com/daMsAQ3HXO
event1
да, конечно вы правы, там должно быть !=