Определение функций, *args, **kwargs, значения по умолчанию, walrus-оператор
- Маршрут: Основы · тема 4 из 7
- Навыки:
J3,M1,M6,S9- До урока:
basic_types,control_flow.- Результат: маленькие компонуемые API с явными сигнатурами и зависимостями.
- Основа: параметры, области видимости и контракты возврата. Работа в эксплуатации: композиция, внедрение зависимостей и служебные сведения. Углубление: протоколы вызываемых объектов,
ParamSpecи защита сигнатуры декоратором.- Подтверждение: лаборатория правил заказа и обоснование API-контракта.
Функция в Python — обычный объект: её можно присвоить переменной, передать в другую функцию, вернуть из функции или сохранить в словаре. На этом свойстве строятся стратегии, таблицы диспетчеризации и декораторы.
def greet(name: str, greeting: str = "Hello") -> str:
"""Приветствует пользователя.
Args:
name: Имя пользователя.
greeting: Текст приветствия.
Returns:
Строка приветствия.
"""
return f"{greeting}, {name}!"-> str — аннотация возвращаемого типа (не обязательна, но рекомендуется)name: str — аннотация параметраgreeting: str = "Hello" — аргумент со значением по умолчаниюdef — docstring, хранится в __doc__def f(a, b, c=10):
...
f(1, 2) # a=1, b=2, c=10
f(1, b=2) # можно передать b по имени
f(a=1, b=2) # оба по имени/def pow(base, exp, /, mod=None):
...
pow(2, 10) # OK
pow(base=2, exp=10) # TypeError: positional-only argumentПараметры до / нельзя передавать по имени. Это позволяет переименовывать параметры без нарушения обратной совместимости.
*args — произвольное число позиционныхdef total(*args):
return sum(args)
total(1, 2, 3) # args = (1, 2, 3)*def connect(host, port, *, timeout=30, ssl=False):
...
connect('localhost', 80, timeout=10) # OK
connect('localhost', 80, 10) # TypeErrorПараметры после * можно передавать только по имени. * без имени не собирает аргументы — только разделяет.
**kwargs — произвольные именованные аргументыdef configure(**kwargs):
host = kwargs.get('host', 'localhost')
port = kwargs.get('port', 8080)
return host, portdef full(pos_only, /, normal, *args, kw_only, **kwargs):
...Порядок: positional-only → обычные → *args → keyword-only → **kwargs. Нарушение → SyntaxError.
| Тип | Синтаксис | Как передавать |
|---|---|---|
| Positional-only | def f(a, b, /) | Только позиционно |
| Обычный | def f(a, b) | Позиционно или по имени |
*args | def f(*args) | Сбор позиционных в кортеж |
| Keyword-only | def f(*, a, b) | Только по имени |
**kwargs | def f(**kwargs) | Сбор именованных в словарь |
Значения по умолчанию вычисляются один раз при определении функции:
def bad_append(x, lst=[]): # Проблема: lst создаётся один раз!
lst.append(x)
return lst
bad_append(1) # [1]
bad_append(2) # [1, 2] — накапливается!
def good_append(x, lst=None): # Вариант: правильно
if lst is None:
lst = []
lst.append(x)
return lstЗначения по умолчанию хранятся в func.__defaults__ (кортеж) и func.__kwdefaults__ (словарь для keyword-only).
def f(x, y=10, z=20): ...
f.__defaults__ # (10, 20)Иногда изменяемый default — это фича:
def memoize(f, _cache={}):
"""Простая мемоизация через shared dict."""
def wrapper(*args):
if args not in _cache:
_cache[args] = f(*args)
return _cache[args]
return wrapperfrom typing import Optional, Union, List
def process(items: List[int], limit: Optional[int] = None) -> Union[int, None]:
...Аннотации хранятся в __annotations__:
def greet(name: str, age: int = 0) -> str: ...
greet.__annotations__
# {'name': <class 'str'>, 'age': <class 'int'>, 'return': <class 'str'>}Важно: аннотации не влияют на выполнение. Python не проверяет типы в рантайме — это задача статических анализаторов (mypy, pyright).
С Python 3.12 упрощённый синтаксис generic-типов:
def first[T](lst: list[T]) -> T: ... # Python 3.12+Python ищет имя в следующем порядке:
len, print, ...)x = 'global'
def outer():
x = 'enclosing'
def inner():
x = 'local'
print(x) # 'local'
inner()
print(x) # 'enclosing'
outer()
print(x) # 'global'global и nonlocalglobal — изменение переменной на уровне модуляcounter = 0
def increment():
global counter # без этого создалась бы локальная переменная
counter += 1Читать глобальную переменную можно без global. Присвоить — нельзя без объявления.
nonlocal — изменение переменной в объемлющей функцииdef make_counter():
count = 0
def increment():
nonlocal count # без этого — UnboundLocalError
count += 1
return count
return increment
counter = make_counter()
counter() # 1
counter() # 2nonlocal ищет переменную в ближайшей объемлющей функции (не в глобальном пространстве).
В Python функции — полноценные объекты:
# Присвоение функции переменной
f = print
f("Hello") # Hello
# Хранение в структурах данных
import operator
ops = {
'+': operator.add,
'-': operator.sub,
'*': operator.mul,
}
result = ops['+'](3, 4) # 7
# Передача как аргумент
numbers = [3, 1, 4, 1, 5]
sorted_numbers = sorted(numbers, key=lambda x: -x)
# Возврат из функции
def get_validator(min_val, max_val):
def validate(x):
return min_val <= x <= max_val
return validate
is_valid_age = get_validator(0, 120)
is_valid_age(25) # True
is_valid_age(200) # Falsedef greet(name):
"""Приветствие."""
return f"Hello, {name}"
greet.__name__ # 'greet'
greet.__doc__ # 'Приветствие.'
greet.__module__ # '__main__'
greet.__code__ # code object
greet.__globals__ # глобальные переменные модуля
greet.__annotations__ # {}
# Можно добавить произвольные атрибуты:
greet.version = "1.0"
greet.calls = 0__call__Функция — не единственный вызываемый объект. Если класс определяет
__call__, его экземпляр можно вызывать с круглыми скобками. Это удобно,
когда обработчику нужно хранить состояние между вызовами.
class Counter:
def __init__(self) -> None:
self.value = 0
def __call__(self, step: int = 1) -> int:
self.value += step
return self.value
counter = Counter()
assert callable(counter)
assert counter() == 1
assert counter(3) == 4Dispatch table заменяет длинную цепочку if/elif, когда выбор поведения можно
свести к ключу. Значениями словаря служат функции, а не результаты их вызова.
Переданная функция также может играть роль Strategy: клиент зависит от
контракта callable и выбирает алгоритм без отдельной иерархии классов.
import operator
OPERATIONS = {
"+": operator.add,
"-": operator.sub,
"*": operator.mul,
"/": operator.truediv,
}
def calculate(left: float, operation: str, right: float) -> float:
try:
handler = OPERATIONS[operation]
except KeyError as error:
raise ValueError(f"Неизвестная операция: {operation}") from error
return handler(left, right)square = lambda x: x ** 2
add = lambda a, b: a + bОграничения lambda:
__name__ всегда '<lambda>' — плохо для отладкиdefЛовушка с замыканиями в цикле:
# Проблема: все функции захватывают одно и то же i
fns = [lambda x: x * i for i in range(3)]
[f(2) for f in fns] # [4, 4, 4] — все используют i=2
# Решение: захватить значение через параметр по умолчанию
fns = [lambda x, i=i: x * i for i in range(3)]
[f(2) for f in fns] # [0, 2, 4] — правильноИспользуйте lambda для sorted(key=...), map(), filter() и коротких
однострочных callback-функций. Подробнее о замыканиях и декораторах на основе
лямбд — в closures_decorators.
functools.partial и functools.reducefunctools.partial — частичное применение аргументовfrom functools import partial
def power(base, exp):
return base ** exp
square = partial(power, exp=2)
cube = partial(power, exp=3)
square(4) # 16
cube(3) # 27
# partial хранит исходный callable и зафиксированные аргументы:
square.func # <function power>
square.args # ()
square.keywords # {'exp': 2}
# Но __name__ и __doc__ исходной функции автоматически не создаются.
# При необходимости: functools.update_wrapper(square, power)partial создаёт объект с атрибутами func, args, keywords. Он удобен, когда нужно зафиксировать несколько параметров; это не означает полного копирования metadata исходной функции.
functools.reduce — свёртка итерируемого объектаfrom functools import reduce
# Сумма (лучше использовать sum())
total = reduce(lambda acc, x: acc + x, [1, 2, 3, 4], 0) # 10
# Произведение (нет встроенной функции)
product = reduce(lambda acc, x: acc * x, [1, 2, 3, 4, 5], 1) # 120
# Построение структур:
data = [('a', 1), ('b', 2), ('c', 3)]
result = reduce(lambda d, kv: {**d, kv[0]: kv[1]}, data, {})
# {'a': 1, 'b': 2, 'c': 3}reduce(f, iterable, initial) вычисляет f(f(f(initial, a), b), c).
functools.lru_cache — мемоизацияfrom functools import lru_cache
@lru_cache(maxsize=128)
def fib(n: int) -> int:
if n < 2:
return n
return fib(n-1) + fib(n-2)
fib(50) # мгновенно
fib.cache_info() # CacheInfo(hits=48, misses=51, maxsize=128, currsize=51)
fib.cache_clear() # очистить кэшfunctools.singledispatch — перегрузка функций по типуfrom functools import singledispatch
@singledispatch
def process(data):
raise NotImplementedError(f"Unsupported type: {type(data)}")
@process.register(str)
def _(data):
return data.upper()
@process.register(list)
def _(data):
return [process(item) for item in data]
@process.register(int)
@process.register(float)
def _(data):
return data * 2
process("hello") # 'HELLO'
process([1, 2, 3]) # [2, 4, 6]
process(3.14) # 6.28def factorial(n: int) -> int:
"""База рекурсии обязательна."""
if n <= 1: # base case
return 1
return n * factorial(n - 1) # рекурсивный случайОграничения:
sys.getrecursionlimit() (обычно 1000)RecursionError: maximum recursion depth exceededsys.setrecursionlimit(5000) (осторожно — риск сегфолта)# Рекурсия: элегантно, но O(n) стека
def factorial_recursive(n): return 1 if n <= 1 else n * factorial_recursive(n - 1)
# Итерация: безопаснее для больших n
def factorial_iterative(n):
result = 1
for i in range(2, n + 1):
result *= i
return resultdef trampoline(f):
"""Превращает хвосто-рекурсивную функцию в итеративную."""
def wrapper(*args, **kwargs):
result = f(*args, **kwargs)
while callable(result):
result = result()
return result
return wrapper
def factorial_step(n, acc=1):
if n <= 1:
return acc
# В thunk вызывается исходная step-функция, а не wrapper декоратора.
return lambda: factorial_step(n - 1, n * acc)
factorial_tr = trampoline(factorial_step)
factorial_tr(10000) # работает без RecursionErrorЕсли украсить factorial_step через @trampoline и внутри lambda вызвать уже
украшенное имя, каждый шаг создаст вложенный wrapper и рекурсия вернётся.
На практике простой цикл выше понятнее; trampoline полезен как объяснение
механизма, а не как рекомендация для вычисления факториала.
Функция с yield не выполняется при вызове — возвращается объект-генератор:
def count_up(n):
for i in range(n):
yield i # приостанавливается здесь
gen = count_up(3) # код не запущен
next(gen) # 0
next(gen) # 1
next(gen) # 2
next(gen) # StopIterationyield from — делегирование генераторуdef chain(*iterables):
for it in iterables:
yield from it # делегирует все значения
list(chain([1, 2], [3, 4], [5])) # [1, 2, 3, 4, 5]Подробнее — в теме Итераторы и генераторы.
:=) — Python 3.8+# Без walrus — вычисляем дважды
data = compute()
if data:
process(data)
# С walrus — одно вычисление
if data := compute():
process(data)
# В цикле
while chunk := file.read(8192):
process(chunk)Важно: Всегда заключайте в скобки во избежание неоднозначности.
def greet(name, age, role): ...
args = ['Alice', 30]
kwargs = {'role': 'admin'}
greet(*args, **kwargs) # грамотная передача
greet(*args, role='admin') # комбинирование
# Python 3.5+: несколько распаковок
def f(a, b, c, d, e): ...
f(*[1, 2], *[3, 4, 5]) # OK
combined = {**dict1, **dict2} # объединение словарейimport inspect
def greet(name: str, age: int = 0) -> str:
"""Приветствие."""
...
sig = inspect.signature(greet)
for name, param in sig.parameters.items():
print(name, param.kind, param.default, param.annotation)
# Виды параметров:
# POSITIONAL_ONLY, POSITIONAL_OR_KEYWORD, VAR_POSITIONAL (*args)
# KEYWORD_ONLY, VAR_KEYWORD (**kwargs)
# Проверка вызываемости
callable(greet) # True
callable(42) # False
callable(print) # True
# Получить исходный код:
print(inspect.getsource(greet))
# Получить значения по умолчанию через signature:
for p in sig.parameters.values():
if p.default is not inspect.Parameter.empty:
print(f"{p.name} default = {p.default}")map(func, iterable) — применить функцию к каждому элементуnumbers = [1, 2, 3, 4, 5]
squares = list(map(lambda x: x**2, numbers)) # [1, 4, 9, 16, 25]
# Несколько итерируемых:
pairs = list(map(lambda a, b: a + b, [1, 2, 3], [10, 20, 30])) # [11, 22, 33]
# Современный Python: list comprehension читабельнее
squares = [x**2 for x in numbers]filter(func, iterable) — отфильтровать по предикатуevens = list(filter(lambda x: x % 2 == 0, range(10))) # [0, 2, 4, 6, 8]
# filter(None, iterable) — убрать falsy-значения
clean = list(filter(None, [0, '', None, 1, 'hello', False])) # [1, 'hello']zip(*iterables) — попарное объединениеnames = ['Alice', 'Bob', 'Carol']
ages = [30, 25, 35]
roles = ['admin', 'user', 'user']
# zip останавливается по кратчайшей последовательности
for name, age, role in zip(names, ages, roles):
print(f"{name}: {age}, {role}")
# Для заполнения до конца используйте zip_longest:
from itertools import zip_longest
for a, b in zip_longest([1, 2, 3], [10, 20], fillvalue=0):
print(a, b) # 1 10 / 2 20 / 3 0
# Unzip: транспонирование списка кортежей
pairs = [(1, 'a'), (2, 'b'), (3, 'c')]
numbers, letters = zip(*pairs) # (1, 2, 3) и ('a', 'b', 'c')from functools import reduce
def compose(*funcs):
"""Применяет функции справа налево: compose(f, g, h)(x) = f(g(h(x)))"""
def composed(x):
return reduce(lambda v, f: f(v), reversed(funcs), x)
return composed
def pipe(*funcs):
"""Применяет функции слева направо: pipe(f, g, h)(x) = h(g(f(x)))"""
def piped(x):
return reduce(lambda v, f: f(v), funcs, x)
return piped
process = pipe(
str.strip,
str.lower,
lambda s: s.replace(' ', '_'),
)
process(" Hello World ") # 'hello_world'def curry(f):
"""Превращает f(a, b, c) в f(a)(b)(c)."""
import inspect
n = len(inspect.signature(f).parameters)
def curried(*args):
if len(args) >= n:
return f(*args[:n])
return lambda *more: curried(*args, *more)
return curried
@curry
def add(a, b, c):
return a + b + c
add(1)(2)(3) # 6
add(1, 2)(3) # 6
add(1)(2, 3) # 6@wraps — сохранение метаданныхfrom functools import wraps
def log_calls(func):
@wraps(func) # копирует __name__, __doc__, __annotations__, __module__
def wrapper(*args, **kwargs):
print(f"Вызов {func.__name__}")
return func(*args, **kwargs)
wrapper.__wrapped__ = func # @wraps ставит автоматически
return wrapper
@log_calls
def greet(name: str) -> str:
"""Приветствует."""
return f"Hello, {name}"
greet.__name__ # 'greet' (а не 'wrapper')
greet.__doc__ # 'Приветствует.'import math
# Медленнее: каждый раз ищет math в globals
def slow_sin(x):
return math.sin(x)
# Быстрее: локальная ссылка разрешается за O(1)
def fast_sin(x, _sin=math.sin):
return _sin(x)
# Ещё паттерн: в начале функции
def process(data):
append = [].append # локальная ссылка
for item in data:
append(item) # быстрее чем list.append через атрибут# Медленно: создаём функцию в каждой итерации
for i in range(1000):
sorted(data, key=lambda x: x[i])
# Быстро: создаём функцию один раз
from operator import itemgetter
key = itemgetter(0)
for _ in range(1000):
sorted(data, key=key)Сигнатура — часть публичного контракта. Результат основы — маленькая функция с однозначными позиционными и именованными параметрами, проверкой границ и стабильным типом результата.
import inspect
def final_price(amount: float, /, *, discount: float = 0.0) -> float:
if type(amount) not in (int, float) or not 0 <= discount <= 1:
raise ValueError("недопустимые amount или discount")
return amount * (1 - discount)
parameters = inspect.signature(final_price).parameters
assert parameters["amount"].kind is inspect.Parameter.POSITIONAL_ONLY
assert parameters["discount"].kind is inspect.Parameter.KEYWORD_ONLY
assert final_price(100, discount=0.2) == 80Практика с подсказками: соберите конвейер правил заказа. Внедряйте правила параметром, не создавайте их внутри функции, проверяйте тип результата каждого шага и добавляйте контекст к ожидаемой доменной ошибке.
Сохраните сигнатуру и служебные сведения функции после декорирования. Затем
опишите тип декоратора через ParamSpec и сравните статический контракт с тем,
что реально проверяется во время выполнения.
merge_dicts(*dicts), которая объединяет произвольное число словарей.@retry(n=3) с functools.wraps, который повторяет вызов при исключении.+, -, *, /).lambda x: x + y захватывает y по ссылке и как это использовать с умом.make_validator(schema: dict), возвращающую функцию-валидатор для словарей.@singledispatch-based serialize(obj), который обрабатывает int, str, list, dict и пользовательские классы через __dict__.1 до n и
проверьте её на n=10_000. Объясните, почему функция Аккермана не является
простой хвостовой рекурсией и не подходит для этого упражнения.merge_dicts() без аргументов возвращает новый пустой словарь; при
конфликте побеждает последний словарь, а входы не меняются.retry сохраняет __name__ и сигнатуру, делает не более n попыток и не
повторяет исключения вне явно заданного набора.10_000 шагов без изменения
sys.setrecursionlimit; обычный цикл всё равно остаётся предпочтительнее.Лаборатория
«Движок правил заказа»
доступна в публичном GitLab. Она проверяет контракты сигнатур, позднее
связывание, чистый конвейер, внедрение зависимостей, метаданные декоратора и
типизацию через ParamSpec.
Склонируйте репозиторий и запустите тесты из его корня:
git clone --branch v0.6.2 --depth 1 https://gitlab.potapov.me/courses/python-labs.git
cd python-labs
uv sync --group test
uv run --group test pytest functions/testsДалее: Строки и форматирование