Как реализовано numpy fancy indexing?

Я немного экспериментировал с 2D-списками и массивами numpy. Из этого я поднял 3 вопроса, которым мне очень интересно знать ответ.

Во-первых, я инициализировал 2D-список python.

>>> my_list = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

Затем я попытался индексировать список с помощью кортежа.

>>> my_list[:,]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: list indices must be integers, not tuple

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

Затем я попытался преобразовать список в массив numpy и сделать то же самое.

>>> np.array(my_list)[:,]
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

Конечно без проблем. Я понимаю, что один из __xx__() был переопределен и реализован в пакете numpy.

Также поддерживается индексирование индексирования:

>>> np.array(my_list)[:,[0, 1]]
array([[1, 2],
       [4, 5],
       [7, 8]])

Это подняло пару вопросов:

  1. Какой метод __xx__ имеет numpy переопределен/определен для обработки фантазии индексации?
  2. Почему списки python изначально не поддерживают причудливую индексацию?

(Бонусный вопрос: почему мои тайминги показывают, что резка в python2 медленнее, чем python3?)

Ответы

Ответ 1

У вас есть три вопроса:

1. Какой метод __xx__ имеет numpy overridden/defined для обработки причудливой индексации?

Оператор индексирования [] можно переопределить с помощью __getitem__, __setitem__ и __delitem__. Это может быть интересно написать простой подкласс, который предлагает некоторую интроспекцию:

>>> class VerboseList(list):
...     def __getitem__(self, key):
...         print(key)
...         return super().__getitem__(key)
...

Сначала сделайте пустую:

>>> l = VerboseList()

Теперь заполните его некоторыми значениями. Обратите внимание, что мы не переопределили __setitem__, поэтому ничего интересного не произошло:

>>> l[:] = range(10)

Теперь давайте получим элемент. При индексе 0 будет 0:

>>> l[0]
0
0

Если мы попытаемся использовать кортеж, мы получим ошибку, но мы сначала увидим кортеж!

>>> l[0, 4]
(0, 4)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in __getitem__
TypeError: list indices must be integers or slices, not tuple

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

>>> l[1:3]
slice(1, 3, None)
[1, 2]

Есть много интересных вещей, которые вы можете сделать с этим объектом - попробуйте!

2. Почему списки python изначально не поддерживают фантастическое индексирование?

Трудно ответить. Один из способов думать об этом - исторический: потому что разработчики numpy сначала подумали об этом.

Вы, молодые люди. Когда я был ребенком...

После первого публичного выпуска в 1991 году у Python не было библиотеки numpy, и для создания многомерного списка вам нужно было создать структуры списка. Я предполагаю, что ранние разработчики - в частности, Guido van Rossum (GvR) - считали, что сохранить вещи просто было лучше всего, изначально. Индексация фрагментов уже была довольно мощной.

Однако, не слишком долго после этого, интерес к Python приобрел научный компьютерный язык. В период с 1995 по 1997 год ряд разработчиков сотрудничали в библиотеке под названием numeric, раннем предшественнике numpy. Хотя он не был основным вкладчиком в numeric или numpy, GvR координировался с разработчиками numeric, расширяя синтаксис фрагмента Python способами, упрощающими индексирование многомерных массивов. Позже появилась альтернатива numeric, названная numarray; и в 2006 году был создан numpy, включающий лучшие возможности обоих.

Эти библиотеки были мощными, но требовали больших расширений c и так далее. Работа с ними в базовом дистрибутиве Python сделала бы его громоздким. И хотя GvR немного улучшил синтаксис фрагментов, добавление причудливой индексации в обычные списки сильно изменило бы их API - и несколько избыточно. Учитывая, что причудливое индексирование может иметься уже с внешней библиотекой, это преимущество не стоило затрат.

Части этого повествования являются спекулятивными, честно говоря. 1 Я не знаю разработчиков! Но это то же решение я бы сделал. Фактически...

Это действительно так.

Хотя фантастическая индексация очень мощная, я рад, что она не является частью ванильного Python даже сегодня, потому что это означает, что вам не нужно много думать при работе с обычными списками. Для многих задач вам это не нужно, и навязываемая им когнитивная нагрузка значительна.

Имейте в виду, что я говорю о нагрузке, наложенной на читателей и сопровождающих. Вы можете быть гениальцем, который может делать 5-ти тензорные продукты в вашей голове, но другие люди должны прочитать ваш код. Сохранение фантазии индексирования в numpy означает, что люди не используют его, если они им не нужны, что делает код более читабельным и поддерживаемым в целом.

3. Почему индексирование numpy очень медленное на python2? Это потому, что у меня нет встроенной поддержки BLAS для numpy в этой версии?

Возможно. Это определенно зависит от окружающей среды; Я не вижу такой же разницы на моей машине.


1. Части повествования, которые не являются спекулятивными, взяты из краткой истории рассказанной в специальном выпуске "Вычислительные технологии в науке и технике" (2011 vol. 13).

Ответ 2

Какой метод __xx__ имеет numpy переопределен/определен для обработки фантазии индексации?

__getitem__ для извлечения, __setitem__ для назначения. Это будет __delitem__ для удаления, за исключением того, что массивы NumPy не поддерживают удаление.

(Все написано на C, однако, то, что они реализовали на уровне C, было mp_subscript и mp_ass_subscript, а __getitem__ и __setitem__ обертки были предоставлены PyType_Ready. __delitem__ тоже, даже хотя удаление не поддерживается, потому что __setitem__ и __delitem__ отображаются на mp_ass_subscript на уровне C.)

Почему списки python не поддерживают фэнтезийную индексацию?

Списки Python представляют собой принципиально одномерные структуры, а массивы NumPy - произвольные. Многомерная индексация имеет смысл только для многомерных структур данных.

У вас может быть список со списками как элементы, например [[1, 2], [3, 4]], но список не знает и не заботится о структуре его элементов. Поддержка индексирования индексов l[:, 2] для индексации потребует, чтобы список знал о многомерной структуре таким образом, чтобы списки не были предназначены. Это также добавит много сложностей, много ошибок обработки и множество дополнительных дизайнерских решений - насколько глубокой должна быть копия l[:, :]? Что произойдет, если структура оборвана или непоследовательно вложена? Должна ли многомерная индексация регистрироваться в элементах, не входящих в список? Что бы сделать del l[1:3, 1:3]?

Я видел реализацию индексации NumPy и дольше, чем вся реализация списков. Здесь его часть. Не стоит делать это в списках, когда массивы NumPy удовлетворяют всем действительно привлекательным случаям использования, в которых вы нуждаетесь.

Почему индексирование numpy очень медленное на python2? Это потому, что у меня нет встроенной поддержки BLAS для numpy в этой версии?

Индексирование NumPy не является операцией BLAS, так что это не так. я не может воспроизводить такие драматические разницы во времени, и различия, которые я вижу, выглядят как небольшие оптимизации Python 3, возможно, немного более эффективное распределение кортежей или фрагментов. То, что вы видите, вероятно, связано с различиями версий NumPy.

Ответ 3

my_list[:,] переводится интерпретатором в

my_list.__getitem__((slice(None, None, None),))

Как вызов функции с *args, но она заботится о переводе нотации : в объект slice. Без , он просто передаст slice. С помощью , он передает кортеж.

Список __getitem__ не принимает кортеж, как показывает ошибка. Массив __getitem__ делает. Я считаю, что возможность передавать кортеж и создавать объекты среза была добавлена ​​как удобство для numpy (или его предицессоров). Обозначение кортежа никогда не было добавлено в список __getitem__. (Существует класс operator.itemgetter, который допускает форму расширенной индексации, но внутренне это просто итератор кода на Python.)

С помощью массива вы можете напрямую использовать кодировку:

In [490]: np.arange(6).reshape((2,3))[:,[0,1]]
Out[490]: 
array([[0, 1],
       [3, 4]])
In [491]: np.arange(6).reshape((2,3))[(slice(None),[0,1])]
Out[491]: 
array([[0, 1],
       [3, 4]])
In [492]: np.arange(6).reshape((2,3)).__getitem__((slice(None),[0,1]))
Out[492]: 
array([[0, 1],
       [3, 4]])

Посмотрите на файл numpy/lib/index_tricks.py, например, на забавный материал, который вы можете сделать с помощью __getitem__. Вы можете просмотреть файл с помощью

np.source(np.lib.index_tricks)

Вложенный список - это список списков:

Во вложенном списке подписи не зависят от содержащего списка. Контейнер имеет указатели на объекты в другом месте:

In [494]: my_list = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
In [495]: my_list
Out[495]: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
In [496]: len(my_list)
Out[496]: 3
In [497]: my_list[1]
Out[497]: [4, 5, 6]
In [498]: type(my_list[1])
Out[498]: list
In [499]: my_list[1]='astring'
In [500]: my_list
Out[500]: [[1, 2, 3], 'astring', [7, 8, 9]]

Здесь я меняю второй элемент my_list; он больше не является списком, а строкой.

Если я применил [:] к списку, я просто получу мелкую копию:

In [501]: xlist = my_list[:]
In [502]: xlist[1] = 43
In [503]: my_list           # didn't change my_list
Out[503]: [[1, 2, 3], 'astring', [7, 8, 9]]
In [504]: xlist
Out[504]: [[1, 2, 3], 43, [7, 8, 9]]

но изменение элемента списка в xlist меняет соответствующий подписок в my_list:

In [505]: xlist[0][1]=43
In [506]: my_list
Out[506]: [[1, 43, 3], 'astring', [7, 8, 9]]

Мне это показывает n-мерное индексирование (как реализовано для массивов numpy) не имеет смысла с вложенными списками. Вложенные списки являются многомерными только в той мере, в какой позволяют их содержимое; в них нет ничего структурного или синтаксически многомерного.

тайминги

Использование двух [:] в списке не делает глубокую копию или не работает по вложенности. Он просто повторяет шаг мелкой копии:

In [507]: ylist=my_list[:][:]
In [508]: ylist[0][1]='boo'
In [509]: xlist
Out[509]: [[1, 'boo', 3], 43, [7, 8, 9]]

arr[:,] просто делает a view of arr. Разница между view и copy является частью понимания разницы между базовой и расширенной индексацией.

Итак, alist[:][:] и arr[:,] - разные, но основные способы создания какой-то копии списков и массивов. Ничего не вычисляет, и не выполняет итерации через элементы. Поэтому сравнение времени не говорит нам о многом.