Функции: 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.
Комментарий # ... - просто текст для читателя, недоступен в 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:
- Local - текущая функция
- Enclosing - окружающая функция (для вложенных)
- Global - модуль
- 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.
Функции как объекты первого класса
Функции в 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. И аннотации необязательны.
Мини-задание
- Напиши функцию с 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))
- Реализуй счётчик через 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
- Функция, возвращающая несколько значений:
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.