Внутри 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.
Если есть что-то, что вы вынесете из этой статьи, то пусть это будет вот это. Погружение в механику конструкции языка программирования, которую вы уже используете, — отличный способ улучшить знание этого языка. н
Оригинал