Рубрики
Без рубрики

Сделать Целые числа Python Итеративными

Итерабли в Python-это объекты и контейнеры, которые можно шагать по одному элементу за раз, обычно используя for … в петле. Например, не все объекты могут быть повторены – мы не можем повторять an…

Автор оригинала: 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 Кэширует целые числа .

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

Вы всегда можете найти меня в твиттере @arpit_bhayani .