24.01.2019 13:45 CPython 3.8
Символ “звездочка” (*), известный также как “астериск” (не путать с галлом Астериксом), встречается довольно часто в Python-коде. Сегодня мы попробуем разобраться в каких ситуациях он используется и почему.
Начнем с самого простого, а именно с умножения чисел:
>>> 2 * 3
6
>>> 1.5 * 2.5
3.75
>>> 3 * 5.5 * 4
66.0
Продолжаем с числами… Если одна звездочка (*) – умножение, то две звездочки (**) – это возведение в степень:
>>> 2 ** 8
256
>>> 5.5 ** 2
30.25
>>> 2 ** 5.5
45.254833995939045
Астериск также позволяет создавать последовательности из повторяющихся элементов.
Рассмотрим пример со строкой (str
):
>>> 'foo' * 3
'foofoofoo'
Такая операция допустима и для других типов.
Пример с кортежем (tuple
):
>>> (0, 1) * 2
(0, 1, 0, 1)
Со списком (list
):
>>> [0, 1] * 2
[0, 1, 0, 1]
С последовательностью байт (bytes
и bytearray
):
>>> b'foo' * 3
b'foofoofoo'
>>> bytearray(b'foo') * 3
bytearray(b'foofoofoo')
Мы видим, что во всех случаях создается новый объект с таким же типом, как у исходного объекта. Обратите внимание, что новый объект – это, по сути, совокупность ссылок (а не копий!) на исходный объект:
>>> spam = [[]] * 2
>>> spam
[[], []]
>>> spam[0].append(1)
>>> spam
[[1], [1]]
В приведенном выше примере исходным объектом является список, единственным элементом которого является другой список (пустой). Новый полученный объект spam
– это совокупность двух ссылок на один и тот же объект (тот самый пустой внутренний список).
Количество повторов задается только целыми числами (int
). Также допускается использование нуля: в этом случае результатом будет пустая последовательность:
>>> 'foo' * 0
''
>>> (0, 1) * 0
()
>>> [0, 1] * 0
[]
Если указать единицу, то будет создана копия:
>>> spam = [1, 2, 3]
>>> eggs = spam * 1
>>> spam == eggs
True
>>> spam is eggs
False
С отрицательными числами дело обстоит также, как и с нулем:
>>> 'foo' * -1
''
Астериск используется также в ситуациях, когда требуется передать переменное количество аргументов функции.
Рассмотрим простую функцию вычитания двух чисел:
def sub(x, y):
return x - y
Функция sub()
имеет два аргумента: x
– уменьшаемое и y
– вычитаемое. Возвращает функция разность:
>>> sub(25, 5)
20
Значения 25
и 5
соответствуют позициям аргументов x
и y
. Поэтому x=25
, а y=5
. Впрочем, можно нарушить этот порядок, указывая имена аргументов явно:
>>> sub(y=5, x=25)
20
Конечно, мы можем использовать имена аргументов и не меняя порядка:
>>> sub(x=25, y=5)
20
Итак, в Python мы можем передавать аргументы функции по позиции и по имени. В первом случае аргументы обычно называются позиционными (positional arguments
), а во втором – именованными (keyword arguments
).
В ситуации, когда требуется передать переменное количество аргументов и выходит на сцену наш главный герой – астериск.
Добавим еще одну функцию, которая позволит нам вычитать произвольное количество чисел:
from functools import reduce
def sub(x, y):
return x - y
def multisub(*args):
return reduce(sub, args)
Конструкция *args
и указывает на то, что ожидается переменное количество аргументов:
>>> multisub(25, 5)
20
>>> multisub(25, 5, 5)
15
args
– общепринятое имя в Python-сообществе, используемое в подобных ситуациях. Но, конечно, мы могли бы использовать другие имена, отличные от args
:
def multisub(*numbers):
return reduce(sub, numbers)
Такая передача аргументов похожа на описанную в самом начале передачу аргументов по позиции. Все передаваемые значения “упаковываются” в кортеж args
(или numbers
, как в последнем примере):
>>> args
(25, 5, 5)
Примечание
Для работы с этим кортежем я использую функцию reduce()
из модуля functools
. Подробнее о reduce()
можно почитать в статье Функции reduce, map и zip в Python.
Давайте немного сократим код:
from functools import reduce
from operator import sub
def multisub(*args):
return reduce(sub, args)
Здесь я использую функцию sub()
из модуля operator
, которая заменяет оператор вычитания -
(“минус”). В этом случае не нужно писать собственную функцию вычитания двух чисел.
С *args
допускается передача аргументов в количестве от нуля до условной бесконечности. Но нам могут быть интересны только ситуации, когда функции передаются хотя бы два числа. Поправим код:
from functools import reduce
from operator import sub
def multisub(x, y, *args):
return reduce(sub, args, x - y)
Не только наши старые аргументы x
и y
вернулись, но и *args
остался при деле! В Python допускается подобное сочетание.
Теперь, для корректного вызова функции требуется задать как минимум два числа:
>>> multisub(25, 5) # корректный вызов (два аргумента)
20
>>> multisub(25, 5, 5) # тоже корректный вызов (больше двух аргументов)
15
>>> multisub(25) # а вот так делать нельзя (меньше двух аргументов)
Traceback (most recent call last):
File "<input>", line 1, in <module>
multisub(25)
TypeError: multisub() missing 1 required positional argument: 'y'
Как мы помним из предыдущих примеров, аргументы можно передавать используя имена:
>>> multisub(x=25, y=5)
20
Можно сочетать передачу аргументов по позиции и по имени, но есть одно условие: именованные аргументы должны следовать за позиционными. Вот пример неправильного вызова функции:
>>> multisub(x=25, 5)
И вот такое использование тоже недопустимо (ведь *args
– это тоже позиционные аргументы):
>>> multisub(x=25, y=5, 5)
В обоих случаях будет сгенерировано исключение:
File "<input>", line 1
SyntaxError: positional argument follows keyword argument
Рассмотрим еще один пример. Предположим, нам нужно написать подпрограмму, которая будет формировать список атрибутов для HTML-элемента img
:
<img src="logo.png" alt="My logo">
Здесь src
и alt
являются атрибутами img
. Оба этих атрибута считаются обязательными, но могут быть и опциональные, такие как width
и height
, например. Наша подпрограмма должна уметь принимать произвольное количество аргументов и возвращать готовую строку со списком атрибутов.
Решим сначала вопрос с обязательными атрибутами:
def img(src, alt):
return f'<img src="{src}" alt="{alt}">'
Примечание
Обратите внимание на префикс f
перед открывающей кавычкой строки. Это признак так называемой f-строки, которые появились в версии Python 3.6. f-строки позволяют нам подойти к форматированию строк немного с другой стороны: такая запись короче и нагляднее, чем аналогичная по сути запись с использованием метода format()
. На этапе выполнения программы вместо {src}
и {alt}
будут подставлены значения соответствующих переменных.
Итак, попробуем в деле нашу функцию:
>>> img('logo.png', 'My logo')
'<img src="logo.png" alt="My logo">'
Похоже, что начало положено! Теперь нам необходимо добавить поддержку опциональных атрибутов для img
. Как и с позиционными аргументами, в Python допускается передача переменного количества именованных аргументов. В этом случае используется двойной астериск (**), а традиционным именем здесь является kwargs
(от keyword arguments
).
Дополним нашу функцию:
def img(src, alt, **kwargs):
extra_attrs = ' '.join(f'{k}="{v}"' for k, v in kwargs.items())
return f'<img src="{src}" alt="{alt}" {extra_attrs}>'
Пробуем передать дополнительные атрибуты:
>>> img('logo.png', 'My logo', width=256, height=256)
'<img src="logo.png" alt="My logo" width="256" height="256">'
Все работает!
По сути своей kwargs
– это словарь (dict
), который содержит все переданные именованные аргументы:
>>> kwargs
{'width': 256, 'height': 256}
В теле функции img()
я вызываю метод словаря items()
, который возвращает итератор. С помощью for
и items()
я постепенно обхожу словарь, на каждом шаге цикла получая пару ключ-значение (k
и v
).
Такая “встроенная” конструкция for
позволяет создать генератор, который в свою очередь будет передан методу строки join()
. Этот генератор будет возвращать строки в нужном нам формате (f'{k}="{v}"'
). В данной ситуации их будет всего две: сначала width="256"
, потом height="256"
. С помощью join()
можно как бы склеивать элементы итерируемых объектов. Сама строка здесь – это просто пробельный символ (' '
); он и будет нашим “склеивающим веществом”.
Итак, join()
постепенно “истощает” наш генератор и в итоге возвращает новую “склеенную” строку: width="256" height="256"
.
Примечание
Некоторые атрибуты HTML-элементов могут не иметь значений. Наша функция пока не готова принимать такие атрибуты, но реализация данного функционала выходит за рамки данной статьи.
Стоит заметить, что конструкции типа *args
и **kwargs
позволяют нам не только “упаковывать” передаваемые аргументы, но и “распаковывать” их:
>>> attrs = ('logo.png', 'My logo')
>>> extra_attrs = {'width': 256, 'height': 256}
>>> img(*attrs, **extra_attrs) # аналогично img('logo.png', 'My logo', width=256, height=256)
'<img src="logo.png" alt="My logo" width="256" height="256">'
Впрочем, это касается не только аргументов функций:
>>> *a, = 1, 2, 3, 4, 5
>>> a
[1, 2, 3, 4, 5]
>>> *a, b = 1, 2, 3, 4, 5
>>> a
[1, 2, 3, 4]
>>> b
5
>>> a, *b = 1, 2, 3, 4, 5
>>> a
1
>>> b
[2, 3, 4, 5]
Символ звездочки также может пригодиться в ситуациях, когда нужно обязать того, кто будет вызывать функцию использовать только именованные аргументы. В определенных ситуациях это может помочь пользователю вашей подпрограммы избежать ряда ошибок и повысить читабельность кода.
Сделаем alt
таким обязательным именованным аргументом:
def img(src, *, alt, **kwargs):
extra_attrs = ' '.join(f'{k}="{v}"' for k, v in kwargs.items())
return f'<img src="{src}" alt="{alt}" {extra_attrs}>'
Все аргументы объявленные после *
должны передаваться исключительно по имени:
>>> img('logo.png', 'My logo', width=256, height=256) # некорректный вызов (забыт alt)
Traceback (most recent call last):
File "<input>", line 1, in <module>
img('logo.png', 'My logo', width=256, height=256)
TypeError: img() takes 1 positional argument but 2 were given
>>> img('logo.png', alt='My logo', width=256, height=256) # корректный вызов (alt присутствует)
'<img src="logo.png" alt="My logo" width="256" height="256">'
Не по теме
Начиная с версии Python 3.8 пользователя подпрограммы можно обязать передавать аргументы только по позиции, используя символ “/”. Более подробно см. здесь.
Python позволяет переопределять операторы такие как +
, -
, \
, *
и проч. Для этого используются специальные методы (так называемые “магические методы”) с двойным символом нижнего подчеркивания (double under
или сокращенно – dunder
).
Предположим, нам требуется реализовать возможность поэлементного умножения списков. То есть мы хотели бы видеть примерно такое:
>>> [1, 2] * [3, 4]
[3, 8]
Но вот что произойдет в действительности, если попытаться перемножить два списка:
>>> [1, 2] * [3, 4]
Traceback (most recent call last):
File "<input>", line 1, in <module>
[1, 2] * [3, 4]
TypeError: can't multiply sequence by non-int of type 'list'
То есть список можно “умножить” только на целое число, которое, как мы помним, указывает на количество повторов:
>>> [0, 1] * 2
[0, 1, 0, 1]
Если стандартный список нам не подходит, то давайте напишем собственный класс списка:
from operator import mul
class MyList(list):
def __mul__(self, other):
if isinstance(other, int):
return super().__mul__(other)
return MyList(map(mul, self, other))
Примечание
В теле __mul__()
я использовал map()
, которая вызывает функцию (она передается map()
первым аргументом) для каждой группы элементов указанных итерируемых объектов (второй и последующий аргументы). Подробнее о map()
можно почитать в статье Функции reduce, map и zip в Python.
Родителем нашего класса MyList
является list
. Это значит, что мы автоматически получаем доступ к его атрибутам и методам. Благодаря переопределенному методу __mul__()
сейчас можно осуществить операцию поэлементного умножения, но при этом остается возможность умножать и на целые числа (int
), как это было раньше (для этого мы вызываем родительский метод: super().__mul__(other)
):
>>> MyList([1, 2]) * [3, 4]
[3, 8]
>>> MyList([1, 2]) * 2
[1, 2, 1, 2]
Все работает, но если поменять операнды местами, то мы снова получим уже знакомую ошибку:
>>> [3, 4] * MyList([1, 2])
Traceback (most recent call last):
File "<input>", line 1, in <module>
[3, 4] * MyList([1, 2])
TypeError: 'list' object cannot be interpreted as an integer
Это все потому, что __mul__()
вызывается только в ситуациях, когда наш список находится слева от оператора *
. Чтобы исправить положение, нам нужно переопределить еще один метод – __rmul__()
:
from operator import mul
class MyList(list):
def __mul__(self, other):
if isinstance(other, int):
return super().__mul__(other)
return MyList(map(mul, self, other))
__rmul__ = __mul__
Выше я просто присвоил __rmul__()
ссылку на __mul__()
, чтобы избежать дублирования кода: обе функции, в данном случае, абсолютно идентичны.
Смотрим на результат:
>>> MyList([1, 2]) * [3, 4] # наш класс может быть слева и тогда вызывается __mul__()...
[3, 8]
>>> [3, 4] * MyList([1, 2]) # ...или справа и тогда вызывается __rmul__()
[3, 8]
Напоследок попробуем реализовать поддержку списков разной длины, которой у нас пока что нет:
>>> MyList([1, 2]) * [3, 4, 5]
[3, 8]
Обратите внимание, что последний элемент списка справа от астериска (5
) просто “потерялся”. Исправим этот недостаток:
from itertools import starmap, zip_longest
from operator import mul
class MyList(list):
def __mul__(self, other):
if isinstance(other, int):
return super().__mul__(other)
return MyList(starmap(mul, zip_longest(self, other, fillvalue=1)))
__rmul__ = __mul__
Я заменил map()
на starmap()
, а также добавил новую функцию zip_longest()
.
Проверяем:
>>> MyList([1, 2]) * [3, 4, 5]
[3, 8, 5]
Результирующий список [3, 8, 5]
получается путем перемножения элементов списков MyList([1, 2])
и [3, 4, 5]
, а недостающий элемент списка слева заменяется единицей: [1*3, 2*4, 1*5] == [3, 8, 5]
.
Примечание
Конечно, здесь открывается большой простор для улучшения нашего класса, но это уже выходит за рамки данной статьи.
На этом я пока ставлю точку. Возможно, в будущих статьях я подробнее остановлюсь на каких-то вопросах, затронутых в этой статье. Но на сегодня у меня все. До скорых встреч!
Дата последнего редактирования статьи: 28.04.2020 15:25