⭐ Звезды в Python

Звездочка (этот символ называется «астериск») – один из самых многоликих операторов в Python. Едва ли хватит пальцев руки, чтобы перечислить все его применения. Давайте по порядку.

Умножение и размножение

Самое простое применение одиночного астериска: умножение чисел. Двойного – возведение числа в степень.

>>> 3 * 4
12
>>> 3 ** 3
27
>>> 4 ** 0.5
2.0

Если мы умножим список (или кортеж) на целое число, то получим новый список (или кортеж), где элементы исходного повторены несколько раз. Подобное случится и со строкой. Если мы умножаем на ноль (0) или на число меньше ноля, то получим пустой список, кортеж или строку.

>>> [1, 2, 3] * 5
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
>>> ("abc", "foo", "bar") * 3
('abc', 'foo', 'bar', 'abc', 'foo', 'bar', 'abc', 'foo', 'bar')
>>> "hello world" * 4
'hello worldhello worldhello worldhello world'
>>> [1, 2, 3] * 0
[]
>>> "wat" * -10
''

Звезды в аргументах функций

Одна звездочка позволяет получить все или некоторые позиционные аргументы функции в виде кортежа. Позиционные – это те, которые просто подряд передаются без указания имени. Те, что с именем, это аргументы ключевого слова. Разберемся сначала с первыми. Ниже идут два примера. В первом мы получаем все позиционные аргументы в кортеж с названием args. Во втором случае мы обязуем пользователя наших функций передать как минимум 2 аргумента (они попадут в x и y) и дополнительно произвольное число (можно ни одного) аргументов, которые попадут в кортеж rest. Я специально развал их по-разному, не обязательно всегда называть их args. Обратите внимание, что это всегда будет кортеж, даже если мы передадим один аргумент, то получим кортеж из одного элемента.

def foo(*args):
    print('You passed {} args, they are {}'.format(len(args), args))

def foofoo(x, y, *rest):
    print('x = {}, y = {}, rest = {}'.format(x, y, rest))

>>> foo()
You passed 0 args, they are ()
>>> foo(10, 20, "str", {})
You passed 4 args, they are (10, 20, 'str', {})

>>> foofoo(11, 22)
x = 11, y = 22, rest = ()
>>> foofoo(12, 13, 15)
x = 12, y = 13, rest = (15,)
>>> foofoo(12, 13, 15, 20)
x = 12, y = 13, rest = (15, 20)

Пример такой функции мы недавно рассматривали – это хорошо знакомый нам print. Как известно, он принимает произвольное число аргументов, пользуясь выше описанным механизмом. Можете пройти по ссылке и увить его сигнатуру.

Важно, чтобы «звездная переменная» была после всех позиционных аргументов, иначе мы получим ошибку SyntaxError.

Две звездочки перед названием аргумента позволяют нам получить произвольное число произвольно названных именованных аргументов (еще их называют аргументами ключевых слов). Такую переменную часто называют **kwargs (от key-word arguments). В нее будет записан словарик (dict) из пар название ключевого слова (строка) и значение аргумента. Давайте обратимся к примерам:

def baz(**kwargs):
    print(kwargs)

>>> baz(x=1, y=2, z="hello")
{'y': 2, 'x': 1, 'z': 'hello'}
>>> baz()
{}

Как видно, без аргументов мы получили пустой словарь. А с именованными аргументами получили словарь, где их имена – ключи-строки, а их значения – собственно сами переданные значений. В функцию baz нельзя передать аргументы без имен, будет ошибка, потому что без имен – позиционные аргументы, а мы никак их не обозначили.

def foobaz(x, y, **kwargs):
    print('x = {} and y = {}'.format(x, y))
    print('also:', kwargs)

>>> foobaz(2, 3)
x = 2 and y = 3
('also:', {})

>>> foobaz(2, 3, other=77, z=88)
x = 2 and y = 3
('also:', {'other': 77, 'z': 88})

>>> foobaz(x=100, y=200, z=300)
x = 100 and y = 200
('also:', {'z': 300})

Тут мы требуем ровно два позиционных аргумента (x и – обязательные аргументы) и любое количество аргументов ключевых слов, которые попадут в kwargs. Нюанс: мы может передать x и y по именам (последний пример), но они не попадут в kwargs, а останутся только в своих соответствующих переменных x и y, и только z попал в kwargs, потому что мы заранее его не объявили.

Можно сочетать *args и **kwags в одной функции, причем именно в этом порядке.

def megafunc(x, y, *args, **kwargs):
    print('x = {} and y = {}'.format(x, y))
    print('also args: {}'.format(args))
    print('also kwargs {}'.format(kwargs))

>>> megafunc(10, 15)
x = 10 and y = 15
also args: ()
also kwargs {}

>>> megafunc(10, 15, 20)
x = 10 and y = 15
also args: (20,)
also kwargs {}

>>> megafunc(10, 15, 20, 22)
x = 10 and y = 15
also args: (20, 22)
also kwargs {}

>>> megafunc(10, 15, 20, 25, troll=30, dwarf=40)
x = 10 and y = 15
also args: (20, 25)
also kwargs {'troll': 30, 'dwarf': 40}

Еще одно применение звездочки – заставить вызывать функцию, указывая название аргументов в обязательном порядке.

def strict_foo(x, *, cat, dog):
    print(x, cat, dog)

>>> strict_foo(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: strict_foo() takes 1 positional argument but 3 were given

>>> strict_foo(1, cat=2, dog=3)
1 2 3

Имя x мы можем и не указывать, просто передадим значение, но cat и dog мы обязаны указать по имени. Зачем это надо? Если функция принимает много разных аргументов, то мы можем принудить указывать имена, чтобы пользователь не перепутал их порядок. Еще такой код просто выглядит более читаемым.

Раскрытие коллекций в аргументах функций при вызове

Также звездочка и двойная звездочка находят применение по другую сторону баррикад – при вызове функции. В предыдущем разделе мы обсуждали, как получать аргументы, а сейчас говорим о том, как их передавать.

Передавая кортеж или список со одной звездочкой – он раскроется в череду позиционных аргументов. Справа от звездочки может быть как имя переменной, так и литерал коллекции или даже вызов функции. Определим две функции: в одной foo – переменное число позиционных аргументов, в другой fixed_foo – фиксированное (три).

def foo(*args):
    print(args)

>>> foo(*[1, 2, 3])
(1, 2, 3)
>>> letters = ('a', 'b', 'c', 'd')
>>> foo(*letters)
('a', 'b', 'c', 'd')

def fixed_foo(a, b, c):
    print(a, b, c, sep=' and ')

>>> fixed_foo(*(1, 2, 3))
1 and 2 and 3
>>> fixed_foo(*(1, 2, 3, 4))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: fixed_foo() takes 3 positional arguments but 4 were given

В foo мы вольны передать список или кортеж любой длины, а в fixed_foo мы обязаны передать список или кортеж длины 3, не больше, не меньше.

Допускается использовать в одном вызове звездочку несколько раз:

>>> foo(*[1, 2, 3], *['a', 'b', 'c'])
(1, 2, 3, 'a', 'b', 'c')

Можно догадаться, что двойная звездочка несет схожую функциональность – раскрывает словарь в именованные аргументы функции.

def baz(a, b, c):
    print('a = ', a)
    print('b = ', b)
    print('c = ', c)

>>> baz(**{'a': 1, 'b': 2, 'c': 3})
a =  1
b =  2
c =  3

Если у нас нет **kwargs, то передаваемый словарь должен содержать ровно столько пар ключ-значение, сколько есть аргументов в функции (без значения по-умолчанию, естественно), причем ключи должен совпадать по именам с названиями аргументов (а вот порядок не важен). То есть при ‘a’: 1 в a попадет 1 и так далее.

>>> baz(10, **{'b': 20, 'c': 30})
a =  10
b =  20
c =  30

>>> baz(b=10, **{'a': 20, 'c': 30})
a =  20
b =  10
c =  30

В примерах выше мы передаем один аргумент явно и два аргумента словарем.

Возможны разнообразные варианты вызова функции. Даже такие:

def uber_func(a, b, c, d, x=10, y=20):
    print(f'a = {a} and b = {b} and c = {c} and d = {d}, x = {x}, y = {y}')

>>> uber_func(*[1, 2], 3, **{'d': 100, 'x': 200})
a = 1 and b = 2 and c = 3 and d = 100, x = 200, y = 20

Склеивание списков и словарей

Мы можем «встраивать» одни списки, кортежи и словари в другие с помощью астерисков. Это значит, что мы добавляем элементы одной коллекции в другую. Одинарная звездочка – для списков и кортежей:

>>> ( *(1, 2), *(3, 4) )
(1, 2, 3, 4)

>>> (1, 2) + (3, 4)  # тоже самое же?
(1, 2, 3, 4)

>>> [ *(1, 2), *[3, 4] ]
[1, 2, 3, 4]

>>> ( *[1, 2], *(3, 4) )
(1, 2, 3, 4)

Это похоже на то, как мы склеиваем коллекции оператором плюс (+), вот только плюсом нельзя соединить кортеж и список, будет ошибка. А через звездочку можно. Но согласитесь, сложение читается понятнее и легче.

Со словарями это немного полезнее. Применение двойной звезды (**) позволяет обновить один словарь элементами другого или нескольких других.

>>> d = { 'x': 10, 'y': 20 }
>>> d2 = { 'a': 100, 'b': 200 }

>>> { **d, **d2 }
{'x': 10, 'y': 20, 'a': 100, 'b': 200}

>>> { **d, **d2, 'other': 'foo' }
{'x': 10, 'y': 20, 'a': 100, 'b': 200, 'other': 'foo'}

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

>>> d_old = { 'president': 'Medvedev' }
>>> d_new = { 'president': 'Putin' }
>>> { **d_old, **d_new }
{'president': 'Putin'}

Распаковка

Позволяет раскидывать по переменным содержимое сложных структур из списков и кортежей. В переменную со звездочкой попадут в виде списка все остальные значения распакуемой структуры, кроме указанных явно. Лучше это понять на примерах:

>>> numbers = [1, 2, 3, 4, 5, 6]

>>> *a, = numbers   # да, там реально одинокая запятая после a
>>> a
[1, 2, 3, 4, 5, 6]

>>> *a, b, c = numbers
>>> a, b, c
([1, 2, 3, 4], 5, 6)

>>> a, b, *middle, c = numbers
>>> a, b, middle, c
(1, 2, [3, 4, 5], 6)

>>> [a, b, *middle, c] = numbers   # скобки можно любые
>>> a, b, middle, c
(1, 2, [3, 4, 5], 6)

Имя со звездочкой может быть как в начале, так и в конце кортежа и даже в середине. В последнем и в предпоследнем примере мы берем первый (a), второй (b) элементы; потом все, кроме последнего в middle пойдут как список, и последний в – c.

Если у нас один элемент со звездочкой и ни одного без звездочки – мы должны после него указать запятую (признак кортежа).

Не важно в какие скобки мы обернем верхний уровень: в круглые, квадратные или в никакие. Обычно скобки опускают.

На одном уровне может быть только 1 элемент со звездой.

>>> *n1, x, *n2 = numbers   # так нельзя!
  File "<stdin>", line 1
SyntaxError: two starred expressions in assignment

На нескольких уровней могут быть свои звездные выражения:

>>> a, [x, y, *rest], *others, last = 100, [20, 30, 40, 50, 60], 120, 140, 160
>>> print(a, x, y, rest, others, last)
100 20 30 [40, 50, 60] [120, 140] 160

При таком присваивании значения переменных копируются.

Справа от звездного присваиванию могут быть любые итераблы, например range:

>>> x1, *middle, x2 = range(10)
>>> x1, middle, x2
(0, [1, 2, 3, 4, 5, 6, 7, 8], 9)

Пока это все применения звездочки в Python, которые мне удалось вспомнить или найти.

Специально для канала PyWay.