in

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

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

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

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

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

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

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

>>> 5 * 5
25

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

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

>>> '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]]

Что?

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

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

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

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

  • Контейнер: структуры, содержащие примитивные типы данных (например, числа и строки) и другие контейнеры. Списки, кортежи и словари являются примерами контейнеров в 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 является объектом.

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

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

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

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

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

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.

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

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

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

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

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

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

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

Давайте воспользуемся dis модулем Python, чтобы разобраться во всем. Он расшифровывается как “дизассемблер” и используется для получения байт-кода 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

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

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

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

Автор истории Аюш @айн.

What do you think?

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

GIPHY App Key not set. Please check settings