Функции: def, return, аннотации, scope (LEGB)

Функции - основной строительный блок программ. В Python они объявляются через def, могут принимать аргументы любого типа, возвращать значения (в том числе несколько сразу) и являются объектами первого класса. В этом уроке - базовое объявление, возврат, аннотации, docstrings и правила области видимости (LEGB).

Базовое объявление

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

print(greet("Alice"))  # Привет, Alice!

Структура:

  • def - ключевое слово
  • greet - имя функции (по PEP 8: snake_case)
  • (name) - список параметров
  • : - двоеточие
  • Тело с отступом
  • return - возврат значения (опциональный)

Без явного return функция неявно возвращает None:

def log(message):
    print(f"[LOG] {message}")

result = log("hello")
print(result)  # None

Аннотации типов

С Python 3 принято писать аннотации:

def add(a: int, b: int) -> int:
    return a + b

def greet(name: str, polite: bool = True) -> str:
    if polite:
        return f"Здравствуйте, {name}"
    return f"Привет, {name}"

Это не проверяется в runtime, но помогает mypy, IDE и читателю. Подробно про типы - в уроке про type hints.

Несколько возвращаемых значений

В Python функция всегда возвращает одно значение, но это может быть кортеж:

def divmod_safe(a: int, b: int) -> tuple[int, int]:
    if b == 0:
        return 0, 0
    return a // b, a % b

q, r = divmod_safe(7, 3)  # 2, 1

Тут возвращается кортеж (2, 1), и распаковывается в две переменные. Это идиоматичный способ вернуть несколько значений.

Если значений много или они логически связаны - лучше dataclass или NamedTuple:

from typing import NamedTuple

class Result(NamedTuple):
    success: bool
    value: int
    error: str

def parse(s: str) -> Result:
    try:
        return Result(True, int(s), "")
    except ValueError as e:
        return Result(False, 0, str(e))

r = parse("42")
print(r.success, r.value)

NamedTuple даёт читаемые имена полей, оставаясь иммутабельным и легковесным.

Docstrings

Документация функции пишется в первой строке тела как обычная строка:

def calculate_tax(amount: float, rate: float = 0.13) -> float:
    """Считает налог по сумме и ставке.

    Args:
        amount: Сумма до налогообложения
        rate: Ставка налога (по умолчанию 0.13 для НДФЛ)

    Returns:
        Размер налога
    """
    return amount * rate

Docstring доступен через function.__doc__ и через help(function). IDE используют его для подсказок.

Форматы docstring:

  • Google style (как выше) - популярный, читаемый
  • NumPy style - для научных проектов
  • Sphinx (reST) - старый стандарт

В новых проектах чаще выбирают Google style.

Docstring - это строка, привязанная к объекту (функции, классу, модулю). Доступна через `__doc__`. Используется генераторами документации.

Комментарий # ... - просто текст для читателя, недоступен в runtime.

Используй docstrings для публичных API, комментарии - для пояснений непонятных мест.

Параметры по умолчанию

def connect(host: str, port: int = 5432, timeout: float = 30.0) -> Connection:
    ...

connect("localhost")                    # порт 5432, timeout 30
connect("localhost", 5433)              # порт 5433, timeout 30
connect("localhost", timeout=10)        # порт 5432, timeout 10
connect(host="db.example.com")          # все по умолчанию

Дефолтные значения вычисляются один раз при определении функции. Это уже разбирали как ловушку с mutable defaults в уроке про изменчивость:

# ПЛОХО
def append_item(item, items=[]):
    items.append(item)
    return items

# ХОРОШО
def append_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

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

При вызове можно передавать аргументы по позиции или по имени:

def make_url(scheme, host, port=80, path=""):
    return f"{scheme}://{host}:{port}{path}"

# Позиционно
make_url("https", "example.com", 443, "/api")

# С именами (keyword)
make_url(scheme="https", host="example.com", port=443, path="/api")

# Смешанно (позиционные сначала, потом keyword)
make_url("https", "example.com", port=443, path="/api")

Именованные аргументы делают код самодокументируемым, особенно для функций с многими параметрами.

Возврат None - явный vs неявный

# Неявный return None
def do_work(item):
    if item.valid:
        item.process()

# Явный
def do_work(item):
    if not item.valid:
        return None
    item.process()
    return None

# Идиоматично - guard и без return
def do_work(item):
    if not item.valid:
        return
    item.process()

В Python пустой return равносилен return None. Линтеры обычно требуют согласованности: либо везде явный return None, либо нигде. Smешивать в одной функции - плохо.

LEGB - правила области видимости

Python ищет имена по правилу LEGB:

  1. Local - текущая функция
  2. Enclosing - окружающая функция (для вложенных)
  3. Global - модуль
  4. Built-in - встроенные (print, len, ...)
x = "global"

def outer():
    x = "enclosing"

    def inner():
        x = "local"
        print(x)  # local

    inner()
    print(x)  # enclosing

outer()
print(x)  # global

При обращении к x Python ищет в локальной области, потом во внешних, потом в модуле, потом во встроенных.

global и nonlocal

Присваивание создаёт локальное имя по умолчанию:

x = 10

def change():
    x = 20  # локальная, глобальная не изменится

change()
print(x)  # 10

Для изменения глобальной - явное global:

x = 10

def change():
    global x
    x = 20

change()
print(x)  # 20

Для изменения переменной во внешней функции - nonlocal:

def counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment

c = counter()
print(c(), c(), c())  # 1 2 3

Без nonlocal строка count += 1 создала бы локальную переменную count, и += 1 упал бы с UnboundLocalError.

Почти никогда. Глобальные переменные затрудняют тестирование и отладку. Лучше передавать данные явно через параметры и возвращать значения. `global` уместен разве что для констант инициализации модуля или в малых скриптах.

Функции как объекты первого класса

Функции в Python - обычные объекты:

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

# Присваиваем переменной
f = greet
print(f("Bob"))  # Привет, Bob

# Передаём как аргумент
def apply(func, value):
    return func(value)

apply(greet, "Charlie")

# Возвращаем из функции
def make_greeter(greeting):
    def greeter(name):
        return f"{greeting}, {name}"
    return greeter

hello = make_greeter("Hello")
hello("World")  # Hello, World

# Храним в списке/словаре
operations = {
    "add": lambda a, b: a + b,
    "sub": lambda a, b: a - b,
}
operations["add"](2, 3)  # 5

Это даёт мощный инструментарий для абстракций - функции высшего порядка, декораторы, callbacks.

Возврат функции из функции

def make_multiplier(factor):
    def multiply(x):
        return x * factor
    return multiply

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

double(5)  # 10
triple(5)  # 15

Это замыкание - вложенная функция запомнила переменную factor из родительской. Подробно про closures - в уроке про замыкания и lambda.

Распространённые ошибки

1. Mutable default (повтор из урока 9):

def add(item, items=[]):   # плохо
    items.append(item)
    return items

2. Return в else.

# Избыточно
def is_adult(age):
    if age >= 18:
        return True
    else:
        return False

# Лучше
def is_adult(age):
    return age >= 18

3. Изменение mutable аргумента без явного намерения.

def normalize(items):
    items.sort()         # ПОБОЧНЫЙ ЭФФЕКТ - изменяет входной список!
    return items

# Лучше
def normalize(items):
    return sorted(items) # возвращает новый

Если функция модифицирует вход - это должно быть явно по имени (sort_in_place).

Сравнение с Go и PHP

В Go функции тоже first-class и поддерживают замыкания:

func makeMultiplier(factor int) func(int) int {
    return func(x int) int {
        return x * factor
    }
}

В PHP функции стали first-class позже, требуется явный синтаксис:

$double = function ($x) use ($factor) {
    return $x * $factor;
};

Python делает это проще: захват происходит автоматически, без use. И аннотации необязательны.

Мини-задание

  1. Напиши функцию с docstring и аннотациями:
def factorial(n: int) -> int:
    """Считает факториал n!.

    Args:
        n: Неотрицательное целое

    Returns:
        Произведение чисел от 1 до n. Для n=0 возвращает 1.

    Raises:
        ValueError: Если n отрицательное
    """
    if n < 0:
        raise ValueError("Факториал не определён для отрицательных")
    result = 1
    for i in range(2, n + 1):
        result *= i
    return result

print(factorial.__doc__)
print(help(factorial))
  1. Реализуй счётчик через closure:
def make_counter(start=0):
    count = start
    def increment(by=1):
        nonlocal count
        count += by
        return count
    return increment

c = make_counter(10)
print(c())     # 11
print(c())     # 12
print(c(5))    # 17
  1. Функция, возвращающая несколько значений:
def stats(numbers):
    if not numbers:
        return 0, 0, 0
    return min(numbers), max(numbers), sum(numbers) / len(numbers)

low, high, avg = stats([1, 5, 3, 8, 2])
print(f"min={low}, max={high}, avg={avg:.2f}")

Что дальше

Освоили базовое объявление функций и scope. Про то, сколько логики стоит держать в одной функции, есть отдельный урок в треке чистого кода - «Маленькие функции и один смысл». В следующем уроке - продвинутые аргументы: *args, **kwargs, positional-only, keyword-only.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.