💻 Примеры

⚡ Функции и области видимости на одном экране

def greet(name, greeting="Hello", *, loud=False):
    text = f"{greeting}, {name}!"
    return text.upper() if loud else text
print(greet("Alice"))                    # Hello, Alice!
print(greet("Bob", loud=True))           # HELLO, BOB!
counter = 0
def increment():
    global counter      # без global — UnboundLocalError
    counter += 1
КонструкцияЧто делаетПример
*argsУпаковывает позиционные аргументы в tupledef f(*args)
**kwargsУпаковывает именованные аргументы в dictdef f(**kwargs)
returnВозвращает значение и завершает функциюбез return — результат None
LEGBПорядок поиска имениLocal → Enclosing → Global → Built-in
global/nonlocalРазрешают менять внешнюю переменнуюglobal counter
Топ-3 ошибки: изменяемое значение по умолчанию def f(items=[]) · функция без return даёт None · присвоение без global/nonlocal → UnboundLocalError.

Примеры идут от основ (def, return, аргументы) к упаковке/распаковке, области видимости LEGB и типичным ловушкам, которые встречаются в реальном коде, а не только в этой лекции. Весь вывод в комментариях получен запуском кода.

Пример 1. def, return и None

return завершает функцию и передаёт значение в точку вызова; если return нет (или он без значения), результатом всегда будет None.

# 01_def_return.py
def greet(name):
    return f"Hello, {name}!"

def validate_data():
    pass   # заготовка: интерфейс понятен, реализации пока нет

print(greet("Alice"))       # Hello, Alice!
print(validate_data())       # None

Что происходит: pass — синтаксическая заглушка, которая ничего не делает, но позволяет оставить тело функции пустым без ошибки отступа. Функция без return всё равно завершается штатно и возвращает None — это не ошибка, а поведение по умолчанию.

Пример 2. Позиционные, именованные и default-аргументы

Позиционные аргументы сопоставляются по порядку, именованные — по имени параметра и могут идти в любом порядке.

# 02_positional_named.py
def describe(name, age, greeting="Hello"):
    return f"{greeting}, {name}! Age: {age}"

print(describe("Bob", 30))
# Hello, Bob! Age: 30

print(describe(age=25, name="Alice", greeting="Hi"))
# Hi, Alice! Age: 25

print(describe("Carol", greeting="Hey", age=40))
# Hey, Carol! Age: 40

Почему так: greeting использует значение по умолчанию, только если вызывающий код его не передал. Именованные аргументы можно перечислять в произвольном порядке — Python сопоставляет их по имени параметра, а не по позиции.

Пример 3. Порядок параметров: default — только после обязательных

Параметр со значением по умолчанию не может стоять раньше обязательного — это ошибка уже на этапе разбора кода.

# 03_default_order.py
try:
    exec("def bad(greeting='Hello', name): pass")
except SyntaxError as error:
    print("SyntaxError:", error)
# SyntaxError: non-default argument follows default argument (<string>, line 1)

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

Пример 4. *args — упаковка позиционных аргументов

*args собирает произвольное количество позиционных значений в один tuple.

# 04_args.py
def show_numbers(*args):
    print(args, type(args))

show_numbers(1, 2, 3)
# (1, 2, 3) <class 'tuple'>
show_numbers()
# () <class 'tuple'>

def sum_all(*args):
    return sum(args)

print(sum_all(1, 2, 3, 4))
# 10

Что происходит: внутри функции args — обычный кортеж, с ним можно работать как с любой последовательностью: суммировать, перебирать циклом, передавать дальше. Имя args — соглашение, а не требование языка; работает и любое другое имя после *.

Пример 5. **kwargs — упаковка именованных аргументов

**kwargs собирает произвольное количество именованных аргументов в dict.

# 05_kwargs.py
def show_info(**kwargs):
    print(kwargs, type(kwargs))

show_info(name="Alice", age=25, city="Paris")
# {'name': 'Alice', 'age': 25, 'city': 'Paris'} <class 'dict'>

Что происходит: ключи словаря — это имена аргументов, с которыми функцию вызвали, значения — переданные значения. Это удобно, когда заранее не известно, сколько именованных параметров понадобится вызывающему коду.

Пример 6. Полный порядок параметров в одной функции

Обычные параметры → *args → параметры со значением по умолчанию → **kwargs.

# 06_combined_order.py
def show_full_info(name, *details, age=25, **extra):
    return name, details, age, extra

print(show_full_info("Alice", "Developer", age=30, city="New York"))
# ('Alice', ('Developer',), 30, {'city': 'New York'})

print(show_full_info("Bob"))
# ('Bob', (), 25, {})

Почему так: после *args в определении функции все следующие параметры можно передать только по имени — поэтому age=25 здесь работает как именованный параметр по умолчанию, а не как второй позиционный.

Пример 7. Распаковка при вызове функции

Операторы * и ** в вызове делают обратное упаковке: разбирают коллекцию на отдельные аргументы.

# 07_unpacking_call.py
def add(a, b, c):
    return a + b + c

nums = (1, 2, 3)
print(add(*nums))
# 6

def show_person(name, age, city):
    print(name, age, city)

person = {"name": "Bob", "age": 30, "city": "London"}
show_person(**person)
# Bob 30 London

Что происходит: add(*nums) эквивалентно add(1, 2, 3), а show_person(**person) — show_person(name="Bob", age=30, city="London"). Ключи словаря должны точно совпадать с именами параметров функции.

Пример 8. Только позиционные (/) и только именованные (*)

Python 3.8+ позволяет явно ограничить, как можно передавать конкретный параметр.

# 08_positional_keyword_only.py
def example(a, b, /, c, *, d):
    return a, b, c, d

print(example(1, 2, 3, d=4))
# (1, 2, 3, 4)

try:
    example(a=1, b=2, c=3, d=4)   # a, b — только позиционные
except TypeError as error:
    print("TypeError:", error)
# TypeError: example() got some positional-only arguments passed as keyword arguments: 'a, b'

try:
    example(1, 2, 3, 4)            # d — только именованный
except TypeError as error:
    print("TypeError:", error)
# TypeError: example() takes 3 positional arguments but 4 were given

Почему так: всё до / можно передать только позиционно, всё после * — только по имени. Это защищает публичный интерфейс функции: имена параметров до / можно свободно менять внутри реализации, не ломая вызывающий код.

⚠️ Проверить по документации: синтаксис / и * выходит за рамки лекции — это дополнение курса. Сверьтесь с разделом Function definitions.

Пример 9. LEGB: порядок поиска имени

Python ищет имя переменной по очереди: сначала в локальной области, затем во внешней (объемлющей) функции, затем на уровне модуля, затем среди встроенных имён.

# 09_legb.py
z = "global z"

def outer():
    x = "enclosing x"
    def inner():
        y = "local y"
        print(y, x, z, len)   # local, enclosing, global, built-in
    inner()

outer()
# local y enclosing x global z <built-in function len>

Что происходит: y найдена в локальной области inner, x — во внешней функции outer (enclosing), z — на уровне модуля (global), len — среди встроенных функций (built-in). Поиск останавливается на первом совпадении, поэтому локальное имя всегда "перекрывает" одноимённое внешнее.

Пример 10. global — изменение глобальной переменной

Без объявления global присваивание внутри функции создаёт новую локальную переменную, а не меняет внешнюю.

# 10_global.py
counter = 0

def increment_counter():
    global counter
    counter += 1

print("до:", counter)      # до: 0
increment_counter()
increment_counter()
print("после:", counter)   # после: 2

def try_increment_without_global():
    counter = counter + 1   # counter уже считается локальной из-за присваивания ниже

try:
    try_increment_without_global()
except UnboundLocalError as error:
    print("UnboundLocalError:", error)
# UnboundLocalError: cannot access local variable 'counter' where it is not associated with a value

Почему так: если внутри функции есть присваивание имени (counter = ...), Python заранее считает это имя локальным для всей функции — даже до строки с присваиванием. Поэтому чтение counter в правой части выражения падает с UnboundLocalError: интерпретатор уже "знает", что counter локальная, но значение ей ещё не присвоено. global counter отменяет это правило для конкретного имени. На практике чаще лучше передавать данные параметром и возвращать через return, чем полагаться на global.

Пример 11. nonlocal — изменение переменной внешней функции

nonlocal решает ту же проблему, что и global, но для переменной из объемлющей (не глобальной) функции — классический сценарий счётчика-накопителя.

# 11_nonlocal.py
def make_accumulator():
    total = 0
    def add_value(value):
        nonlocal total
        total += value
        return total
    return add_value

accumulate = make_accumulator()
print(accumulate(10))   # 10
print(accumulate(5))    # 15
print(accumulate(1))    # 16

Что происходит: каждый вызов make_accumulator() создаёт свою собственную переменную total, а вложенная функция add_value хранит связь с ней благодаря nonlocal. Без nonlocal строка total += value создала бы новую локальную переменную внутри add_value и упала бы с UnboundLocalError, как в примере 10. Полный механизм такого "запоминания" — тема отдельного урока про замыкания.

Пример 12. Ловушка: изменяемое значение по умолчанию

Значение параметра по умолчанию вычисляется один раз — при определении функции, а не при каждом вызове. Для изменяемых объектов (списков, словарей) это создаёт скрытую "память" между вызовами.

# 12_mutable_default.py
def append_bad(value, items=[]):
    items.append(value)
    return items

print(append_bad(1))   # [1]
print(append_bad(2))   # [1, 2]  — список "запомнил" предыдущий вызов!

def append_good(value, items=None):
    if items is None:
        items = []
    items.append(value)
    return items

print(append_good(1))  # [1]
print(append_good(2))  # [2]

Почему так: список [] в def append_bad(value, items=[]) создаётся один раз при определении функции и переиспользуется на каждом вызове без явного аргумента. Стандартное решение — параметр по умолчанию None и создание нового списка внутри тела функции.

⚠️ Проверить по документации: эта ловушка не разбиралась в лекции, но входит в базовый набор частых ошибок Python — Function definitions, Default parameter values.

Что делать дальше

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