Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Функции и аргументы
functions

Функции и аргументы

Определение функций, *args, **kwargs, значения по умолчанию, walrus-оператор

Открыть лабораториюv0.6.2Запускается локально из публичного репозитория

Функции и аргументы в Python

  • Маршрут: Основы · тема 4 из 7
  • Навыки: J3, M1, M6, S9
  • До урока: basic_types, control_flow.
  • Результат: маленькие компонуемые API с явными сигнатурами и зависимостями.
  • Основа: параметры, области видимости и контракты возврата. Работа в эксплуатации: композиция, внедрение зависимостей и служебные сведения. Углубление: протоколы вызываемых объектов, ParamSpec и защита сигнатуры декоратором.
  • Подтверждение: лаборатория правил заказа и обоснование API-контракта.

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

#1. Базовое определение и синтаксис

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__

#2. Все виды параметров

#Позиционные и ключевые (по умолчанию)

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) # оба по имени

#Positional-only (PEP 570, Python 3.8+) — параметры до /

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)

#Keyword-only — параметры после *

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, port

#Полный порядок параметров

def full(pos_only, /, normal, *args, kw_only, **kwargs): ...

Порядок: positional-only → обычные → *args → keyword-only → **kwargs. Нарушение → SyntaxError.

ТипСинтаксисКак передавать
Positional-onlydef f(a, b, /)Только позиционно
Обычныйdef f(a, b)Позиционно или по имени
*argsdef f(*args)Сбор позиционных в кортеж
Keyword-onlydef f(*, a, b)Только по имени
**kwargsdef f(**kwargs)Сбор именованных в словарь

#3. Значения по умолчанию — ловушка с изменяемыми объектами

Значения по умолчанию вычисляются один раз при определении функции:

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

Иногда изменяемый default — это фича:

def memoize(f, _cache={}): """Простая мемоизация через shared dict.""" def wrapper(*args): if args not in _cache: _cache[args] = f(*args) return _cache[args] return wrapper

#4. Аннотации типов (PEP 484)

from 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+

#5. Область видимости: правило LEGB

Python ищет имя в следующем порядке:

  1. Local — текущая функция
  2. Enclosing — объемлющие функции (для замыканий)
  3. Global — уровень модуля
  4. Built-in — встроенные имена (len, print, ...)
x = 'global' def outer(): x = 'enclosing' def inner(): x = 'local' print(x) # 'local' inner() print(x) # 'enclosing' outer() print(x) # 'global'

#6. global и nonlocal

#global — изменение переменной на уровне модуля

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() # 2

nonlocal ищет переменную в ближайшей объемлющей функции (не в глобальном пространстве).

#7. Функции первого класса

В 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) # False

#Атрибуты функций

def 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) == 4

#Таблица диспетчеризации

Dispatch 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)

#8. Lambda-функции

square = lambda x: x ** 2 add = lambda a, b: a + b

Ограничения lambda:

  • Только одно выражение (не блок кода)
  • Нет аннотаций типов
  • __name__ всегда '<lambda>' — плохо для отладки
  • PEP 8 не рекомендует присваивать 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.

#9. functools.partial и functools.reduce

#functools.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.28

#10. Рекурсия

def factorial(n: int) -> int: """База рекурсии обязательна.""" if n <= 1: # base case return 1 return n * factorial(n - 1) # рекурсивный случай

Ограничения:

  • CPython ограничивает глубину стека: sys.getrecursionlimit() (обычно 1000)
  • При превышении — RecursionError: maximum recursion depth exceeded
  • Изменить лимит: sys.setrecursionlimit(5000) (осторожно — риск сегфолта)
  • CPython не оптимизирует хвостовую рекурсию → для глубокой рекурсии используйте цикл
# Рекурсия: элегантно, но 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 result

#Хвостовая рекурсия через итерацию (trampolining)

def 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 полезен как объяснение механизма, а не как рекомендация для вычисления факториала.

#11. Генераторные функции

Функция с 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) # StopIteration

#yield from — делегирование генератору

def chain(*iterables): for it in iterables: yield from it # делегирует все значения list(chain([1, 2], [3, 4], [5])) # [1, 2, 3, 4, 5]

Подробнее — в теме Итераторы и генераторы.

#12. Оператор walrus (:=) — Python 3.8+

# Без walrus — вычисляем дважды data = compute() if data: process(data) # С walrus — одно вычисление if data := compute(): process(data) # В цикле while chunk := file.read(8192): process(chunk)

Важно: Всегда заключайте в скобки во избежание неоднозначности.

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

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} # объединение словарей

#14. Интроспекция функций

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}")

#15. Высокоуровневые функции: map, filter, zip

#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')

#16. Функциональные паттерны

#Compose (композиция функций)

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'

#Currying

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__ # 'Приветствует.'

#17. Производительность функций

#Локальные переменные быстрее глобальных

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)

#Практика: от пробы к рабочему решению

#Шаг 1. Соберите минимальный пример

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

#Шаг 2. Проверьте поведение

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

#Шаг 3. Доведите решение до рабочего сценария

Практика с подсказками: соберите конвейер правил заказа. Внедряйте правила параметром, не создавайте их внутри функции, проверяйте тип результата каждого шага и добавляйте контекст к ожидаемой доменной ошибке.

#Шаг 4. Объясните внутренний механизм

Сохраните сигнатуру и служебные сведения функции после декорирования. Затем опишите тип декоратора через ParamSpec и сравните статический контракт с тем, что реально проверяется во время выполнения.

#Упражнения

  1. Напишите функцию merge_dicts(*dicts), которая объединяет произвольное число словарей.
  2. Реализуйте декоратор @retry(n=3) с functools.wraps, который повторяет вызов при исключении.
  3. Создайте dispatch table для вычислителя простых выражений (+, -, *, /).
  4. Объясните, почему lambda x: x + y захватывает y по ссылке и как это использовать с умом.
  5. Напишите make_validator(schema: dict), возвращающую функцию-валидатор для словарей.
  6. Реализуйте @singledispatch-based serialize(obj), который обрабатывает int, str, list, dict и пользовательские классы через __dict__.
  7. Напишите trampoline-версию хвосторекурсивной суммы чисел от 1 до n и проверьте её на n=10_000. Объясните, почему функция Аккермана не является простой хвостовой рекурсией и не подходит для этого упражнения.

#Самопроверка

  • merge_dicts() без аргументов возвращает новый пустой словарь; при конфликте побеждает последний словарь, а входы не меняются.
  • retry сохраняет __name__ и сигнатуру, делает не более n попыток и не повторяет исключения вне явно заданного набора.
  • Dispatch table проверяется на неизвестном операторе и делении на ноль.
  • Для late binding создайте функции в цикле: сначала воспроизведите одинаковый результат, затем исправьте его параметром по умолчанию.
  • Trampoline должен пройти 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

Далее: Строки и форматирование