Python с нуля · Глава 7 из 10

Глава 7. Функции

Прогресс сохранится в этом браузере (войдите, чтобы синхронизировать).

Объявление функции

Функция — это именованный блок кода, который можно вызывать многократно с разными аргументами. Объявляется ключевым словом def:

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

greet("Анна")     # Привет, Анна!
greet("Борис")    # Привет, Борис!

Главный смысл функции — DRY (Don't Repeat Yourself): написали один раз, вызываем сколько нужно.

Возврат значения: return

Функция может не только действовать (печать), но и возвращать результат через return:

def add(a, b):
    return a + b

result = add(3, 4)
print(result)        # 7
print(add(10, 20))   # 30

Как только встретился return, функция завершается. Если return нет или он без значения — функция возвращает None:

def do_nothing():
    pass

print(do_nothing())   # None

Возврат нескольких значений

Технически возвращается кортеж, но Python позволяет его сразу распаковать:

def min_max(numbers):
    return min(numbers), max(numbers)

lo, hi = min_max([3, 1, 4, 1, 5, 9])
print(lo, hi)   # 1 9

Параметры и аргументы

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

def describe(name, age, city):
    print(f"{name}, {age} лет, г. {city}")

describe("Анна", 28, "Москва")              # позиционные
describe(name="Анна", age=28, city="Москва")# именованные
describe("Анна", city="Москва", age=28)     # смешанно

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

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

greet("Анна")               # Привет, Анна!
greet("Анна", "Здравствуй")  # Здравствуй, Анна!

Важно: параметры со значениями по умолчанию должны идти после обязательных.

Ловушка изменяемого значения по умолчанию

Это классический грабли Python — избегайте изменяемых объектов в качестве дефолта:

# ПЛОХО: список создаётся один раз и разделяется между вызовами
def add_item(item, items=[]):
    items.append(item)
    return items

print(add_item("a"))   # ['a']
print(add_item("b"))   # ['a', 'b'] — сюрприз!

# ХОРОШО: используем None как сигнальное значение
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

*args и **kwargs

*args собирает лишние позиционные аргументы в кортеж, **kwargs — именованные в словарь:

def log(message, *args, **kwargs):
    print(f"[{message}]", *args)
    for key, value in kwargs.items():
        print(f"  {key} = {value}")

log("INFO", "запущено", "в 10:00", user="Анна", host="localhost")
# [INFO] запущено в 10:00
#   user = Анна
#   host = localhost

args и kwargs — просто договорённость об именах; сути дела это не меняет (* и ** важны, а не имена).

Распаковка при вызове

Звёздочки работают и в обратную сторону — распаковывают коллекцию в аргументы:

def add(a, b, c):
    return a + b + c

nums = [1, 2, 3]
print(add(*nums))   # 6 — список распакован в a, b, c

opts = {"a": 1, "b": 2, "c": 3}
print(add(**opts))  # 6 — словарь распакован в именованные

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

Переменные внутри функции — локальные, они не видны снаружи:

def f():
    x = 10        # локальная переменная
    print(x)

f()           # 10
# print(x)    # NameError — x не существует снаружи

Функция видит внешние переменные (чтение), но не может их изменить без объявления global:

counter = 0

def increment():
    global counter    # разрешаем менять глобальную
    counter += 1

increment()
print(counter)   # 1

Однако global — обычно признак плохой архитектуры. Лучше передавать значение через параметр и возвращать результат. Глобальные переменные усложняют отладку и тестирование.

Анонимные функции: lambda

lambda — короткая функция в одну строку, без имени. Удобна, когда функция нужна «один раз на месте»:

# Обычная функция
def square(x):
    return x ** 2

# Эквивалентная lambda
square = lambda x: x ** 2

print(square(5))   # 25

Чаще всего lambda используют как аргумент для sorted(), map(), filter():

users = [
    {"name": "Анна", "age": 28},
    {"name": "Борис", "age": 22},
    {"name": "Вера", "age": 35},
]

# Сортировка словарей по ключу age
sorted_users = sorted(users, key=lambda u: u["age"])
print([u["name"] for u in sorted_users])   # ['Борис', 'Анна', 'Вера']

Если lambda не помещается в одну короткую строку — перепишите её в обычный def.

Docstrings — документация функции

Строка сразу после def становится документацией функции. Её видят редакторы и инструменты автогенерации документации:

def divide(a, b):
    '''Возвращает частное a / b. Возбуждает ZeroDivisionError при b == 0.'''
    return a / b

print(divide.__doc__)   # Возвращает частное a / b...

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

Аннотации — это «подсказки» о типах параметров и возвращаемого значения. Python их не проверяет во время выполнения, но их используют редакторы и статические анализаторы (mypy):

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

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

# Составные типы
def average(numbers: list[int]) -> float:
    return sum(numbers) / len(numbers)

def get_user(user_id: int) -> dict[str, str] | None:
    ...

Символ | (Python 3.10+) означает «или»: dict | None — «словарь или None».

Кейс из реального проекта: helpers для магазина

Несколько небольших функций-помощников для обработки цен и скидок. Каждая с аннотацией и docstring:

def format_price(amount: float) -> str:
    '''Форматирует цену: 199.5 -> '199.50 ₽'.'''
    return f"{amount:.2f} ₽"

def apply_discount(price: float, percent: float) -> float:
    '''Возвращает цену со скидкой. percent — от 0 до 100.'''
    if not 0 <= percent <= 100:
        raise ValueError("Скидка должна быть от 0 до 100")
    return price * (1 - percent / 100)

def total_price(items: list[tuple[str, float]]) -> float:
    '''Сумма цен из списка пар (название, цена).'''
    return sum(price for _, price in items)

cart = [("Ноутбук", 89990.0), ("Мышь", 1200.0)]
print(format_price(apply_discount(total_price(cart), 10)))
# 82071.00 ₽

Заметьте: каждая функция делает одно дело и легко тестируется отдельно. Это и есть хорошая декомпозиция.

Типичные ошибки

1. Изменяемый дефолт

# Плохо
def f(items=[]):
    items.append(1)
    return items

# Хорошо
def f(items=None):
    if items is None:
        items = []
    ...

2. Функция что-то считает, но забыли return

def double(x):
    x * 2      # результат потерян, функция вернёт None

def double(x):
    return x * 2   # правильно

3. Слишком много global

total = 0
def add(x):
    global total   # лучше возвращать и присваивать снаружи
    total += x

Практика

  1. Напишите функцию is_even(n), возвращающую bool.
  2. Сделайте функцию format_name(first, last, uppercase=False) с параметром по умолчанию.
  3. Реализуйте sum_all(*args), возвращающую сумму всех аргументов.
  4. Отсортируйте список строк по длине через key=len и через lambda.
  5. Добавьте аннотации типов и docstring к функции из задания 1.

Итог

Функции — основной способ структурировать код. Мы изучили объявление, return, параметры и значения по умолчанию, *args и **kwargs, области видимости, lambda, docstrings и аннотации типов. В следующей главе — работа с файлами и обработка исключений, без которых не обходится ни одно реальное приложение.

Содержание серии (10)
{# Звук celebration (этап 4.3):