Pixamp

Астериск в Python

24.01.2019 13:45 CPython 3.8

The article image

Символ “звездочка” (*), известный также как “астериск” (не путать с галлом Астериксом), встречается довольно часто в 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

Следующая статья › Функции reduce, map и zip в Python

Главная страница