open atlas
↑ К треку
Python для JS/TS-разработчиков PY · 01 · 03

Comprehensions и функции

Comprehension — питоничный map/filter; generator expression стримит лениво по огромным входам. У функций острая ловушка — аргументы по умолчанию вычисляются один раз при def — плюс *args/**kwargs и LEGB-замыкания, непохожие на JS.

PY Middle ◷ 17 min
Уровень
ОсновыJuniorMiddleSenior

Коллега переносит JS-пайплайн на Python и пишет result = list(map(lambda x: x * 2, filter(lambda x: x > 0, nums))). Работает, но читается как перевод, а не как Python. А другая функция, def add_tag(tag, tags=[]), вдруг начинает возвращать теги из прошлых вызовов, которых она не получала, — призрак, копящийся на весь процесс. Оба случая — один и тот же урок: инструменты Python на уровне выражений и его модель функций ведут себя не как в JavaScript, и расхождения бьют ровно там, где ты перестаёшь следить.

Comprehension: map и filter, слитые в одно выражение

List comprehension — идиоматичная замена цепочке .map() и .filter() в Python. Форма [f(x) for x in xs if pred(x)] читается в порядке вычисления — источник, фильтр, преобразование — и за один проход даёт список. Там, где JavaScript пишет xs.filter(pred).map(f), Python ставит преобразование первым, а фильтр последним, но данные текут так же: каждый элемент xs проверяется pred, и выжившие проходят через f.

nums = [-2, -1, 0, 1, 2, 3]
# JS-эквивалент: nums.filter(x => x > 0).map(x => x * 2)
doubled = [x * 2 for x in nums if x > 0]   # [2, 4, 6]

# Тот же синтаксис, другие скобки, другая коллекция:
squares = {x: x * x for x in range(4)}      # dict: {0: 0, 1: 1, 2: 4, 3: 9}
unique  = {len(w) for w in ["a", "bb", "cc"]}  # set: {1, 2}

Форма {...} даёт dict или set comprehension в зависимости от того, пишешь ли ты k: v или голый элемент, — настолько прямого аналога объектному литералу в JS нет. Это первая причина, почему comprehensions важны: одно семейство синтаксиса покрывает map, filter и построение любой встроенной коллекции.

Вложенные comprehensions существуют — [c for row in matrix for c in row] расплющивает 2D-список, причём циклы читаются слева направо, как если бы ты вложил for-инструкции. Но именно тут читаемость гибнет быстро. Comprehension с двумя for и условием — на грани того, что читатель распарсит с одного взгляда; три — уже за гранью. Сеньорское суждение: comprehension должен умещаться в голове за один проход. Как только перестаёт — пиши явный цикл. Хитрость в comprehension — это цена, которую платишь на каждом будущем прочтении.

Генераторы: та же форма, но лениво

Замени квадратные скобки на круглые, и [f(x) for x in xs] становится generator expression (f(x) for x in xs). Это не косметика. List comprehension жадный: он строит весь список в памяти до того, как выполнится следующая строка. Генератор ленивый: вычисляет по одному значению, только когда попросят, и держит по сути постоянную память независимо от того, сколько элементов через него проходит.

# Жадно: материализует 10 миллионов int — сотни МБ резидентно.
total = sum([x * x for x in range(10_000_000)])

# Лениво: по одному значению, память остаётся плоской.
total = sum(x * x for x in range(10_000_000))

Та же идея движет ключевым словом yield. Функция, содержащая yield, — это функция-генератор: её вызов не выполняет тело, а возвращает объект-генератор, который работает лениво, приостанавливаясь на каждом yield и возобновляясь по следующему запросу. Так стримят данные, не помещающиеся в память.

def read_lines(path):
    with open(path) as f:
        for line in f:
            yield line.rstrip("\n")   # резидентна одна строка, а не весь файл

for line in read_lines("huge.log"):    # постоянная память на файле 50 ГБ
    ...

Это и есть настоящий сеньорский смысл всей первой половины. Тянись к жадному списку, когда результат нужен не один раз, нужна его длина или индексация. Тянись к ленивому генератору, когда вход большой или неограниченный, ты проходишь по нему один раз и хочешь, чтобы память оставалась плоской, — стриминговый пайплайн, чтение файла, пагинатор API.

Почему это работает

Генератор одноразовый: это итератор, который исчерпывается по мере потребления. После одного полного прохода он пуст — второй for-цикл по тому же объекту-генератору не даст ничего, а list(gen) после того, как ты уже проитерировал, вернёт []. Это удивляет тех, кто пришёл из JS-массивов, которые можно перебирать сколько угодно раз. Если нужно пройти данные дважды — либо материализуй их один раз через list(gen), либо построй новый генератор. Лень, экономящая память, ровно и делает его одноразовым.

Функции: аргументы и ловушка, определяющая Python

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

def определяет функцию; аргументы передаются позиционно или по ключу, а параметры могут нести значения по умолчанию: def connect(host, port=5432, *, timeout=30). Голая * помечает всё после неё только-по-ключу — вызывающий обязан писать timeout=10, а не голую позицию, — так держат место вызова читаемым по мере роста сигнатуры. *args собирает лишние позиционные в кортеж, а **kwargs — лишние ключевые в словарь; на месте вызова те же */** распаковывают итерируемое или отображение в аргументы — питоновский аналог JS spread.

def log(level, *args, **kwargs):
    print(level, args, kwargs)

log("INFO", "started", port=8080)   # INFO ('started',) {'port': 8080}

Теперь ловушка. Аргумент по умолчанию вычисляется ровно один раз, когда выполняется инструкция def, — а не на каждый вызов. Если default — изменяемый объект, каждый вызов, опускающий этот аргумент, делит один и тот же объект, и мутации сохраняются между вызовами.

def add_tag(tag, tags=[]):     # [] создаётся ОДИН раз, при def
    tags.append(tag)
    return tags

add_tag("a")   # ['a']
add_tag("b")   # ['a', 'b']  ← тот же список, переживший вызовы

Это не баг Python; это прямое следствие правила «defaults вычисляются во время определения». JavaScript перевычисляет параметры по умолчанию на каждый вызов, поэтому function f(x = []) свежий каждый раз — ровно наоборот. Идиоматичный питоновский фикс — sentinel:

def add_tag(tag, tags=None):
    if tags is None:
        tags = []          # свежий список на каждый вызов, опускающий его
    tags.append(tag)
    return tags

Lambda, замыкания и LEGB-scope

lambda создаёт анонимную функцию, но она намеренно ограничена: одно выражение, никаких инструкций, никаких аннотаций. Это не JS arrow function — многострочной lambda не бывает. Используй её для одноразовой key-функции (sorted(words, key=lambda w: len(w))) и тянись к именованному def в тот же миг, когда ей нужна вторая строка.

Функции — объекты первого класса (передаются, возвращаются, хранятся), а внутренняя функция, ссылающаяся на переменную внешней, образует замыкание (closure), захватывая её по ссылке. Разрешение имён идёт по LEGB: Python смотрит в Local-scope, затем в любые объемлющие (Enclosing) scope функций, затем в Global (модульный) scope, затем в Built-ins. Важно: присваивание имени внутри функции делает его локальным на всю функцию, если не сказать иное, — global перепривязывает имя уровня модуля, а nonlocal — имя в ближайшей объемлющей функции. JS тоже разрешает scope лексически, но let/const дают привязку на блок, тогда как единица scope в Python — функция, и «присваивание означает локальное» — более острый край.

def make_counter():
    count = 0
    def tick():
        nonlocal count    # без этого count += 1 кидает UnboundLocalError
        count += 1
        return count
    return tick
Идиома JavaScriptИдиома PythonЗаметка
xs.map(f)[f(x) for x in xs]Comprehension, один проход
xs.filter(p)[x for x in xs if p(x)]Фильтр — это хвостовой if
x => x * 2lambda x: x * 2Только одно выражение; иначе def
f(…arr) / {…obj}f(*seq) / f(**mapping)Spread → распаковка
function(…args)def f(*args, **kwargs)Rest-параметры → *args/**kwargs
генераторы / yield(f(x) for x in xs) / yieldТа же лень, круглые скобки, не квадратные
function f(x = []) свежий каждый вызовdef f(x=None) + sentinelDefault в Python вычисляется один раз при def
Викторина

Что напечатает add_tag('a'); add_tag('b'), если def add_tag(tag, tags=[]): tags.append(tag); return tags?

Викторина

g = (x*x for x in range(3)). После total = sum(g) что вернёт list(g)?

Выбери лучший вариант

Пайплайн должен преобразовать лог-файл 50 ГБ построчно и записать сводку на машине с 2 ГБ RAM. Выбери конструкцию.

Вспомните перед уходом
  1. 01
    Когда выбирать generator expression вместо list comprehension и какова цена?
  2. 02
    Почему def f(x=[]) копит состояние между вызовами и каков идиоматичный фикс?
Итог

Comprehension — это слитые map и filter Python: [f(x) for x in xs if pred(x)] заменяет цепочку .filter().map() одним читаемым проходом, а форма {...} строит ещё и dict, и set, — но comprehension, который больше не умещается в голове за один проход, должен стать явным циклом. Замена квадратных скобок на круглые превращает его в ленивый generator expression (выражение-генератор, вычисляет по одному значению) — та же ленивая механика, что и yield: по одному значению, постоянная память, одноразовость. Собирай списком, когда результат нужен многократно, нужна длина или индексация; стримь генератором, когда вход большой или одно-проходный. На стороне функций *args/**kwargs собирают и распаковывают аргументы, голая * делает параметры только-по-ключу, а lambda — одноразовое одно-выражение; тянись к def, как только она растёт. Определяющая ловушка: аргументы по умолчанию вычисляются один раз при def, поэтому изменяемый default вроде [] общий для вызовов; используй None плюс sentinel. Наконец, разрешение имён — LEGB, scope — на функцию, а не на блок, и присваивание делает имя локальным, пока ты не тянешься к global или nonlocal, чтобы перепривязать внешнее. Теперь, когда встречаешь в сигнатуре x=None с проверкой сразу в теле или генераторное выражение, переданное прямо в sum(), — ты видишь намеренный паттерн, а не случайность.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 5 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.