← назад к разделу

PYTHON · FUNCTIONS AND MODULES Фаза 2. Основы языка: Python

Обязательно

Функции и модули: аргументы, замыкания, import

Python · Closures · Imports

Аргументы, значения по умолчанию, область видимости и импорт.

1. Функция называет операцию

1 / 5

def задаёт параметры и тело, return возвращает результат. Без return функция возвращает None.

def area(width, height): return width * heightarea(3, 4) → 12no return → None

2. Аргументы бывают разных видов

2 / 5

Позиционные зависят от порядка, именованные явно называют параметр. *args собирает позиционные, **kwargs - именованные.

Positionalarea(3, 4)Keywordarea(width=3, height=4)clear intent

3. Значение по умолчанию создаётся один раз

3 / 5

Mutable default сохраняется между вызовами. Вместо items=[] используют items=None и создают list внутри.

items=[]call 1 → [1]call 2 → [1, 2]Shared state

4. Замыкание хранит окружение

4 / 5

Внутренняя функция помнит переменные внешней области. nonlocal позволяет изменить их, но глобальное состояние лучше ограничивать.

make_countercount = 0inner() → count + 1closure remembers environment

5. Модуль изолирует пространство имён

5 / 5

Import загружает файл один раз и кеширует его. if __name__ == "__main__" запускает код только при прямом вызове.

import modulesys.modules cacheloaded oncemain guard

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

Как объявить функцию

Функция объявляется ключевым словом def: имя, скобки с параметрами, двоеточие и тело с отступом. Результат возвращается через return.

live example

def area(width, height):
    return width * height

print(area(3, 4))  # 12
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

Если return нет (или он без значения), функция возвращает None — специальный объект «ничего». Это не ошибка, а нормальное поведение для функций, которые что-то делают, но ничего не отдают (например, печатают).

live example

def greet(name):
    print(f"Привет, {name}")

result = greet("Аня")  # печатает приветствие
print(result)          # None
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

Короткая формула: def объявляет функцию, return отдаёт значение, без return отдаётся None.

Позиционные и именованные аргументы

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

live example

def connect(host, port):
    print(f"{host}:{port}")

connect("localhost", 5432)        # позиционно
connect(port=5432, host="localhost")  # по имени, порядок не важен
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

Именованные аргументы удобны, когда у функции много параметров и из вызова неочевидно, что есть что. Сравните create_user(True, False, True) и create_user(is_admin=True, is_active=False, send_email=True) — второй вариант читается сам.

Значения по умолчанию

Параметру можно задать значение по умолчанию — тогда при вызове его можно не передавать.

live example

def connect(host, port=5432, timeout=30):
    print(f"{host}:{port}, таймаут {timeout}")

connect("localhost")              # localhost:5432, таймаут 30
connect("localhost", timeout=5)   # localhost:5432, таймаут 5
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

Параметры со значениями по умолчанию идут после обычных — иначе Python не поймёт, где заканчиваются обязательные.

Важная ловушка: не используйте изменяемый объект как значение по умолчанию. Значение вычисляется один раз — при объявлении функции, а не при каждом вызове. Поэтому список «живёт» между вызовами:

live example

def add_item(item, basket=[]):   # так НЕ надо
    basket.append(item)
    return basket

print(add_item("яблоко"))  # ['яблоко']
print(add_item("груша"))   # ['яблоко', 'груша'] — корзина не очистилась!
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

Правильный приём — значение по умолчанию None, а реальный объект создавать внутри:

def add_item(item, basket=None):
    if basket is None:
        basket = []
    basket.append(item)
    return basket

*args и **kwargs: переменное число аргументов

Иногда заранее неизвестно, сколько аргументов передадут. Тогда параметр с * собирает все лишние позиционные аргументы в кортеж, а ** — все лишние именованные в словарь.

live example

def log(*args, **kwargs):
    print("позиционные:", args)    # кортеж
    print("именованные:", kwargs)  # dict

log(1, 2, 3, level="INFO", user="Аня")
# позиционные: (1, 2, 3)
# именованные: {'level': 'INFO', 'user': 'Аня'}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

Имена args и kwargs — это просто соглашение, значимы именно * и **. Эти же звёздочки работают и в обратную сторону — чтобы распаковать коллекцию в аргументы:

live example

def point(x, y):
    return f"({x}, {y})"

coords = [10, 20]
print(point(*coords))   # распаковали список → point(10, 20)

data = {"x": 1, "y": 2}
print(point(**data))    # распаковали словарь → point(x=1, y=2)
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

Область видимости

Область видимости (scope) — это то, где переменная «видна». В Python переменные ищутся по правилу LEGB: сначала Local (внутри функции), потом Enclosing (объемлющая функция), потом Global (на уровне модуля), потом Built-in (встроенные имена вроде len).

Local внутри функции Enclosing объемлющая функция Global модуль Builtins встроенные имена

Правило LEGB: имя ищут от внутренней области к внешней; присваивание внутри функции создаёт локальное имя, и внешнее за ним не видно.

live example

x = "глобальная"

def outer():
    x = "локальная"   # своя переменная, глобальную не трогает
    print(x)

outer()    # локальная
print(x)   # глобальная
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

Внутри функции можно читать глобальную переменную, но присваивание создаёт новую локальную. Чтобы изменить именно глобальную, нужен global; чтобы изменить переменную объемлющей функции — nonlocal. На практике оба используют редко: менять внешнее состояние из функции — частый источник трудноуловимых ошибок, лучше возвращать значение через return.

Функции — это объекты

В Python функция — обычный объект, как число или строка. Её можно положить в переменную, передать в другую функцию, вернуть из функции, сложить в список.

live example

def shout(text):
    return text.upper()

f = shout            # без скобок — это сам объект-функция
print(f("привет"))   # ПРИВЕТ

# передаём функцию как аргумент
names = ["bob", "ann"]
print(list(map(shout, names)))  # ['BOB', 'ANN']
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

Это и есть фундамент для многих приёмов: коллбэков, сортировки по ключу (sorted(..., key=...)), декораторов.

Для короткой функции из одного выражения есть lambda — безымянная функция прямо в месте использования:

orders = [{"id": "a", "total": 300}, {"id": "b", "total": 100}]
cheapest_first = sorted(orders, key=lambda o: o["total"])

Внутри lambda только одно выражение: ни return, ни инструкций. Как только логика перестаёт помещаться в строку, её выносят в обычную def с именем.

Замыкания

Замыкание (closure) — это функция, которая «запоминает» переменные из того места, где она была создана, даже после того, как внешняя функция завершилась.

live example

def make_multiplier(factor):
    def multiply(value):
        return value * factor   # factor взят из объемлющей функции
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(10))  # 20
print(triple(10))  # 30
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

double помнит, что factor равен 2, хотя make_multiplier уже отработала. Замыкания — удобный способ настроить функцию параметрами и получить «готовую к работе» специализированную версию.

Модули: один файл — один модуль

Модуль — это просто файл .py. Всё, что в нём объявлено (функции, классы, переменные), можно подключить в другом файле через import.

Пусть есть файл geometry.py:

# geometry.py
PI = 3.14159

def circle_area(radius):
    return PI * radius ** 2

Подключаем его в другом файле:

import geometry
print(geometry.circle_area(5))   # обращаемся через имя модуля
print(geometry.PI)

# или импортируем отдельные имена
from geometry import circle_area, PI
print(circle_area(5))

# можно дать псевдоним
import geometry as geo
print(geo.circle_area(5))

Форма from module import * тянет всё подряд и засоряет пространство имён — на практике её избегают. Лучше импортировать явно то, что нужно.

Пакеты: папка с модулями

Пакет — это папка с модулями. Раньше требовался файл __init__.py, чтобы папка считалась пакетом; сейчас он не обязателен, но часто его всё равно добавляют (в нём можно собрать публичный интерфейс пакета). Внутри пакета модули адресуются через точку:

from shop.orders import create_order
import shop.payments as payments

Здесь shop — папка-пакет, orders и payments — модули внутри неё.

if __name__ == "__main__"

Когда Python запускает файл напрямую (python geometry.py), он присваивает специальной переменной __name__ значение "__main__". Когда же файл импортируют, в __name__ оказывается имя модуля ("geometry").

Это позволяет отделить код, который должен выполняться только при прямом запуске, от кода, который просто объявляет функции для импорта:

# geometry.py
def circle_area(radius):
    return PI * radius ** 2

if __name__ == "__main__":
    # выполнится только при `python geometry.py`,
    # но НЕ при `import geometry`
    print(circle_area(5))

Без этой проверки демонстрационный или отладочный код сработал бы при каждом импорте модуля — что почти никогда не нужно.

Дополнительно: при первом чтении можно пропустить

Глубже: параметры только по имени, ловушка замыкания и декораторрасширенное

В сигнатуре можно зафиксировать, как передавать аргумент. Всё, что левее /, принимается только по позиции, всё, что правее *, — только по имени:

def connect(host, /, port=5432, *, timeout=30):
    ...

connect("localhost", timeout=5)      # так можно
connect(host="localhost")            # TypeError: host только позиционный
connect("localhost", 5432, 10)       # TypeError: timeout только по имени

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

Замыкание запоминает переменную, а не её значение в момент создания. Поэтому три функции из цикла вернут одно и то же:

live example

callbacks = [lambda: i for i in range(3)]
print([f() for f in callbacks])            # [2, 2, 2]

callbacks = [lambda i=i: i for i in range(3)]
print([f() for f in callbacks])            # [0, 1, 2] — значение зафиксировано по умолчанию
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

Тот же эффект даёт functools.partial(f, i): он подставляет аргумент сразу.

Декоратор — это функция, которая принимает функцию и возвращает её обёртку. Запись @timed над def означает handler = timed(handler):

import functools, time

def timed(fn):
    @functools.wraps(fn)                   # сохраняет имя и документацию оригинала
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        try:
            return fn(*args, **kwargs)
        finally:
            print(f"{fn.__name__}: {time.perf_counter() - start:.3f} c")
    return wrapper

@timed
def handler(order_id):
    ...

На этом же механизме держатся @property, @dataclass, @contextmanager и маршруты FastAPI вроде @app.get("/orders"): это обычные функции, которым передали вашу.

Три вопроса про декораторы задают чаще других. Первый: что будет, если wrapper не вернуть. Тогда timed(handler) вернёт None, и имя handler станет None: при вызове упадёт TypeError: 'NoneType' object is not callable. Второй: чем @retry отличается от @retry(). Без скобок декоратором становится сама retry, со скобками сначала вызывается retry(), и декоратором становится то, что она вернула. Поэтому декоратор с параметрами это фабрика декораторов, функция на три уровня:

live example

import functools


def retry(times):
    def decorator(fn):
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return fn(*args, **kwargs)
                except ConnectionError as e:
                    print(f"попытка {attempt}: {e}")
            raise RuntimeError("не удалось")
        return wrapper
    return decorator


calls = 0


@retry(times=3)
def fetch():
    global calls
    calls += 1
    if calls < 3:
        raise ConnectionError("нет сети")
    return "данные"


print(fetch())  # две неудачи, потом данные
print(fetch.__name__)  # fetch: wraps сохранил имя
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

Третий: зачем functools.wraps. Без него fetch.__name__ станет wrapper, сломаются документация, логи по имени функции и pickle, а FastAPI не увидит исходную сигнатуру и аннотации. Декоратором может быть любой вызываемый объект, в том числе класс с __call__, а применять его можно и к классам: @dataclass именно такой.

Коротко

  • Функция объявляется через def, возвращает значение через return; без return отдаёт None.
  • Аргументы передаются по позиции или по имени; параметрам можно задать значения по умолчанию — но не изменяемые объекты (используйте None и создавайте объект внутри).
  • *args собирает лишние позиционные аргументы в кортеж, **kwargs — именованные в словарь; те же */** распаковывают коллекции в аргументы.
  • Переменные ищутся по правилу LEGB; присваивание внутри функции создаёт локальную переменную.
  • Функция — это объект: её можно передавать, возвращать и хранить; на этом строятся замыкания, запоминающие переменные из места создания.
  • Модуль — это файл .py, пакет — папка с модулями; подключаются через import.
  • Проверка if __name__ == "__main__" отделяет код прямого запуска от кода для импорта.
  • lambda — функция из одного выражения для key= и коллбэков; / и * в сигнатуре задают, как передавать аргументы; декоратор — функция, принимающая функцию и возвращающая обёртку.
  • Декоратор с параметрами это фабрика на три уровня: @retry(times=3) сначала вызывает retry, а декоратором становится результат; без return wrapper функция превращается в None; functools.wraps сохраняет имя и сигнатуру.

Что почитать дальше