Давайте попробуем реализовать очень простую функцию, суммирующую числа. Для начала определим функцию sum()
, принимающую на вход два числа и возвращающую их сумму:
def sum(a, b):
return a + b
sum(1, 2) # 3
sum(-3, 10) # 7
Пока всё просто и понятно. Сложности возникают при дополнительных требованиях: что, если захотим суммировать не два, а три числа? Или пять, или даже десять? Писать для обработки каждого случая отдельную функцию — очевидно плохой вариант:
def sum_of_three(a, b, c):
return a + b + c
def sum_of_ten(a, b, c, d, e, f, g, h, i, j):
return a + b + c + d + e + f + g + h + i + j # фух...
# def sum_of_thousand ???
# def sum_of_million ???
Надо, чтобы единая функция могла работать с разным количеством аргументов. Как это сделать?
Можно заметить, что в стандартной библиотеке Python существуют функции, которые могут принимать разное количество аргументов. Например, сигнатура функции max() определяется так:
max(arg1, arg2, *args, key=None)
Она говорит нам о том, что в max()
передается 2 обязательных аргумента (arg1
и arg2
) и еще любое количество аргументов (*args
):
max(10, 20) # 20
max(10, 20, 30) # 30
max(10, 20, 30, 40, 50) # 50
max(-10, -20, -30) # -10
С точки зрения вызова — ничего необычного, просто разное число аргументов. А вот определение функции с переменным числом аргументов выглядит необычно:
def func(*args):
# args — это кортеж, содержащий все
# переданные при вызове функции аргументы
print(args)
func() # => ()
func(9) # => (9,)
func(9, 4) # => (9, 4)
func(9, 4, 1) # => (9, 4, 1)
func(9, 4, 1, -3) # => (9, 4, 1, -3)
Символ астериска *
перед именем позиционного параметра в определении функции обозначает оператор упаковки (строго говоря, у *
-оператора нет формального имени, но в документации *args
называют "arguments packing"). Запись *args
в определении func()
из примера выше означает буквально следующее: "все переданные при вызове функции аргументы поместить в кортеж args
".
Если вовсе не передать аргументов, то кортеж args
будет пустым:
func() # => ()
В функцию можно передать любое количество аргументов — все они попадут в кортеж args
:
func(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
# => (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
Аргументы могут быть любого типа — числа, строки, списки:
func(1, 2, 'hello', [3, 4, 5], True)
# => (1, 2, 'hello', [3, 4, 5], True)
Теперь у нас достаточно знаний, чтобы с помощью оператора упаковки переписать нашу функцию sum()
так, чтобы она умела суммировать любое количество чисел:
def sum(*numbers):
# По умолчанию 0, так как сумма ничего это 0
result = 0
for num in numbers:
result += num
return result
sum() # 0
sum(10) # 10
sum(10, 4) # 14
sum(8, 10, 4) # 22
В таком контексте кортеж можно считать как "необязательные аргументы", которые можно либо вовсе не передавать, либо передавать столько, сколько хочешь. А что, если мы захотим, чтобы функция имела два обыкновенных ("обязательных") параметра, а остальные были необязательными? Всё просто: при определении функции сначала указываем стандартные параметры (например, a
и b
) и в конце добавляем *args
:
def func(a, b, *args):
# параметр 'a' содержит первый аргумент
print(f'a -> {a}')
# параметр 'b' содержит второй аргумент
print(f'b -> {b}')
# args содержит все остальные аргументы
print(f'args -> {args}')
func(9, 4)
# => a -> 9
# => b -> 4
# => args -> ()
func(9, 4, 1)
# => a -> 9
# => b -> 4
# => args -> (1,)
func(9, 4, 1, -3)
# => a -> 9
# => b -> 4
# => (1, -3)
func(9, 4, 1, -3, -5)
# => a -> 9
# => b -> 4
# => args -> (1, -3, -5)
То же можно сделать и для одного аргумента:
def func(a, *args):
# ...
и для трёх:
def func(a, b, c, *args):
# ...
Эту идею можно продолжать и дальше, делая обязательными то количество аргументов, которое требуется. Единственное ограничение: rest-оператор может быть использован только для последнего параметра. То есть такой код синтаксически неверен:
def func(*args, a):
# ...
# и такой тоже
def func(a, *args, b):
# ...
Упаковка именованных аргументов
Для упаковки именованных аргументов существует свой оператор - **
, "keyword argument packing". По аналогии с упаковкой позиционных аргументов, этот оператор упаковывает именованные аргументы, но, как вы могли догадаться, не в кортеж, а словарь.
def func(**kwargs):
# kwargs — это словарь, содержащий все
# переданные при вызове функции аргументы
print(kwargs)
func() # => {}
func(a=2, b=4) # => {'a': 2, 'b': 4}
func(a='foo', b=[1, 2, 3]) # => {'a': 'foo', 'b': [1, 2, 3]}
Так как **kwargs
будут преобразованы в словарь, то мы можем работать с ними как с любыми словарями:
def func(**kwargs):
print(kwargs.keys())
print(kwargs.values())
print(kwargs | {'key': 'value'})
# => dict_keys(['a', 'b'])
# => dict_values([2, 5])
# => {'a': 2, 'b': 5, 'key': 'value'}
Разумеется мы можем сочетать все способы объявления аргументов. Главное, стоит помнить, что позиционные аргументы (*args
тоже позиционные) идут впереди именованных.
def func(a, b, *args, f='bar', k=42, **kwargs):
# параметр 'a' содержит первый аргумент
print(f'a -> {a}')
# параметр 'b' содержит второй аргумент
print(f'b -> {b}')
# args содержит все остальные позиционные аргументы
print(f'args -> {args}')
# f содержит именованный аргумент и равен bar по умолчанию
print(f'f -> {f}')
# k содержит именованный аргумент и равен 42 по умолчанию
print(f'k -> {k}')
# kwargs содержит все остальные именованные аргументы
print(f'kwargs -> {kwargs}')
func(1, 2)
# => a -> 1
# => b -> 2
# => args -> ()
# => f -> bar
# => k -> 42
# => kwargs -> {}
func(1, 2, 3)
# => a -> 1
# => b -> 2
# => args -> (3,)
# => f -> bar
# => k -> 42
# => kwargs -> {}
func(1, 2, 3, 4, 5, f='hello', k=24)
# => a -> 1
# => b -> 2
# => args -> (3, 4, 5)
# => f -> hello
# => k -> 24
# => kwargs -> {}
func(1, 2, 3, 4, 5, f='hello', k=24, l=[1, 2], ll={'key':'value'})
# => a -> 1
# => b -> 2
# => args -> (3, 4, 5)
# => f -> hello
# => k -> 24
# => kwargs -> {'l': [1, 2], 'll': {'key': 'value'}}
Выводы
В этом уроке мы научились создавать функции с переменным числом параметров, используя оператор упаковки. Операторы *
и **
упаковывают в себя переданные аргументы и позволяют работать с ними как с коллекциями: кортежами или словарями.
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.