Внутри Python: изучение языковой механики с помощью оператора Star

Внутри Python: изучение языковой механики с помощью оператора Star

20 декабря 2023 г.

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

Оператор звездочки или звездочки (*) можно использовать не только для умножения в Python. Правильное его использование может сделать ваш код более понятным и идиоматичным.

Где используется

Числовое умножение

Для полноты картины я уберу умножение. Самый простой пример — умножение двух чисел:

>>> 5 * 5
25

Повторяющиеся элементы

Помимо арифметики, мы можем использовать оператор звезды для повторения символов в строке:

>>> 'a' * 3
'aaa'
>>> 'abc' * 2
'abcabc'

Или для повторяющихся элементов в списках или кортежах:

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

Однако нам следует быть осторожными (или даже избегать) повторяющихся изменяемых элементов (например, списков). Для иллюстрации:

>>> x = [[3, 4]] * 2
>>> print(x)
[[3, 4], [3, 4]]

Все идет нормально. Но давайте попробуем извлечь элемент из второго списка.

>>> x[1].pop()
4
>>> print(x)
[[3], [3]]

Что?

Когда мы повторяем элементы с помощью оператора звезды, разные повторяющиеся элементы относятся к одному и тому же базовому объекту. Это нормально, когда элемент является неизменяемым, поскольку по определению мы не можем его изменить. Но, как мы видели выше, это может привести к проблемам с изменяемыми элементами. Лучшим способом повторения изменяемых элементов является понимание списка:

>>> x = [[3, 4] for _ in range(2)]
>>> x[1].pop()
4
>>> print(x)
[[3, 4], [3]]

Распаковка элементов

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

* Контейнер: структуры, содержащие примитивные типы данных (например, числа и строки) и другие контейнеры. Списки, кортежи и словари являются примерами контейнеров в Python. * Итерируемый: официальный глоссарий Python определяет итерируемый объект как «объект, способный возвращать его члены по одному». В эту категорию попадает любой объект, элементы которого можно перебирать с помощью цикла for. Таким образом, списки, кортежи, словари, строки и диапазон являются примерами итераций.

Проще говоря, распаковка — это извлечение элементов из итерируемого объекта в контейнер. Основываясь на этом определении, попробуйте угадать вывод следующего фрагмента:

>>> x = [*[3, 5], 7]
>>> print(x)

Здесь внутренняя итерация представляет собой список с 3 и 5, который находится внутри внешнего списка (контейнера). Извлечение элементов внутреннего списка во внешний список дает нам:

>>> print(x)
[3, 5, 7]

В итерируемом списке нет ничего особенного. Еще несколько примеров:

>>> [1, 2, *range(4, 9), 10]
[1, 2, 4, 5, 6, 7, 8, 10]
>>> (1, *(2, *(3, *(4, 5, 6))))
(1, 2, 3, 4, 5, 6)

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

>>> *[1, 2]
  File "<stdin>", line 1
SyntaxError: can't use starred expression here

Расширенная итеративная распаковка

Расширенная итеративная распаковка звучит сложно, но на практике все просто. Предположим, вы хотите написать функцию для извлечения всех элементов итерации, кроме первого, а затем возвращать выходные данные в виде списка. Не используя расширенную итерируемую распаковку (мы доберемся до этого через минуту), вы можете написать что-то вроде этого:

def all_but_first(seq):
    it = iter(seq)
    next(it)
    return [*it]

Давайте проверим это:

>>> all_but_first(range(1, 5))
[2, 3, 4]

Идеальный. Теперь воспользуемся расширенной итеративной распаковкой.

def all_but_first(seq):
    first, *rest = seq
    return rest

Очень чистый! И если вы проверите это, вы увидите, что эта функция эквивалентна предыдущей.

:::информация Есть еще много вещей, для которых * используется в Python, например, принятие переменного количества аргументов в функциях (например, def f(*args):). Но мне не хотелось делать статью слишком длинной.

:::

За кулисами

Как один и тот же оператор (*) выполняет столько разных функций? Чтобы понять это, нам нужно углубиться в Python. Помните, что все в Python является объектом.

Если вы не знакомы с парадигмой объектно-ориентированного программирования, вы можете думать об объектах как о сущностях, которые имеют свойства (называемые атрибутами) и могут выполнять действия (называемые методами), во многом подобно реальным объектам.

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

Учитывая, что все является объектом, мы готовы понять, как оператор звездочка работает для умножения и повторения элементов.

Умножение и повторяющиеся элементы

В Python классы имеют специальные предопределенные методы «двойного подчеркивания». Самым известным из них, вероятно, является метод __init__, используемый для инициализации объектов. Их еще называют дандер или магическими методами. Их называют магическими методами, потому что они вызываются за кулисами и почти никогда напрямую. Например, рассмотрим следующий класс:

class Doggo:
    def __init__(self, name):
        self.name = name

    def __call__(self):
        print(f"I am {self.name}.")

>>> oreo = Doggo("Oreo")
>>> kitkat = Doggo("Kit Kat")

Создание экземпляра объекта Doggo вызывает метод __new__ (для создания объекта) и метод __init__ (для инициализации объекта) «за кулисами». А __call__ — это волшебный метод, который позволяет мне делать следующее:

>>> oreo()
I am Oreo.
>>> kitkat()
I am Kit Kat.

Это то же самое, что

>>> oreo.__call__()
I am Oreo.
>>> kitkat.__call__()
I am Kit Kat.

Прохладный! И, как вы уже догадались, у оператора звезды также есть магический метод: __mul__. Следующие два идентичны:

>>> 25 * 4
100
>>> (25).__mul__(4)
100

Таким образом, разные объекты ведут себя по-разному, когда к ним используется оператор звезды, поскольку базовый магический метод __mul__ имеет разные определения в соответствующем классе. Для строк и списков:

>>> 'bana'.__mul__(3)
'banabanabana'
>>> [2].__mul__(4)
[2, 2, 2, 2]

Распаковка и расширенная итеративная распаковка

Хотя __mul__ объясняет магию умножения и повторения элементов, он не объясняет распаковку или расширенную итерируемую распаковку.

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

Давайте воспользуемся модулем Python dis, чтобы разобраться. Оно означает «дизассемблер» и используется для получения байт-кода Python из кода. Глоссарий Python определяет байт-код Python как «внутреннее представление программы Python в интерпретаторе CPython». " Хорошей аналогией является ассемблерный код для C. Вы поймете, что я имею в виду.

>>> import dis
>>> dis.dis('[1, *(2, 3)]')
  1           0 LOAD_CONST               0 (1)
              2 BUILD_LIST               1
              4 LOAD_CONST               1 ((2, 3))
              6 LIST_EXTEND              1
              8 RETURN_VALUE

Это показывает, что список [1] сначала создается, а затем расширяется с помощью (2, 3). Что-то похожее на:

>>> l = [1]
>>> l.extend((2, 3))
>>> print(l)
[1, 2, 3]

Это объясняет, почему мы можем выполнять распаковку только внутри контейнеров — снаружи контейнеров расширять будет нечего.

Что касается расширенной итеративной распаковки, для этого существует специальная инструкция байт-кода под названием UNPACK_EX. Для иллюстрации:

>>> dis.dis('a, *b = [1, 2, 3]')
  1           0 BUILD_LIST               0
              2 LOAD_CONST               0 ((1, 2, 3))
              4 LIST_EXTEND              1
              6 UNPACK_EX                1
              8 STORE_NAME               0 (a)
             10 STORE_NAME               1 (b)
             12 LOAD_CONST               1 (None)
             14 RETURN_VALUE

Заключительные мысли

Операторы-звездочки открывают нам дверь во внутреннюю работу Python. Пытаясь понять, как это работает, мы узнали, что в Python все является объектом. Мы узнали, что эти объекты имеют специальные «магические» методы, такие как __call__ и __mul__, которые позволяют добавлять поведение, такое как вызов этого объекта (как если бы это была функция) или использование * для выполнения таких действий, как умножение или повторение. Наконец, мы также коснулись модуля dis и байт-кода Python.

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


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE