Определение функций, *args, **kwargs, значения по умолчанию, walrus-оператор
Функция — это объект первого класса: её можно присвоить переменной, передать в другую функцию, вернуть из функции, хранить в словаре.
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 = 0square = 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'ов.
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 сохраняет метаданные:
square.func # <function power>
square.args # ()
square.keywords # {'exp': 2}partial создаёт объект с атрибутами func, args, keywords. Удобнее lambda, когда нужно зафиксировать несколько именованных параметров.
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
@trampoline
def factorial_tr(n, acc=1):
if n <= 1:
return acc
return lambda: factorial_tr(n - 1, n * acc)
factorial_tr(10000) # работает без RecursionErrorФункция с 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)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__.trampoline-версию функции Аккермана, избегая RecursionError.Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.