Как реализовано 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]])
Это подняло пару вопросов:
- Какой метод
__xx__
имеет numpy переопределен/определен для обработки фантазии индексации? - Почему списки 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[:,]
- разные, но основные способы создания какой-то копии списков и массивов. Ничего не вычисляет, и не выполняет итерации через элементы. Поэтому сравнение времени не говорит нам о многом.