Программирование с ИИ
ENRU
6.2

Идиомы, которые ИИ пишет постоянно

шесть конструкций, после которых Python читается свободно

Что это

У Python есть набор оборотов, которые ИИ использует почти в каждом файле. Они короче обычных циклов и считаются «питоновскими» — поэтому модель тянется к ним, даже когда можно проще.

Выучить их надо один раз. После этого сгенерированный Python читается почти без спотыканий.

Как выглядит в коде

from dataclasses import dataclass
from pathlib import Path


@dataclass
class City:
    name: str
    temp: float | None = None


cities = [City("Oslo", 4.0), City("Tromsø"), City("Bergen", 7.5)]

warm = [c.name for c in cities if c.temp is not None and c.temp > 5]
print("тёплые:", warm)

for i, c in enumerate(cities, start=1):
    print(f"{i}. {c.name}: {c.temp if c.temp is not None else 'нет данных'}")

codes = ["OSL", "TOS", "BGO"]
print(dict(zip(codes, (c.name for c in cities))))

p = Path("/tmp/idioms.py")
print(p.name, "|", p.suffix, "|", p.exists())

Вывод:

измерено
тёплые: ['Bergen']
1. Oslo: 4.0
2. Tromsø: нет данных
3. Bergen: 7.5
{'OSL': 'Oslo', 'TOS': 'Tromsø', 'BGO': 'Bergen'}
idioms.py | .py | True

1. Списковое включение

warm = [c.name for c in cities if c.temp is not None and c.temp > 5]

Читается слева направо как предложение: собрать c.name для каждого c из cities, где температура выше пяти.

Форм три, и различаются они только скобками:

ЗаписьЧто получается
[x for x in ...]список
{k: v for ...}словарь
(x for x in ...)генератор — считается по мере обхода

Третья форма экономит память на больших данных, но её нельзя пройти дважды: после первого обхода генератор пуст.

2. @dataclass

@dataclass
class City:
    name: str
    temp: float | None = None

Избавляет от __init__ и даёт читаемый вывод. Когда ИИ предлагает класс только для хранения данных — это должен быть @dataclass (статья 2.7).

3. enumerate

for i, c in enumerate(cities, start=1):

Индекс и элемент сразу. Если видишь for i in range(len(x)) — это тот же цикл, написанный неудобно.

4. zip

dict(zip(codes, names))

Идёт по двум спискам параллельно. Важная деталь: zip молча останавливается на том, который короче. Если списки разной длины, часть данных пропадёт без единой ошибки.

5. pathlib

p = Path("/tmp/idioms.py")
p.name, p.suffix, p.exists()
p.parent / "other.py"

Современная замена os.path. Косая черта вместо os.path.join — не опечатка, а перегруженный оператор.

6. f-строки

f"{c.name}: {c.temp if c.temp is not None else 'нет данных'}"

Внутри фигурных скобок — обычное выражение. Полезные дополнения:

ЗаписьЧто делает
f"{x!r}"показывает кавычки и невидимое (статья 4.5)
f"{x:.2f}"два знака после запятой
f"{x=}"печатает x=значение — удобно при отладке

Как сказать ИИ

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

Обратный приём — ограничить питоновскую изощрённость заранее:

Запрос
Пиши просто: обычные циклы вместо вложенных включений,
без walrus-оператора и однострочников. Код должны читать
не только ты и я.

На что смотреть

if x: там, где ноль — нормальное значение. if c.temp: отбросит и None, и 0.0: мороз в ноль градусов станет «нет данных». Пустая строка, 0 и [] тоже ложны. Если проверяешь «есть ли значение» — пиши is not None. ИИ путает эти две проверки постоянно.

Вложенные включения. [y for x in data for y in x if y] — формально питоновски, фактически нечитаемо. Два уровня — предел.

zip на списках разной длины. Молча обрежет по короткому.

Генератор вместо списка там, где нужен повторный обход. Второй проход даст пустоту, без ошибки.

Walrus-оператор :=. if (n := len(data)) > 10: — экономит строку, усложняет чтение. ИИ использует его чаще, чем нужно.

dict вместо dataclass. data["temp"] ломается опечаткой в ключе, city.temp — нет. Если структура известна заранее, ей место в классе.

Шпаргалка

  • Включения: [...] список, {k: v ...} словарь, (...) генератор.

  • Генератор проходится один раз.

  • @dataclass — класс только для данных.

  • enumerate вместо range(len(...)), zip для двух списков.

  • zip молча режет по короткому списку.

  • f"{x!r}" и f"{x=}" — для отладки; pathlib вместо os.path.