Функционал операторов * и ** развивается уже много лет. Я хочу рассмотреть все способы их использования по состоянию на текущий момент. Буду указывать, что конкретно работает только в современных версиях питона. Поэтому, если вы изучали операторы * и ** еще во времена питона 2 (Python 2), советую хотя бы проглядеть данную статью, потому что в питоне 3 (Python 3) этим операторам добавили много новых возможностей.
Если вы начали изучать питон недавно и еще не освоили аргументы ключевых слов (keyword arguments; также известные как именованные аргументы, named arguments), предлагаю сперва прочитать мою статью про аргументы ключевых слов в питоне.
Что мы обсуждать не будем
В данной статье, говоря про операторы * и **, я имею в виду операторы-префиксы (prefix operators), а не инфиксы (infix). То есть, функции умножения и возведения в степень не входят в тему статьи.
>>> 2 * 5
10
>>> 2 ** 5
32
Тогда про что же мы говорим
Мы говорим про операторы-префиксы * и **, которые используются перед переменной (variable). Например:
>>> more_numbers = [*numbers, 11, 18]
>>> print(*more_numbers, sep=', ')
2, 1, 3, 4, 7, 11, 18
В данном коде можно увидеть два способа использования оператора *. Оператор ** отсутствует.
В сферу применения рассматриваемых операторов входит:
1. Операторы * и **: передача аргументов в функцию.
2. Операторы * и **: захват аргументов, переданных в функцию.
3. Оператор *: принятие аргументов, содержащих только ключевые слова.
4. Оператор *: захват элементов во время распаковки кортежа (tuple).
5. Оператор *: распаковка итерируемых объектов в списке или кортеже.
6. Оператор **: + распаковка словарей в других словарях.
Даже если вам кажется, что вы освоили все эти способы, позволяющие использовать операторы * и **, рекомендую посмотреть на все нижеприведенные блоки кода, чтобы убедиться, что они все вам знакомы. Последние несколько лет разработчики ядра питона продолжают добавлять этим операторам новые возможности, поэтому какие-то из новых применений можно проглядеть.
Операторы * и ** при распаковке во время вызова функции
При вызове функции оператор * можно задействовать для распаковки итерируемого объекта в аргументах, введенных для вызова:
>>> print(fruits[0], fruits[1], fruits[2], fruits[3])
lemon pear watermelon tomato
>>> print(*fruits)
lemon pear watermelon tomato
В строке print(*fruits) производится вызов всех элементов списка fruits в функции print. Они становятся отдельными аргументами. При этом нам даже не надо знать, сколько всего аргументов окажется в списке.
В данном примере оператор * – не просто синтактический выверт (syntactic sugar). Без * отправить все элементы конкретного итерируемого объекта в качестве отдельных аргументов было бы невозможно (это не касается списков с сфиксированной длиной).
Еще один пример:
return [
list(row)
for row in zip(*list_of_lists)
]
В данном случае мы принимаем список со списками в качестве элементов и возвращаем «преобразованный» список со списками:
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
Функционал оператора ** примерно такой же, просто он применяется к аргументам ключевых слов. Он позволяет нам взять словарь, содержащий пары из ключей и значений, и распаковать его в аргументы ключевых слов при вызове функции.
>>> filename = "{year}-{month}-{day}.txt".format(**date_info)
>>> filename
'2020-01-01.txt'
Скажу из своего опыта. Оператор ** не часто используется для распаковки аргументов ключевых слов при вызове функции. Чаще всего я вижу такие примеры при работе с наследованием: вызовы super() часто включают в себя оба оператора.
Операторы * и ** можно использовать неоднократно при вызове функции. Данная возможность появилась в питоне 3.5. Иногда это может оказаться очень уместным:
>>> numbers = [2, 1, 3, 4, 7]
>>> print(*numbers, *fruits)
2 1 3 4 7 lemon pear watermelon tomato
Неоднократное использование ** выглядит примерно так же:
>>> track_info = {'artist': "Beethoven", 'title': 'Symphony No 5'}
>>> filename = "{year}-{month}-{day}-{artist}-{title}.txt".format(
... **date_info,
... **track_info,
... )
>>> filename
'2020-01-01-Beethoven-Symphony No 5.txt'
Однако не нужно терять бдительности при неоднократном использовании операторов * и **. Неоднократное указание одного и того же аргумента ключевых слов не допускается в функциях питона. Поэтому, если использовать ** для словарей, то ключи должны быть уникальными. В противном случае возникнет исключение.
Операторы * и ** при упаковке аргументов, переданных функции
При определении тела функции можно использовать оператор *, чтобы захватывать неограниченное количество позиционных аргументов, переданных этой функции. Они оформляются в кортеж.
def roll(*dice):
return sum(randint(1, die) for die in dice)
Данная функция принимает любое количество аргументов.
18
>>> roll(6, 6)
9
>>> roll(6, 6, 6)
8
Функции питона print и zip принимают любое количество позиционных аргументов. Такое использование оператора * при упаковке аргументов позволяет нам создавать свои функции, которые (аналогично print и zip) принимают любое количество аргументов.
Кроме того, для оператора ** в данном вопросе предусмотрена еще одна возможность: его можно использовать при определении тела функции, позволяющей захватить в словарь любые аргументы ключевых слов, переданные этой функции:
attribute_list = [
f'{name}="{value}"'
for name, value in attributes.items()
]
return f"<{tag_name} {' '.join(attribute_list)}>"
В данном месте ** захватывает в словарь аргументы ключевых слов, которые мы передаем данной функции. Впоследствии аргументы атрибутов (attributes) данной функции смогут на него ссылаться.
>>> tag('img', height=20, width=40, src="/face.jpg")
Позиционные аргументы, содержащие аргументы только из ключевых слов
питон 3 ввел специальный синтаксис для передачи аргументов, содержащих только ключевые слова, в функцию. Они представляют собой такие аргументы функции, которые можно определить только с помощью синтаксиса для ключевых слов. Это означает, что их нельзя определить позиционно.
Чтобы принимать аргументы, содержащие только ключевые слова, мы можем поместить именованные аргументы после оператора * при определении тела функции:
return [
dictionary.get(key, default)
for key in keys
]
Данную функцию можно использовать так:
>>> get_multiple('lemon', 'tomato', 'squash', dictionary=fruits, default='unknown')
['yellow', 'red', 'unknown']
Аргументы dictionary и default поставлены после *keys. То есть, их можно только в качестве аргументов ключевых слов. Если мы попытаемся определить их позиционно, то увидим ошибку:
>>> get_multiple('lemon', 'tomato', 'squash', fruits, 'unknown')
Traceback (most recent call last):
+ File "", line 1, in
TypeError: get_multiple() missing 1 required keyword-only argument: 'dictionary'
Данное поведение внедрено в питон с помощью предложения PEP 3102.
Аргументы, содержащие только ключевые слова и не содержащие позиционные аргументы
Аргументы, содержащие только ключевые слова, – неплохое средство. Но что если вы хотите требовать ввода аргументов, содержащих только ключевые слова, не захватывая неограниченное количество позиционных аргументов?
питон позволяет сделать это с помощью немного странного синтаксиса, когда оператор * как бы сам по себе:
"""Yield each iterable item along with the item before it."""
previous = fillvalue
for item in iterable:
yield previous, item
previous = item
Данная функция принимает аргумент, содержащий итерируемый объект (iterable). Его можно определить позиционно (то есть, первым) или с помощью названия и аргумента fillvalue, который входит в число аргументов, допускающих только ключевые слова. Это означает, что мы можем вызвать функцию with_previous вот так:
[(0, 2), (2, 1), (1, 3)]
А так – нет:
Traceback (most recent call last):
File "", line 1, in
TypeError: with_previous() takes 1 positional argument but 2 were given
Данная функция принимает два аргумента. Один из них, fillvalue, обязательно определяется как аргумент, содержащий ключевое слово.
Обычно я использую аргументы, допускающие только ключевые слова, при захвате неопределенного количества позиционных аргументов. Но иногда я использую данную возможность оператора *, чтобы форсировать исключительно позиционное определение аргумента.
На самом деле, данный подход используется встроенной функцией питона sorted. Если посмотреть справку для sorted, можно увидеть следующее:
Help on built-in function sorted in module builtins:
sorted(iterable, /, *, key=None, reverse=False)
Return a new list containing all items from the iterable in ascending order.
A custom key function can be supplied to customize the sort order, and the reverse flag can be set to request the result in descending order.
Пример использования оператора * как самого по себе прямо в документации по аргументам функции sorted.
Операторы * и ** при распаковке кортежа
Дополнительно к вышенаписанному, в питоне 3 добавлен новый способ применения *, который некоторым образом связан с вышеописанными возможностями этого оператора при определении тела функции и при вызове функции.
Оператор * теперь можно использовать при распаковке кортежа:
>>> first, second, *remaining = fruits
>>> remaining
['watermelon', 'tomato']
>>> first, *remaining = fruits
>>> remaining
['pear', 'watermelon', 'tomato']
>>> first, *middle, last = fruits
>>> middle
['pear', 'watermelon']
Если вы задаете себе вопрос: как же мне использовать это в своем коде, посмотрите примеры в моей статье про распаковку кортежей в питоне. В этой статье я показал, каким образом такое использование оператора * может, в некоторых случаях, стать альтернативой для срезания последовательностей (sequence slicing).
Обычно во время своих лекций об операторе * я говорю, что можно использовать только одно выражение с ним в отдельном вызове многократного присвоения (multiple assignment). Технически это некорректно, потому что можно его использовать два раза при вложенной распаковке (nested unpacking). Данный вопрос я рассмотрел подробно в статье про распаковку кортежей.
>>> ((first_letter, *remaining), *other_fruits) = fruits
>>> remaining
['e', 'm', 'o', 'n']
>>> other_fruits
['pear', 'watermelon', 'tomato']
Правда, я никогда не встречал хорошего примера такого использования. Не думаю, что стал бы его рекомендовать, даже если бы удалось найти. Он выглядит немного непонятно.
Данная возможность добавлена в питон на основе предложения PEP 3132. Следует отметить, что оно не относится к очень длинным.
Операторы * и ** в литерале списка
В питоне 3.5 добавлено очень много возможностей, связанных с оператором *, на основе предложения PEP 448. Одной из самых заметных новых возможностей стало использование * для вывода итерируемого объекта в новый список.
Допустим, у вас есть функция, которая принимает любые последовательности и возвращает список, содержащий каскад из последовательности и ее реверсивного варианта:
return list(sequence) + list(reversed(sequence))
Данная функция должна пару раз провести конвертацию в список, чтобы объединить списки и вернуть результат. Начиная с питона 3.5, мы можем, вместо вышеприведенного примера, написать следующее:
return [*sequence, *reversed(sequence)]
В данном коде больше нет нескольких ненужных вызовов списков. Поэтому он стал эффективнее и лучше читается.
Еще пример:
return [*sequence[1:], sequence[0]]
Данная функция возвращает новый список, в котором первый элемент переданного списка (или другой последовательности) перенесен в конец нового списка.
Это очень удачная возможность, позволяющая с помощью оператора * объединять итерируемые объекты различных типов. Оператор * работает с любыми итерируемыми объектами, а оператор + работает только с определенными последовательностями, при чем все из объединяемых должны быть одного типа.
Отмечу, что данная возможность не ограничивается только созданием списков. Мы можем выводить итерируемые объекты в новые кортежи или множества (set):
>>> (*fruits[1:], fruits[0])
('pear', 'watermelon', 'tomato', 'lemon')
>>> uppercase_fruits = (f.upper() for f in fruits)
>>> {*fruits, *uppercase_fruits}
{'lemon', 'watermelon', 'TOMATO', 'LEMON', 'PEAR', 'WATERMELON', 'tomato', 'pear'}
Обратите внимание, что последняя строка принимает список и генератор (generator), а потом выводит их в новое множество. Перед появлением этой возможности для оператора * было непросто сделать это в одну строку кода. Разумеется, способ сделать это существовал, но его было непросто вспомнить или обнаружить:
{'lemon', 'watermelon', 'TOMATO', 'LEMON', 'PEAR', 'WATERMELON', 'tomato', 'pear'}
Оператор ** в литерале словаря
Помимо вышеприведенного на основе предложения PEP 448 в функционал ** добавлен вывод пар ключ/значение (key/value) из словаря в новый словарь:
>>> track_info = {'artist': "Beethoven", 'title': 'Symphony No 5'}
>>> all_info = {**date_info, **track_info}
>>> all_info
{'year': '2020', 'month': '01', 'day': '01', 'artist': 'Beethoven', 'title': 'Symphony No 5'}
Про это я написал еще одну статью. Сейчас ее можно найти под новым названием про идиоматический способ сливать словари в питоне. Данную возможность можно использовать не только для сливания двух словарей. Например, можно скопировать словарь, параллельно добавляя в него новые значения:
>>> event_info = {**date_info, 'group': "Python Meetup"}
>>> event_info
{'year': '2020', 'month': '01', 'day': '7', 'group': 'Python Meetup'}
Еще можно скопировать или слить словари, переписывая определенные значения:
>>> new_info = {**event_info, 'day': "14"}
>>> new_info
{'year': '2020', 'month': '01', 'day': '14', 'group': 'Python Meetup'}
Операторы * и ** обладают немалыми возможностями в питоне
В питоне операторы * и ** – не просто синтактический выверт. Некоторые из их возможностей реализуются и другими средствами, но доступные альтернативы обычно более громоздкие и потребляют больше ресурсов. Кроме того, некоторые элементы в функционале этих операторов попросту не доступны без их использования. Например, без помощи * невозможно передать в функцию неопределенное количество позиционных аргументов.
Прочитав обо всех возможностях * и **, вы, возможно, удивитесь названиям, под которыми используются эти странные операторы. К сожалению, для них нет лаконичных названий. Я слышал, как * называли оператором для упаковки и распаковки. Еще слышал, как его называли «splat» (это из мира Руби) и просто звездой.
Чаще всего я называю их звездой (star) и двойной звездой (double star) (или звездой-звездой (star star)). В данном случае разделения с их функциями как инфиксов не проводится (речь идет про операции умножения и возведения в степень). Но обычно из контекста очевидно, о чем идет речь, про префиксы или инфиксы.
Если вы не понимаете операторы * и ** или боитесь не запомнить все их возможности, не нужно беспокоиться. Способов использования много, и запоминать каждую конкретную возможность для каждого из них не так важно. Лучше будет осознать, в каких случаях к ним можно обратиться. Предлагать использовать данную статью как чек-лист или создать свой чек-лист, который поможет вам использовать операторы * и ** в питоне.