Зарегистрируйтесь, чтобы продолжить обучение

Операторы упаковки Python: Функции

Давайте попробуем реализовать очень простую функцию, суммирующую числа. Для начала определим функцию 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):
  # ...

Эту идею можно продолжать и дальше, делая обязательными то количество параметров, которое требуется. Единственное ограничение: оператор упаковки может быть использован только для последнего параметра. То есть такой код синтаксически неверен:

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'}}

Выводы

В этом уроке мы научились создавать функции с переменным числом параметров, используя оператор упаковки. Операторы * и ** упаковывают в себя переданные аргументы и позволяют работать с ними как с коллекциями: кортежами или словарями.


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff