Как выполнить (расширенное) индексированное локальное (расширенное) присвоение пустому вектору, используя только более сложные методы модели данных? - PullRequest
2 голосов
/ 01 ноября 2019

Я реализую узкий и ограниченный сценарий DSL с использованием python, и я хотел бы иметь возможность функционально сделать эквивалент следующего:

import numpy as np
a = np.arange(10)
a[ a > 5 ] += 42

=> array([ 0,  1,  2,  3,  4,  5, 48, 49, 50, 51])

Приведенный выше код работает, как и следовало ожидать. Если я начну расширять приведенный выше код, я получу следующий первый слой внутренних компонентов:

a[a>5].__iadd__(42)

, который также работает как ожидалось. Однако я не могу найти метод индексатора, который позволил бы мне работать с __iadd__ над самим массивом вместо его копии. Таким образом, неудивительно, что следующий код не делает то, что я хочу:

import numpy as np
a = np.arange(10)
a.__getitem__(a>5).__iadd__(42)

=> array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

Только если я делаю:

a.__setitem__(a>5, a.__getitem__(a>5).__iadd__(42))

мне кажется, что я получаю поведениеищет, но на данный момент это больше не является правильным оператором присваивания на месте, и что более важно, я индексирую дважды (один раз для чтения и один раз для записи).

Индекс Нампи page , по-видимому, подразумевает, что расширенное индексирование (т. Е. Индексирование, где список нижних индексов является ndarray) всегда возвращает копию. Означает ли это, что на самом деле a[a>5].__iadd__(42) всегда реализуется с использованием резервного метода? Я что-то упускаю, или это просто невозможно, или, по крайней мере, невозможно без магии переводчика?


Редактировать:

Таким образом, согласно ответу @donkopotamus, модель данных не позволяет нам сделать это за один снимок. Это отвечает на вопрос.

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

Вот «доказательство»:

import cython
import numpy as np

@cython.locals(arr="float[:]",
               mask="bint[:]",
               val=float,
               i=int)
@cython.boundscheck(False)
def func(arr,mask,val):
       for i in range(len(mask)):
               if mask[i]:
                        arr[i] += val

Этот код, когда он скомпилирован и синхронизирован, на медленнее , чем numpy на месте:

a = np.arange(1e6)

%%timeit
a[a%3==0] += 42

=> 40.5 ms ± 376 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

a = np.arange(1e6)

%%timeit
func(a, (a%3==0), 42)

=> 116 ms ± 2.76 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Таким образом, оператор REPL интерпретировалработает быстрее, чем 3-строчная функция Cython, которая в значительной степени копирует представление памяти с той скоростью, с которой это позволяет процессор.

На данном этапе ни одно из них больше не имеет никакого смысла. Я знаю, что numpy создан вручную для оптимизации операций векторизации, но я не понимаю, как он интегрируется с интерпретатором Python таким образом, который имеет смысл. Это кэширует пару BINARY_SUBSCR / STORE_SUBSCR?

@ donkopotamus, обратите внимание, что хотя операция индексации не вычисляется дважды, в коде Python она интерпретируется дважды в том смысле, что маска выполняетсячтение, а затем выполняется полное второе сканирование и маска. В приведенном выше коде Cython эта операция выполняется только один раз для чтения и записи).

Любое понимание приветствуется.

Ответы [ 2 ]

2 голосов
/ 01 ноября 2019

Проблема, с которой вы сталкиваетесь, не относится только к самой numpy или расширенному индексированию в numpy, а также к тому, создает ли оно копии или нет. Вместо этого он полностью обусловлен неопределенностью относительно того, гарантированно ли:

  • индексация возвращает значения, которые каким-то образом находятся «внутри» контейнера (его нет);и может ли

  • добавить на месте гарантированно вернуть измененную версию исходного значения (его нет)

Рассмотрим выражение:

x[a] += 100

где x - список сказать. Результатом x[a] является значение y, которое не имеет явных сведений о списке, в котором оно содержалось, и выражение y += 100 не гарантирует изменение исходного значения y ... таким образом, мыникогда не сможет гарантировать, что выражение вида x.__getitem__(x).__iadd__(100) влияет на оригинал x.

Таким образом, выражение x[a] += 100 должно быть оценено компилятором с помощью шагов:

  1. y = x[a]
  2. y += 100
  3. x[a] = y

В случае расширенной индексации можно ожидать, что a[ a > 5 ] += 42 будет реализован как:

  1. b = a > 5
  2. c = a[b]
  3. c += 42
  4. a[b] = c

Это может бытьдемонстрируется разбором примера функции

def f(a):
    a[a > 5] += 42

затем

>>> dis.dis(f)
 0 LOAD_FAST                0 (a)
 2 LOAD_FAST                0 (a)
 4 LOAD_CONST               1 (5)
 6 COMPARE_OP               4 (>)  # 1. b = a > 5
 8 DUP_TOP_TWO
10 BINARY_SUBSCR                   # 2. c = a[b]
12 LOAD_CONST               2 (42)
14 INPLACE_ADD                     # 3. c += 42
16 ROT_THREE
18 STORE_SUBSCR                    # 4. a[b] = c
20 LOAD_CONST               0 (None)
22 RETURN_VALUE

Обратите внимание, что в этой реализации индексирование a > 5 не выполняется дважды. Однако, если вы должны реализовать в виде связанного набора методов, у вас не будет иного выбора, кроме как реализовать его, как вы предложили.

0 голосов
/ 01 ноября 2019

Давайте изобразим соответствующие уловки, чтобы увидеть, что делает numpy / python:

import numpy as np

class spyarray(np.ndarray):
    def __getitem__(self, *args):
        print("__getitem__",self,*args)
        return np.ndarray.__getitem__(self, *args)
    def __setitem__(self, *args):
        print("__setitem__",self,*args)
        return np.ndarray.__setitem__(self, *args)
    def __add__(self, *args):
        print("__add__",self,*args)
        return np.ndarray.__add__(self, *args)
    def __iadd__(self, *args):
        print("__iadd__",self,*args)
        return np.ndarray.__iadd__(self, *args)
    def __repr__(self):
        return np.ndarray.__repr__(self.view(np.ndarray))
    def __str__(self):
        return np.ndarray.__str__(self.view(np.ndarray))


a = np.arange(10).view(spyarray)
a[a>5] += 42

Отпечатки:

__getitem__ [0 1 2 3 4 5 6 7 8 9] [False False False False False False  True  True  True  True]
__iadd__ [6 7 8 9] 42
__setitem__ [0 1 2 3 4 5 6 7 8 9] [False False False False False False  True  True  True  True] [48 49 50 51]

Так что это очень похоже на то, что вы придумали.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...