Звёздный Python: где и как используются * и **

В Python много где можно встретить * и **. Два этих оператора порой могут быть загадкой как для начинающих программистов, так и для тех, кто пришёл в Python из других языков, не имеющих точно таких же операторов. Сегодня мы поговорим о том, как их можно использовать.

Что мы не собираемся обсуждать

Речь пойдёт о префиксных, а не об инфиксных операторах. То есть мы не собираемся обсуждать умножение и возведение в степень:

>>> 2 * 5
10
>>> 2 ** 5
32

О чём пойдёт речь

Мы обсудим префиксные операторы * и **, которые используются перед переменными. Например:

>>> numbers = [2, 1, 3, 4, 7]
>>> more_numbers = [*numbers, 11, 18]
>>> print(*more_numbers, sep=', ')
2, 1, 3, 4, 7, 11, 18

Здесь показаны два примера использования * и ни одного примера для **. В их число входит:

  • Использование * и ** для передачи аргументов в функцию;
  • Использование * и **   для сбора переданных в функцию аргументов;
  • Использование ** для принятия только именованные аргументов;
  • Использование * при распаковке кортежей;
  • Использование * для распаковки итерируемых объектов в список/кортеж;
  • Использование ** для распаковки словарей в другие словари.

Даже если вы считаете, что вам знакомы все эти способы использования * и **, не будет лишним посмотреть все примеры ниже, чтобы убедиться в этом. С течением времени разработчики языка добавляли новые возможности для этих операторов, поэтому не будет ничего удивительного в том, что вы упустили новые варианты их использования.

Прим. перев. Примеры кода в статье предполагают использование Python 3.

Звёздочки для распаковки в аргументы функции

При вызове функции можно использовать оператор * для распаковки итерируемого объекта в аргументы вызова:

>>> fruits = ['lemon', 'pear', 'watermelon', 'tomato']
>>> print(fruits[0], fruits[1], fruits[2], fruits[3])
lemon pear watermelon tomato
>>> print(*fruits)
lemon pear watermelon tomato

Строка print(*fruits) передаёт все элементы списка fruits в вызов print() как отдельные аргументы, поэтому нам даже не нужно знать, сколько элементов в списке.

Здесь оператор * — не просто синтаксический сахар. Без фиксированной длины списка было бы невозможно передать элементы итерируемого объекта как отдельные аргументы, не используя *.

Вот другой пример:

def transpose_list(list_of_lists):
    return [
        list(row)
        for row in zip(*list_of_lists)
    ]

Здесь мы принимаем список со списками и возвращаем «транспонированный» список.

>>> transpose_list([[1, 4, 7], [2, 5, 8], [3, 6, 9]])
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]

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

>>> date_info = {'year': "2020", 'month': "01", 'day': "01"}
>>> filename = "{year}-{month}-{day}.txt".format(**date_info)
>>> filename
'2020-01-01.txt'

Начиная с Python 3.5  * и ** можно использовать несколько раз в вызове функции.

Порой бывает полезно использовать * несколько раз:

>>> fruits = ['lemon', 'pear', 'watermelon', 'tomato']
>>> numbers = [2, 1, 3, 4, 7]
>>> print(*numbers, *fruits)
2 1 3 4 7 lemon pear watermelon tomato

Похожим образом используются **:

>>> date_info = {'year': "2020", 'month': "01", 'day': "01"}
>>> 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'

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

Звёздочки для упаковки аргументов, переданных в функцию

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

from random import randint

def roll(*dice):
    return sum(randint(1, die) for die in dice)

Эта функция принимает любое количество аргументов:

>>> roll(20)
18
>>> roll(6, 6)
9
>>> roll(6, 6, 6)
8

Стандартные функции Python print() и zip() принимают любое количество позиционных аргументов. Благодаря * мы можем написать свою функцию, работающую похожим образом.

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

def tag(tag_name, **attributes):
    attribute_list = [
        f'{name}="{value}"'
        for name, value in attributes.items()
    ]
    return f"<{tag_name} {' '.join(attribute_list)}>"

Оператор ** соберёт все переданные именованные аргументы в словарь, на который ссылается аргумент attributes:

>>> tag('a', href="http://example.com")
'<a href="http://example.com">'
>>> tag('img', height=20, width=40, src="img.jpg")
'<img height="20" width="40" src="img.jpg">'

Позиционные аргументы с только именованными аргументами

В Python 3 появился специальный синтаксис для только именованных (keyword-only) аргументов. Такие аргументы нельзя указать позиционно, только по имени.

Чтобы принимать только именованные аргументы, при определении функции мы можем расположить именованные аргументы после *:

def get_multiple(*keys, dictionary, default=None):
    return [
        dictionary.get(key, default)
        for key in keys
    ]

Эту функцию можно использовать следующим образом:

>>> fruits = {'lemon': 'yellow', 'orange': 'orange', 'tomato': 'red'}
>>> get_multiple('lemon', 'tomato', 'squash', dictionary=fruits, default='unknown')
['yellow', 'red', 'unknown']

Аргументы dictionary и default идут после *keys, а это значит, что их можно указать только как именованные аргументы. Если попытаться сделать иначе, мы получим ошибку:

>>> fruits = {'lemon': 'yellow', 'orange': 'orange', 'tomato': 'red'}
>>> 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.

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

Описанный выше пример выглядит здорово, но что, если мы хотим получить только именованные аргументы без захвата неограниченного количества позиционных?

Прим. перев. Если к этому моменту вы задались вопросом, зачем вообще нужны только именованные аргументы, то всё просто: вызов функции с именованными аргументами выглядит гораздо лучше — сразу можно понять, какой аргумент за что отвечает. Более того, при использовании позиционных аргументов вы вынуждены соблюдать их порядок, в то время как именованные аргументы можно расположить как угодно. Также они позволяют не указывать значения аргументов, у которых есть значения по умолчанию.

Python позволяет сделать это с помощью одинокой звёздочки:

def with_previous(iterable, *, fillvalue=None):
    """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() вот так:

>>> list(with_previous([2, 1, 3], fillvalue=0))
[(0, 2), (2, 1), (1, 3)]

Но не так:

>>> list(with_previous([2, 1, 3], 0))
Traceback (most recent call last):
  File "", line 1, in 
TypeError: with_previous() takes 1 positional argument but 2 were given

Эта функция принимает два аргумента, и один из них, fillvalue, должен быть именованным.

Встроенная функция sorted() использует этот подход. Если посмотреть справку по этой функции, мы увидим следующее:

>>> help(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.

Прим. перев. Нечто аналогичное можно сделать и для позиционных аргументов. Как вы могли заметить, в определении sort() используется /. Он нужен для того, чтобы предшествующие аргументы можно было передавать только как позиционные, а не по имени.

Звёздочки для распаковки

В Python 3 также появилась возможность использовать оператор * для распаковки итерируемых объектов:

>>> fruits = ['lemon', 'pear', 'watermelon', 'tomato']
>>> first, second, *remaining = fruits
>>> remaining
['watermelon', 'tomato']
>>> first, *remaining = fruits
>>> remaining
['pear', 'watermelon', 'tomato']
>>> first, *middle, last = fruits
>>> middle
['pear', 'watermelon']

Распаковка может быть даже вложенной:

>>> fruits = ['lemon', 'pear', 'watermelon', 'tomato']
>>> ((first_letter, *remaining), *other_fruits) = fruits
>>> remaining
['e', 'm', 'o', 'n']
>>> other_fruits
['pear', 'watermelon', 'tomato']

Вряд ли вам представится возможность так сделать, но, возможно, это и к лучшему.

Tакая функциональность была добавлена в PEP 3132.

Звёздочки в литералах списков

В Python 3.5 появились новые способы использования звёздочек. Одной из ключевых новых фич являлась возможность сложить итерируемый объект в новый список.

Допустим, у вас есть функция, которая принимает любую последовательность и возвращает список, состоящий из этой последовательности и её обратной копии, сконкатенированных вместе:

def palindromify(sequence):
    return list(sequence) + list(reversed(sequence))

Здесь нам требуется несколько раз преобразовывать последовательности в списки, чтобы получить конечный результат. В Python 3.5 можно поступить по-другому:

def palindromify(sequence):
    return [*sequence, *reversed(sequence)]

Этот вариант избавляет нас от необходимости лишний раз вызывать list и делает наш код более эффективным и читаемым.

Ещё пример:

def rotate_first_item(sequence):
    return [*sequence[1:], sequence[0]]

Эта функция возвращает новый список, в котором первый элемент переданного списка (или другой последовательности) перемещается в конец нового списка.

Такой вариант использования оператора * является отличной возможностью для конкатенации итерируемых объектов разных типов. Оператор * работает с любым итерируемым объектом, в то время как оператор + работает только с определёнными последовательностями, которые должны быть одного типа.

Мы не ограничены созданием списков. Мы также можем создавать новые кортежи или множества:

>>> fruits = ['lemon', 'pear', 'watermelon', 'tomato']
>>> (*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'}

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

>>> set().union(fruits, uppercase_fruits)
{'lemon', 'watermelon', 'TOMATO', 'LEMON', 'PEAR', 'WATERMELON', 'tomato', 'pear'}

Двойные звёздочки в литералах словарей

В PEP 448 были также добавлены новые возможности для **, благодаря которым стало возможным перемещение пар ключ-значение из одного словаря (словарей) в новый:

>>> date_info = {'year': "2020", 'month': "01", 'day': "01"}
>>> 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'}

Однако это можно использовать не только для объединения двух словарей.

Например, мы можем скопировать словарь, параллельно добавляя в него новое значение:

>>> date_info = {'year': '2020', 'month': '01', 'day': '7'}
>>> event_info = {**date_info, 'group': "Python Meetup"}
>>> event_info
{'year': '2020', 'month': '01', 'day': '7', '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'}

Звёздочки — сила

Операторы * и ** в Python не просто синтаксический сахар. Часть из того, что можно сделать с их помощью, можно достичь другими путями, но они, как правило, более громоздкие и ресурсоёмкие. А некоторые из возможностей, предоставляемых звёздочками, вовсе нельзя реализовать иначе: например, принять переменное количество аргументов в функцию без *.

Перевод статьи «Asterisks in Python: what they are and how to use them»

Подобрали три теста для вас:
— А здесь можно применить блокчейн?
Серверы для котиков: выберите лучшее решение для проекта и проверьте себя.
Сложный тест по C# — проверьте свои знания.

Также рекомендуем: