Автор оригинала: Arpit Bhayani.
Итерабли в Python-это объекты и контейнеры, которые можно шагать по одному элементу за раз, обычно используя for ... в цикле
. Например, не все объекты могут быть повторены – мы не можем повторять целое число, это единственное значение. Лучшее, что мы можем сделать здесь, – это выполнить итерацию по диапазону целых чисел, используя тип range
, который помогает нам выполнить итерацию по всем целым числам в диапазоне [0, n)
.
Поскольку целые числа, индивидуально, не являются итеративными, когда мы пытаемся сделать для x в 7
, это вызывает исключение, указывающее TypeError: объект 'int' не является итеративным
. Итак, что, если мы изменим исходный код Python и сделаем целые числа итеративными , скажем, каждый раз, когда мы делаем для x в 7
, вместо того, чтобы вызывать исключение, оно фактически повторяет значения [0, 7)
. В этом эссе мы будем проходить именно через это, и вся повестка дня будет:
- Что такое итерация Python?
- Что такое протокол итератора?
- Изменение исходного кода Python и возможность итерации целых чисел, а также
- Почему это может быть плохой идеей?
Любой объект, который может быть повторен, является повторяемым в Python. Этот список должен быть самым популярным итерируемым, и он находит свое применение почти в каждом отдельном приложении Python – прямо или косвенно. Еще до выполнения первой пользовательской команды интерпретатор Python при загрузке уже создал 406
списки, для его внутреннего использования.
В приведенном ниже примере мы видим, как список a
повторяется с помощью for ... в цикле
и к каждому элементу можно получить доступ через переменную x
.
>>> a = [2, 3, 5, 7, 11, 13] >>> for x in a: print(x, end=" ") 2 3 5 7 11 13
Подобно list
, range
– это тип python, который позволяет нам перебирать целочисленные значения, начиная со значения start
и продолжая до end
, каждый раз переступая через step
значения. range
чаще всего используется для реализации C-подобного цикла for
в Python. В приведенном ниже примере цикл for
повторяется по диапазону , который начинается с
0 , идет до
7 с шагом
1 - создание последовательности
[0, 7) .
# The range(0, 7, 1) will iterate through values 0 to 6 and every time # it will increment the current value by 1 i.e. the step. >>> for x in range(0, 7, 1): print(x, end=" ") 0 1 2 3 4 5 6
Помимо list
и range
другие итерабельные являются – кортеж
, set
, frozenset
, str
, bytes
, bytearray
, memoryview
и dict
. Python также позволяет нам создавать пользовательские итеративные объекты, заставляя объекты и типы следовать протоколу Iterator .
Python, сохраняя простоту, определяет iterable как любой объект, который следует протоколу Iterator ; это означает, что объект или контейнер реализует следующие функции
____it__
должен возвращать объект итератора, реализовавший метод_ _ next__
__next__
должен вернуть следующий элемент итерации, и если элементы исчерпаны, то вызовите исключениеStopIteration
.
Итак, в сущности, __it__
– это то, что делает любой объект python итеративным; следовательно, чтобы сделать целые числа итеративными, нам нужно иметь __iter__
набор функций для целых чисел.
Наиболее известной и широко используемой реализацией Python является CPython , где ядро реализовано в чистом C. Поскольку нам нужно внести изменения в один из основных типов данных Python, мы изменим CPython, добавим __iter__
функцию к целочисленному типу и перестроим двоичный файл. Но прежде чем перейти к реализации, важно понять несколько основных принципов.
Объект PyTypeObject
Каждый объект в Python связан с типом, и каждый тип является экземпляром структуры с именем PyTypeObject . Новый экземпляр этой структуры фактически является новым типом в python. Эта структура содержит несколько метаданных и кучу указателей на функции C, каждый из которых реализует небольшой сегмент функциональности типа. Большинство из этих “слотов” в структуре являются необязательными, которые могут быть заполнены путем размещения соответствующих указателей функций и управления соответствующей функциональностью.
Слот tp_iter
Среди всех доступных слотов нас интересует слот tp_iter
, который может содержать указатель на функцию, возвращающую объект итератора. Этот слот соответствует функции __iter__
, которая эффективно делает объект итеративным. Значение, отличное от NULL
этого слота, указывает на итеративность. tp_iter
содержит функцию со следующей сигнатурой
PyObject * tp_iter(PyObject *);
Целые числа в Python не имеют фиксированного размера; скорее размер целого числа зависит от значения, которое оно содержит. Как Python реализует сверхдлинные целые числа – это отдельная история, но основную реализацию можно найти в longobject.c . Экземпляр PyTypeObject
, который определяет тип integer/long, является PyLong_Type
и имеет свой tp_iter
слот, установленный в 0
т. е. NULL
, который утверждает тот факт, что целые числа в python не являются итеративными.
PyTypeObject PyLong_Type = { ... "int", /* tp_name */ offsetof(PyLongObject, ob_digit), /* tp_basicsize */ sizeof(digit), /* tp_itemsize */ ... 0, /* tp_iter */ ... };
Это NULL
значение для tp_it
делает int
объект не итерационным, и, следовательно, если этот слот был занят соответствующим указателем функции с вышеупомянутой сигнатурой, это вполне может сделать любое целое число итерационным.
Теперь мы реализуем функцию tp_iter
для целочисленного типа, называя ее long_either
, которая возвращает объект итератора, как того требует соглашение. Основная функциональность, которую мы хотим реализовать здесь, заключается в следующем: когда целое число n
повторяется, оно должно повторяться через последовательность [0, n)
с шагом 1
. Это поведение очень близко к предопределенному типу range
, который перебирает диапазон целочисленных значений, более конкретно range
, который начинается с 0
, идет до n
с шагом 1
.
Мы определяем функцию полезности в rangeobject.c
, которая, учитывая целое число python, возвращает экземпляр longrangeiterobject
в соответствии с нашими спецификациями. Эта служебная функция создаст экземпляр longrangeiterobject
с запуском как 0
, заканчивающийся на длинном значении, указанном в аргументе, и шаг как 1
. Функция полезности приведена ниже.
/* * PyLongRangeIter_ZeroToN creates and returns a range iterator on long * iterating on values in the range [0, n). * * The function creates and returns a range iterator from 0 till the * provided long value. */ PyObject * PyLongRangeIter_ZeroToN(PyObject *long_obj) { // creating a new instance of longrangeiterobject longrangeiterobject *it; it = PyObject_New(longrangeiterobject, &PyLongRangeIter_Type); // if unable to allocate memoty to it, return NULL. if (it == NULL) return NULL; // we set the start to 0 it->start = _PyLong_Zero; // we set the step to 1 it->step = _PyLong_One; // we set the index to 0, since we want to always start from the first // element of the iteration it->index = _PyLong_Zero; // we set the total length of iteration to be equal to the provided value it->len = long_obj; // we increment the reference count for each of the values referenced Py_INCREF(it->start); Py_INCREF(it->step); Py_INCREF(it->len); Py_INCREF(it->index); // downcast the iterator instance to PyObject and return return (PyObject *)it; }
Функция полезности Py Long Range Inter_Zero To N
определена в rangeobject.c
и будет объявлена в rangeobject.h
, чтобы ее можно было использовать в CPython. Объявление функции в rangeobject.h
с использованием стандартных макросов Python выглядит следующим образом
PyAPI_FUNC(PyObject *) PyLongRangeIter_ZeroToN(PyObject *);
Функция, занимающая слот tp_iter
, получит объект self
в качестве входного аргумента и, как ожидается, вернет экземпляр итератора. Следовательно, функция long_iter
получит целочисленный объект python (self), который повторяется в качестве входного аргумента, и она должна вернуть экземпляр итератора. Здесь мы будем использовать функцию полезности Py Long Range Iter_ZeroToN
, которую мы только что определили, которая возвращает нам экземпляр итератора диапазона. Вся функция long_term
может быть определена как
/* * long_iter creates an instance of range iterator using PyLongRangeIter_ZeroToN * and returns the iterator instance. * * The argument to the `tp_iter` is the `self` object and since we are trying to * iterate an integer here, the input argument to `long_iter` will be the * PyObject of type PyLong_Type, holding the integer value. */ static PyObject * long_iter(PyObject *long_obj) { return PyLongRangeIter_ZeroToN(long_obj); }
Теперь, когда мы определили long_term
, мы можем поместить функцию в tp_iter
слот PyLong_Type
, который обеспечивает требуемую итеративность для целых чисел.
PyTypeObject PyLong_Type = { ... "int", /* tp_name */ offsetof(PyLongObject, ob_digit), /* tp_basicsize */ sizeof(digit), /* tp_itemsize */ ... long_iter, /* tp_iter */ ... };
Консолидированный поток
Как только у нас все будет на месте, весь поток пойдет следующим образом –
Каждый раз, когда целое число повторяется, используя любой метод итерации – например, for ... в
он проверит tp_iter
типа PyLongType
, и поскольку теперь он содержит указатель функции long_iter
, функция будет вызвана. Этот вызов вернет объект итератора типа longrangeiterobject
с фиксированными значениями начала, индекса и шага, что в терминах питона фактически является диапазоном |/(0, n, 1) . Следовательно,
для x в 7 по своей сути оценивается как
для x в диапазоне(0, 7, 1) , что позволяет нам перебирать целые числа.
Эти изменения также размещены в удаленной ветке cpython@02-long-term и запрос на вытягивание, содержащий diff
, можно найти здесь .
Как только мы создадим новый двоичный файл python с вышеупомянутыми изменениями, мы сможем увидеть повторяющиеся целые числа в действиях. Теперь , когда мы делаем для x в 7
, вместо того, чтобы вызывать исключение, оно фактически перебирает значения [0, 7)
.
>>> for i in 7: print(i, end=" "); 0 1 2 3 4 5 6 # Since integers are now iterable, we can create a list of [0, 7) using `list` # Internally `list` tries to iterate on the given object i.e. `7` # now that the iteration is defined as [0, 7) we get the list from # from iteration, instead of an exception >>> list(7) [0, 1, 2, 3, 4, 5, 6]
Хотя это кажется забавным и несколько полезным, иметь повторяющиеся целые числа, на самом деле это не очень хорошая идея. Основная причина этого заключается в том, что это делает распаковку непредсказуемой. Распаковка-это когда вы распаковываете итерацию и назначаете ее нескольким переменным. Например: a,, 4
присвоит 3 a и 4 b. Поэтому назначение a,
должно быть ошибкой, потому что есть только одно значение справа и несколько слева.
Распаковка рассматривает размер правой руки как итеративный и пытается выполнить итерацию по нему; и теперь, поскольку целые числа итеративны с правой стороны, после итерации получается 7 значений, которые левая сторона имеет всего 2 переменные; Следовательно, возникает исключение ValueError: слишком много значений для распаковки (ожидается 2)
.
Все будет работать просто отлично, если мы сделаем a,
, как сейчас, правая сторона, после итерации, имеет два значения, а левая сторона имеет две переменные. Таким образом, два очень похожих утверждения приводят к двум очень разным результатам, что делает распаковку непредсказуемой.
>>> a, b = 7 Traceback (most recent call last): File "", line 1, in ValueError: too many values to unpack (expected 2) >>> a, b = 2 >>> a, b 0, 1
В этом эссе мы изменили исходный код Python и сделали целые числа итеративными. Хотя это не очень хорошая идея, но забавно играть с кодом и вносить изменения в наш любимый язык программирования. Это помогает нам получить подробное представление о реализации ядра python и может проложить нам путь к тому, чтобы стать разработчиком ядра Python. Это одна из многих статей в серии Python Internals – Как python реализует сверхдлинные целые числа? и Python Кэширует целые числа .
- Python Кэширует Целые Числа
- Как python реализует сверхдлинные целые числа?
- Я изменил свой Python и сделал его сомнительным | Python Internals
- Построение конечных автоматов с помощью сопрограмм Python
- Персонализируйте запрос на python
Если вам понравилось то, что вы прочитали, подумайте о подписке на мою еженедельную рассылку по адресу arpitbhayani.me/newsletter были, раз в неделю, я пишу эссе о внутренних языках программирования, или глубокое погружение в какой-то суперумный алгоритм, или просто несколько советов по созданию масштабируемых распределенных систем.
Вы всегда можете найти меня в твиттере @arpit_bhayani .