Примеры идут от основ (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 и создание нового списка внутри тела функции.
Что делать дальше
Разберите файлы репозитория — там те же идеи в исходном виде, затем переходите к заданиям. Если что-то из примеров не запустилось — сначала загляните в типичные ошибки.