Статьи

Всё, что нужно знать о звёздочках в Python

2022-10-10 13:00 Базовый Python
Все знают, что в Python символ звёздочки используется как оператор умножения:
product = 4 * 2  # 8
Однако, звёздочка имеет особое значение для сложных структур данных, например списков или словарей.

*args и **kwargs

*args в определении функции

В Python оператор распаковки (*) наиболее широко используется в функциях.
Допустим, у нас есть функция, которая может складывать значения и возвращать их сумму:
def add(number_1, number_2):
    return number_1 + number_2

print(add(1, 2)) # 3
Но что делать, если мы хотим сложить произвольное количество значений? Мы можем просто добавить звёздочку перед именем параметра:
def add(*numbers):
    sum_ = 0
    for number in numbers:
        sum_ += number
    return sum_
В теле функции мы ожидаем, что объект numbers будет итерируемым. И действительно, теперь он стал кортежем, что позволяет нам использовать функцию с любым количеством аргументов:
print(add(1, 2, 3, 4))  # 10

* при вызове функции

Мы уже видели, как можно определить функцию, которая будет принимать любое количество аргументов. Однако что делать, если у нас уже есть функция с фиксированным набором параметров, и мы хотим передать ей список значений? Давайте рассмотрим следующий пример:
def add(number_1, number_2, number_3):
    return number_1 + number_2 + number_3
У этой функции 3 параметра. Давайте предположим, что у нас есть список, содержащий ровно три элемента. Конечно, мы могли бы вызвать функцию следующим образом:
my_list = [1, 2, 3]

add(my_list[0], my_list[1], my_list[2])
К счастью, оператор распаковки (*) работает в обоих направлениях. Мы уже видели его использование в определении функции, но и при вызове функции это тоже сработает:
my_list = [1, 2, 3]

print(add(*my_list)) # 6

**kwargs в определении функции

Распаковывать словари с помощью оператора ** можно точно так же, как мы делали с *.
Рассмотрим следующую функцию:
def change_user_details(username, email, phone, date_of_birth, street_address):
    user = get_user(username)
    user.email = email
    user.phone = phone
    user.date_of_birth = date_of_birth
    user.street_address = street_address
Если вызывать функцию с именованными аргументами, вызов будет выглядеть следующим образом:
change_user_details('Олег', 
                    email='myemail@ya.ru', 
                    phone='+77777777777', 
                    date_of_birth='30.08.1991',
                    street_address='Какая-то улица')
В таком случае мы бы могли переписать функцию, чтобы она принимала произвольное количество именованных аргументов, которые затем представляются в виде словаря kwargs.
def change_user_details(username, **kwargs):
    user = get_user(username)
    user.email = kwargs['email']
    user.phone = kwargs['phone']
    user.date_of_birth = kwargs['date_of_birth']
    user.street_address = kwargs['street_address']
Конечно, мы можем использовать словарь kwargs, как и любой другой словарь в Python, так что функция может стать немного чище, если использовать его следующим образом:
def change_user_details(username, **kwargs):
    user = get_user(username)
    for attribute, value in kwargs.items():
        setattr(user, attribute, value)

** при вызове функции

Конечно, оператор ** работает и при вызове функции для распаковки аргументов:
details = {
    'email': 'myemail@ya.ru',
    'phone': '+77777777777',
    'date_of_birth': '30.08.1991',
    'street_address': 'Какая-то улица'
}

change_user_detail('Олег', **details)

Ограничение способов вызова функций

Только именованные аргументы

Интересной особенностью звёздочки в определении функций является то, что её можно использовать автономно, то есть без названия переменной (параметра). И в Python такое определение функции тоже будет абсолютно правильным:
def my_function(*, keyword_arg_1):
    pass
Но что в этом случае делает самостоятельная звёздочка? Звёздочка упаковывает все аргументы (не являющиеся именованными), как мы видели выше. Но в нашем случае у неё нет названия (и ее нельзя использовать), а после * идёт переменная keyword_arg_1. Поскольку * уже соответствует всем позиционным аргументам, у нас остается keyword_arg_1, который обязан использоваться в качестве именованного аргумента.

Вызов функции с позиционными аргументами (my_function(1)) вызовет ошибку:
TypeError: my_function() takes 0 positional arguments but 1 was given

Только позиционные аргументы

Что, если мы хотим заставить пользователей использовать только позиционные аргументы, в отличие от именованных аргументов в предыдущем примере?
Есть очень простой способ. Мы используем знак /:
def only_positional_arguments(arg1, arg2, /):
    pass
Удивительно, но немногие разработчики знают об этом приёме, который был введён ещё в Python 3.8 с PEP 570.
Если попытаться вызвать функцию с именованными аргументами only_positional_arguments(arg1=1, arg2=2), это приведёт к ошибке TypeError.
TypeError: only_positional_arguments() got some positional-only arguments passed as keyword arguments: 'arg1, arg2'

Использование * и ** при создании объектов

Операторы * и ** могут использоваться не только для определения и вызова функций, но ещё и для создания списков и словарей.

Создание списков

Допустим, у нас есть два отдельных списка и мы хотим их объединить.
my_list_1 = [1, 2, 3]
my_list_2 = [10, 20, 30]
Конечно, мы можем сделать это с помощью оператора +:
merged_list = my_list_1 + my_list_2
Однако, оператор * предоставляет нам больше гибкости. Если мы хотим включить единичное значение между списками, мы можем сделать так:
some_value = 42
merged_list = [*my_list_1, some_value, *my_list_2]

print(merged_list)
# [1, 2, 3, 42, 10, 20, 30]

Создание словарей

Опять же, принципы, актуальные для оператора * и списков, также актуальны и для оператора ** со словарями.
social_media_details = {
    'telegram': 'pythontalk_ru'
}

contact_details = {
    'email': 'myemail@ya.ru'
}

user_dict = {'username': 'pythontalk', **social_media_details, **contact_details}
# {'username': 'pythontalk', 'telegram': 'pythontalk_ru', 'email': 'myemail@ya.ru'}

Распаковка объектов

Возможно, вы уже знаете, как разделить элементы последовательности на несколько переменных следующим образом:
my_list = [1, 2, 3]
a, b, c = my_list
print(a, b, c) # 1 2 3
Но знаете ли вы, что звездочки (*) можно использовать для назначения переменных, когда у вас есть последовательность произвольной длины?

Скажем, вам нужен первый элемент и последний элемент списка в определенных переменных.

Вы можете сделать это, используя звездочку следующим образом:
my_list = [1, 2, 3]
a, *b, c = my_list
В этом примере a теперь содержит первый элемент, а c — последний элемент my_list. А что же в b?

В b находится весь список, исключая первый и последний элемент, то есть [2]. Обратите внимание, что b теперь является списком.

Посмотрим на распаковку списка с бОльшим количеством элементов:
my_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
a, *b, c = my_list
print(a) # 1
print(b) # [2, 3, 4, 5, 6, 7, 8, 9]
print(c) # 10

Источник: Bas Steins