20. Параметры и аргументы функций
В главе 19 мы научились определять и вызывать функции с базовыми параметрами. Теперь мы подробно рассмотрим гибкую систему параметров и аргументов в Python. Понимание этих механизмов позволяет писать функции, которые одновременно мощные и удобные в использовании.
20.1) Позиционные и ключевые аргументы
Когда вы вызываете функцию, вы можете передавать аргументы двумя фундаментальными способами: по позиции или по имени (keyword).
20.1.1) Позиционные аргументы
Позиционные аргументы сопоставляются с параметрами на основе их порядка. Первый аргумент передаётся первому параметру, второй — второму и так далее.
def calculate_discount(price, discount_percent):
"""Calculate the final price after applying a discount."""
discount_amount = price * (discount_percent / 100)
final_price = price - discount_amount
return final_price
# Передача аргументов по позиции
result = calculate_discount(100, 20)
print(result)Output:
80.0В этом примере 100 присваивается price, а 20 — discount_percent исключительно на основе их позиций в вызове функции.
Порядок критически важен при использовании позиционных аргументов:
# Пример: мы хотим посчитать цену товара за $100 со скидкой 20%
# Правильный порядок: сначала price, затем discount
print(calculate_discount(100, 20))
# Неправильный порядок: сначала discount, затем price
print(calculate_discount(20, 100))Output:
80.0
-16.0Когда вы меняете аргументы местами, Python не знает, что вы ошиблись — он просто присваивает их по порядку. Это даёт математически корректный, но логически неверный результат (отрицательную цену!).
20.1.2) Ключевые аргументы
Ключевые аргументы явно указывают, какой параметр получает какое значение, используя имя параметра, затем знак равенства и значение. Это делает код более читаемым и защищает от ошибок порядка.
def create_user_profile(username, email, age):
"""Create a user profile with the given information."""
profile = f"User: {username}\nEmail: {email}\nAge: {age}"
return profile
# Использование ключевых аргументов
profile = create_user_profile(username="alice_smith", email="alice@example.com", age=28)
print(profile)Output:
User: alice_smith
Email: alice@example.com
Age: 28При ключевых аргументах порядок не важен:
# Тот же результат, другой порядок
profile1 = create_user_profile(username="bob", email="bob@example.com", age=35)
profile2 = create_user_profile(age=35, username="bob", email="bob@example.com")
profile3 = create_user_profile(email="bob@example.com", age=35, username="bob")
# Все три дают идентичные результаты
print(profile1 == profile2 == profile3)Output:
TrueЭта гибкость особенно ценна, когда у функции много параметров: становится легко увидеть, какое значение соответствует какому параметру.
20.1.3) Смешивание позиционных и ключевых аргументов
Вы можете комбинировать оба стиля в одном вызове функции, но есть важное правило: позиционные аргументы должны идти перед ключевыми аргументами.
def format_address(street, city, state, zip_code):
"""Format a mailing address."""
return f"{street}\n{city}, {state} {zip_code}"
# Допустимо: сначала позиционные аргументы, затем ключевые аргументы
address = format_address("123 Main St", "Springfield", state="IL", zip_code="62701")
print(address)Output:
123 Main St
Springfield, IL 62701Здесь "123 Main St" и "Springfield" — позиционные (присваиваются street и city), а state и zip_code задаются по имени.
Попытка поставить позиционные аргументы после ключевых приводит к ошибке:
# Недопустимо: позиционный аргумент после ключевого аргумента
# address = format_address(street="123 Main St", "Springfield", state="IL", zip_code="62701")
# SyntaxError: positional argument follows keyword argumentPython принудительно применяет это правило, потому что как только вы начинаете использовать ключевые аргументы, становится неоднозначно, какой позиционный параметр должен заполнить следующий безымянный аргумент.
20.1.4) Когда использовать каждый стиль
Используйте позиционные аргументы, когда:
- У функции мало параметров (обычно 1–3)
- Порядок параметров очевиден и интуитивен
- Функция часто используется, и порядок хорошо известен
# Очевидно и лаконично
print(len("hello"))
result = max(10, 20, 5)Используйте ключевые аргументы, когда:
- У функции много параметров
- Значения параметров не очевидны сразу
- Вы хотите пропускать некоторые параметры, у которых есть значения по умолчанию (об этом далее)
- Вы хотите, чтобы код сам себя документировал
# Понятно и явно
user = create_user_profile(username="charlie", email="charlie@example.com", age=42)20.2) Значения параметров по умолчанию
Функции могут задавать значения по умолчанию для параметров. Когда вызывающий код не передаёт аргумент для параметра со значением по умолчанию, Python использует значение по умолчанию.
20.2.1) Определение параметров со значениями по умолчанию
Значения по умолчанию задаются в определении функции с помощью оператора присваивания:
def greet_user(name, greeting="Hello"):
"""Greet a user with a customizable greeting."""
return f"{greeting}, {name}!"
# Использование приветствия по умолчанию
print(greet_user("Alice"))
# Передача собственного приветствия
print(greet_user("Bob", "Good morning"))
print(greet_user("Carol", greeting="Hi"))Output:
Hello, Alice!
Good morning, Bob!
Hi, Carol!Параметр greeting имеет значение по умолчанию "Hello". Когда вы вызываете greet_user("Alice"), Python использует это значение по умолчанию. Когда вы передаёте второй аргумент, он переопределяет значение по умолчанию.
20.2.2) Параметры со значениями по умолчанию должны идти после обязательных параметров
Python требует, чтобы параметры со значениями по умолчанию располагались после всех параметров без значений по умолчанию. Это правило предотвращает неоднозначность в том, какие аргументы соответствуют каким параметрам.
# Правильно: сначала обязательные параметры, затем значения по умолчанию
def create_product(name, price, category="General", in_stock=True):
"""Create a product record."""
return {
"name": name,
"price": price,
"category": category,
"in_stock": in_stock
}
product = create_product("Laptop", 999.99)
print(product)Output:
{'name': 'Laptop', 'price': 999.99, 'category': 'General', 'in_stock': True}Попытка поставить обязательный параметр после параметра со значением по умолчанию вызывает синтаксическую ошибку:
# Invalid: required parameter after default parameter
# def invalid_function(name="Unknown", age):
# return f"{name} is {age} years old"
# SyntaxError: non-default argument follows default argumentЭто имеет смысл: если у name есть значение по умолчанию, а у age — нет, как Python должен понять, означает ли invalid_function(25) name=25 при отсутствии age, или age=25 при использовании значения name по умолчанию? Правило устраняет эту неоднозначность.
20.2.3) Практические применения параметров по умолчанию
Параметры по умолчанию отлично подходят для функций, где некоторые аргументы редко меняются:
def calculate_shipping(weight, distance, express=False):
"""Calculate shipping cost based on weight and distance."""
base_rate = 0.50 * weight + 0.10 * distance
if express:
base_rate *= 2 # Стоимость экспресс-доставки в два раза выше
return round(base_rate, 2)
# Большинство отправлений — стандартные
standard_cost = calculate_shipping(5, 100)
print(f"Standard: ${standard_cost}")
# Иногда кому-то нужна экспресс-доставка
express_cost = calculate_shipping(5, 100, express=True)
print(f"Express: ${express_cost}")Output:
Standard: $12.5
Express: $25.0Такой дизайн делает распространённый случай (стандартная доставка) простым для вызова, но при этом поддерживает и менее распространённый случай (экспресс-доставка), когда это нужно.
20.2.4) Несколько значений по умолчанию и выборочное переопределение
Когда у функции несколько параметров со значениями по умолчанию, вы можете переопределить любую комбинацию из них с помощью ключевых аргументов:
def format_currency(amount, currency="USD", show_symbol=True, decimal_places=2):
"""Format a number as currency."""
symbols = {"USD": "$", "EUR": "€", "GBP": "£", "JPY": "¥"}
formatted = f"{amount:.{decimal_places}f}"
if show_symbol and currency in symbols:
formatted = f"{symbols[currency]}{formatted}"
return formatted
# Использование всех значений по умолчанию
print(format_currency(42.5))
# Переопределяем только currency
print(format_currency(42.5, currency="EUR"))
# Переопределяем несколько значений по умолчанию
print(format_currency(42.5, currency="JPY", decimal_places=0))Output:
$42.50
€42.50
¥42Эта гибкость позволяет вызывающему коду настраивать ровно то, что нужно, сохраняя вызов функции лаконичным.
20.3) Списки аргументов переменной длины с *args
Иногда вы хотите, чтобы функция принимала любое количество аргументов, не зная заранее, сколько их будет. Python предоставляет для этого *args.
20.3.1) Понимание *args
Синтаксис *args в списке параметров собирает все лишние позиционные аргументы в кортеж. Имя args — это соглашение (сокращение от “arguments”), но после звёздочки можно использовать любое допустимое имя параметра.
def calculate_total(*numbers):
"""Calculate the sum of any number of values."""
total = 0
for num in numbers:
total += num
return total
# Работает с любым количеством аргументов
print(calculate_total(10))
print(calculate_total(10, 20))
print(calculate_total(10, 20, 30, 40))
print(calculate_total())Output:
10
30
100
0Внутри функции numbers — это кортеж, содержащий все позиционные аргументы, переданные в функцию. Когда аргументы не переданы, это пустой кортеж.
20.3.2) Сочетание обычных параметров с *args
Перед *args могут быть обычные параметры. Обычные параметры «поглощают» первые несколько аргументов, а *args собирает остальные:
def create_team(team_name, *members):
"""Create a team with a name and any number of members."""
member_list = ", ".join(members)
return f"Team {team_name}: {member_list}"
# Первый аргумент идёт в team_name, остальные — в members
print(create_team("Alpha", "Alice", "Bob"))
print(create_team("Beta", "Carol"))
print(create_team("Gamma", "Dave", "Eve", "Frank", "Grace"))Output:
Team Alpha: Alice, Bob
Team Beta: Carol
Team Gamma: Dave, Eve, Frank, GraceПервый аргумент ("Alpha", "Beta" или "Gamma") присваивается team_name, а все оставшиеся аргументы собираются в кортеж members.
20.4) Параметры только по ключу и **kwargs
Python предоставляет два дополнительных механизма для работы с аргументами: параметры только по ключу (keyword-only) и **kwargs для сбора произвольных ключевых аргументов.
20.4.1) Параметры только по ключу
Параметры только по ключу должны задаваться ключевыми аргументами — их нельзя передать позиционно. Вы создаёте их, размещая после * или после *args в списке параметров.
def create_account(username, *, email, age):
"""Create an account. Email and age must be specified by name."""
return {
"username": username,
"email": email,
"age": age
}
# Правильно: email и age указаны по ключу
account = create_account("alice", email="alice@example.com", age=28)
print(account)
# Invalid: trying to pass email and age positionally
# account = create_account("bob", "bob@example.com", 30)
# TypeError: create_account() takes 1 positional argument but 3 were givenOutput:
{'username': 'alice', 'email': 'alice@example.com', 'age': 28}* в списке параметров работает как разделитель. Всё, что находится после него, должно передаваться как ключевой аргумент. Это полезно, когда вы хотите заставить вызывающий код явно указывать некоторые параметры, делая код более читаемым и менее подверженным ошибкам.
Также можно комбинировать обычные параметры, *args и параметры только по ключу:
def log_event(event_type, *details, severity="INFO", timestamp=None):
"""Логировать событие с необязательными подробностями и метаданными."""
# Мы подробно изучим модуль datetime в главе 39,
# но пока просто знайте, что эти строки получают текущее время
# и форматируют его как строку временной метки
from datetime import datetime
if timestamp is None:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
details_str = " | ".join(details)
return f"[{timestamp}] {severity}: {event_type} - {details_str}"
# event_type — позиционный, details собираются через *details,
# severity и timestamp — только по ключу
print(log_event("Login", "User: alice", "IP: 192.168.1.1"))
print(log_event("Error", "Database connection failed", severity="ERROR"))Output (timestamp will vary based on when you run the code):
[2025-12-18 19:29:16] INFO: Login - User: alice | IP: 192.168.1.1
[2025-12-18 19:29:16] ERROR: Error - Database connection failed20.4.2) Понимание **kwargs
Синтаксис **kwargs собирает все лишние ключевые аргументы в словарь. Как и args, имя kwargs — условность (сокращение от “keyword arguments”), но после двойной звёздочки можно использовать любое допустимое имя.
def create_product(**attributes):
"""Create a product with any number of attributes."""
product = {}
for key, value in attributes.items():
product[key] = value
return product
# Передавайте любые ключевые аргументы
laptop = create_product(name="Laptop", price=999.99, brand="TechCorp", in_stock=True)
print(laptop)
phone = create_product(name="Phone", price=699.99, color="Black")
print(phone)Output:
{'name': 'Laptop', 'price': 999.99, 'brand': 'TechCorp', 'in_stock': True}
{'name': 'Phone', 'price': 699.99, 'color': 'Black'}Внутри функции attributes — это словарь, где ключи — имена параметров, а значения — переданные аргументы.
20.4.3) Сочетание обычных параметров, *args и **kwargs
Вы можете использовать все эти механизмы вместе, но они должны располагаться в определённом порядке:
- Обычные позиционные параметры
*args(если присутствует)- Параметры только по ключу (если присутствуют)
**kwargs(если присутствует)
def complex_function(required, *args, keyword_only, **kwargs):
"""Demonstrate all parameter types together."""
print(f"Required: {required}")
print(f"Args: {args}")
print(f"Keyword-only: {keyword_only}")
print(f"Kwargs: {kwargs}")
complex_function(
"value1", # required
"value2", "value3", # args
keyword_only="kw", # keyword_only
extra1="e1", # kwargs
extra2="e2" # kwargs
)Output:
Required: value1
Args: ('value2', 'value3')
Keyword-only: kw
Kwargs: {'extra1': 'e1', 'extra2': 'e2'}Эта гибкость очень мощная, но её следует использовать осмотрительно. Большинству функций не нужны все эти механизмы.
20.4.4) Практический случай: функции конфигурации
Распространённое применение **kwargs — создание функций, которые принимают параметры конфигурации:
def connect_to_database(host, port, **options):
"""Connect to a database with flexible configuration options."""
connection_string = f"Connecting to {host}:{port}"
# Обработка любых дополнительных параметров из словаря options
if options.get("ssl"):
connection_string += " with SSL"
if options.get("timeout"):
connection_string += f" (timeout: {options['timeout']}s)"
if options.get("pool_size"):
connection_string += f" (pool size: {options['pool_size']})"
return connection_string
# Базовое подключение
print(connect_to_database("localhost", 5432))
# С SSL
print(connect_to_database("db.example.com", 5432, ssl=True))
# С несколькими options
print(connect_to_database("db.example.com", 5432, ssl=True, timeout=30, pool_size=10))Output:
Connecting to localhost:5432
Connecting to db.example.com:5432 with SSL
Connecting to db.example.com:5432 with SSL (timeout: 30s) (pool size: 10)Этот паттерн позволяет функции принимать любое число необязательных параметров конфигурации, не определяя их все явно в списке параметров.
20.5) Распаковка аргументов при вызове функций
Так же как *args и **kwargs собирают аргументы при определении функций, вы можете использовать * и **, чтобы распаковывать коллекции при вызове функций.
20.5.1) Распаковка последовательностей с *
Оператор * распаковывает последовательность (список, кортеж и т. д.) в отдельные позиционные аргументы:
def calculate_rectangle_area(width, height):
"""Calculate the area of a rectangle."""
return width * height
# Вместо передачи аргументов по отдельности
dimensions = [5, 10]
area = calculate_rectangle_area(dimensions[0], dimensions[1])
print(area)
# Распаковать список напрямую
area = calculate_rectangle_area(*dimensions)
print(area)Output:
50
50Когда вы пишете *dimensions, Python распаковывает список [5, 10] в два отдельных аргумента, как если бы вы написали calculate_rectangle_area(5, 10).
Это работает с любым итерируемым объектом:
def format_name(first, middle, last):
"""Format a full name."""
return f"{first} {middle} {last}"
# Распаковка кортежа
name_tuple = ("John", "Q", "Public")
print(format_name(*name_tuple))
# Распаковка списка
name_list = ["Jane", "M", "Doe"]
print(format_name(*name_list))
# Даже распаковка строки (каждый символ становится аргументом)
# Это работает только если функция ожидает правильное количество аргументов
def show_first_three(a, b, c):
return f"{a}, {b}, {c}"
print(show_first_three(*"ABC"))Output:
John Q Public
Jane M Doe
A, B, C20.5.2) Распаковка словарей с **
Оператор ** распаковывает словарь в ключевые аргументы:
def create_user(username, email, age):
"""Create a user profile."""
return f"User: {username}, Email: {email}, Age: {age}"
# Словарь с ключами, совпадающими с именами параметров
user_data = {
"username": "alice",
"email": "alice@example.com",
"age": 28
}
# Распаковать словарь
profile = create_user(**user_data)
print(profile)Output:
User: alice, Email: alice@example.com, Age: 28Когда вы пишете **user_data, Python распаковывает словарь в ключевые аргументы, что эквивалентно:
create_user(username="alice", email="alice@example.com", age=28)Ключи словаря должны совпадать с именами параметров функции, иначе вы получите ошибку:
# Invalid: dictionary key doesn't match parameter name
invalid_data = {"name": "bob", "email": "bob@example.com", "age": 30}
# profile = create_user(**invalid_data)
# TypeError: create_user() got an unexpected keyword argument 'name'20.5.3) Сочетание распаковки с обычными аргументами
Вы можете смешивать распакованные аргументы с обычными аргументами:
def calculate_total(base_price, tax_rate, discount):
"""Calculate total price after tax and discount."""
subtotal = base_price * (1 + tax_rate)
total = subtotal * (1 - discount)
return round(total, 2)
# Часть аргументов обычная, часть распакована
pricing = [0.08, 0.10] # tax_rate и discount
total = calculate_total(100, *pricing)
print(total)Output:
97.2Также можно распаковать несколько коллекций в одном вызове:
def create_full_address(street, city, state, zip_code, country):
"""Create a complete address."""
return f"{street}, {city}, {state} {zip_code}, {country}"
street_address = ["123 Main St", "Springfield"]
location_details = ["IL", "62701", "USA"]
address = create_full_address(*street_address, *location_details)
print(address)Output:
123 Main St, Springfield, IL 62701, USA20.5.4) Практический пример: гибкие вызовы функций
Распаковка особенно полезна при работе с данными из внешних источников:
def send_email(recipient, subject, body, cc=None, bcc=None):
"""Send an email with optional CC and BCC."""
message = f"To: {recipient}\nSubject: {subject}\n\n{body}"
if cc:
message += f"\nCC: {cc}"
if bcc:
message += f"\nBCC: {bcc}"
return message
# Данные письма из конфигурационного файла или базы данных
email_config = {
"recipient": "user@example.com",
"subject": "Welcome",
"body": "Thank you for signing up!",
"cc": "manager@example.com"
}
# Распаковать конфигурацию напрямую
result = send_email(**email_config)
print(result)Output:
To: user@example.com
Subject: Welcome
Thank you for signing up!
CC: manager@example.comЭтот паттерн упрощает передачу аргументов функции в виде структур данных, что часто встречается при построении API или обработке конфигурационных файлов.
20.6) Ловушка изменяемых аргументов по умолчанию (почему значения списка по умолчанию сохраняются)
Одна из самых печально известных ловушек Python связана с использованием изменяемых объектов (например, списков или словарей) в качестве значений параметров по умолчанию. Понимание этой проблемы критически важно для написания корректных функций.
20.6.1) Проблема: общие изменяемые значения по умолчанию
Рассмотрим эту, казалось бы, безобидную функцию:
def add_student(name, grades=[]):
"""Add a student with their grades."""
grades.append(name)
return grades
# Первый вызов
students1 = add_student("Alice")
print(students1)
# Второй вызов — ожидаем новый список
students2 = add_student("Bob")
print(students2)
# Третий вызов
students3 = add_student("Carol")
print(students3)Output:
['Alice']
['Alice', 'Bob']
['Alice', 'Bob', 'Carol']Такое поведение удивляет многих программистов. Каждый вызов add_student() без передачи аргумента grades использует тот же самый объект списка, а не новый. Список сохраняется между вызовами функции, накапливая значения.
20.6.2) Почему это происходит: значения по умолчанию создаются один раз
Ключ к пониманию этого поведения — знание того, когда создаются значения по умолчанию. Python вычисляет значения параметров по умолчанию один раз, когда функция определяется, а не каждый раз при вызове функции.
def demonstrate_default_creation():
"""Show when defaults are created."""
print("Function defined!")
def use_default(value=demonstrate_default_creation()):
"""Use a default that calls a function."""
return value
# Сообщение печатается, когда функция ОПРЕДЕЛЕНА, а не вызванаOutput:
Function defined!Когда Python встречает строку def use_default, он вычисляет параметр по умолчанию value=demonstrate_default_creation(). Это вызывает demonstrate_default_creation(), которая сразу же печатает "Function defined!". Позднейшие вызовы use_default() не вычисляют значение по умолчанию заново, поэтому ничего дополнительно не печатается.
Когда Python встречает def add_student(name, grades=[]):, он создаёт пустой объект списка и сохраняет его как значение по умолчанию для grades. Каждый последующий вызов, который не передаёт аргумент grades, использует тот же объект списка.
Вот более наглядная демонстрация с использованием идентичности объекта:
def show_list_identity(items=[]):
"""Show that the same list object is reused."""
print(f"List ID: {id(items)}")
items.append("item")
return items
# Каждый вызов использует один и тот же объект списка (одинаковый ID)
show_list_identity()
show_list_identity()
show_list_identity()Output:
List ID: 140234567890123
List ID: 140234567890123
List ID: 140234567890123Точные значения ID будут отличаться на вашей системе, но обратите внимание, что все три вызова показывают одно и то же значение ID, доказывая, что используется один и тот же объект списка. Функция id() возвращает уникальный идентификатор для каждого объекта в памяти — когда ID совпадают, это один и тот же объект.
20.6.3) Правильный паттерн: использовать None по умолчанию
Стандартное решение — использовать None в качестве значения по умолчанию и создавать новый изменяемый объект внутри функции:
def add_student_correct(name, grades=None):
"""Add a student with their grades (correct version)."""
if grades is None:
grades = [] # Создать НОВЫЙ список каждый раз
grades.append(name)
return grades
# Теперь каждый вызов получает свой список
students1 = add_student_correct("Alice")
print(students1)
students2 = add_student_correct("Bob")
print(students2)
students3 = add_student_correct("Carol")
print(students3)Output:
['Alice']
['Bob']
['Carol']Этот паттерн работает, потому что None неизменяемый, и новый список создаётся внутри тела функции каждый раз, когда grades равно None.
20.6.4) Та же проблема со словарями
Эта проблема затрагивает все изменяемые типы, а не только списки:
# WRONG: Dictionary default
def create_config_wrong(key, value, config={}):
"""Create a configuration (BUGGY VERSION)."""
config[key] = value
return config
config1 = create_config_wrong("theme", "dark")
print(config1)
config2 = create_config_wrong("language", "en")
print(config2)
print("---")
# CORRECT: None as default
def create_config_correct(key, value, config=None):
"""Create a configuration (CORRECT VERSION)."""
if config is None:
config = {}
config[key] = value
return config
config1 = create_config_correct("theme", "dark")
print(config1)
config2 = create_config_correct("language", "en")
print(config2)Output:
{'theme': 'dark'}
{'theme': 'dark', 'language': 'en'}
---
{'theme': 'dark'}
{'language': 'en'}20.6.5) Итог: золотое правило
Никогда не используйте изменяемые объекты (списки, словари, множества) как значения параметров по умолчанию. Всегда используйте None и создавайте изменяемый объект внутри функции:
# ❌ WRONG
def function(items=[]):
pass
# ✅ CORRECT
def function(items=None):
if items is None:
items = []
# Теперь items можно безопасно использоватьЭтот паттерн гарантирует, что каждый вызов функции получает собственный независимый изменяемый объект, предотвращая загадочные баги, когда данные «утекают» между вызовами.
В этой главе мы подробно рассмотрели гибкую систему параметров и аргументов Python. Вы узнали, как использовать позиционные и ключевые аргументы, задавать значения по умолчанию, обрабатывать переменное число аргументов с помощью *args и **kwargs, распаковывать коллекции при вызове функций и избегать ловушки изменяемых аргументов по умолчанию.
Эти механизмы дают вам мощные инструменты для проектирования интерфейсов функций, которые одновременно гибкие и удобные. По мере того как вы будете писать больше функций, у вас появится интуиция, какие шаблоны параметров лучше подходят для разных ситуаций. Главное — балансировать гибкость и ясность: делайте ваши функции простыми для корректного вызова и сложными для некорректного вызова.