Передайте пользовательский компаратор в приоритетную очередь в Cython

Модуль Cython libcpp содержит шаблон для priority_queue, и это здорово, за исключением одного: я не могу передать ему пользовательский компаратор (или, по крайней мере, не знаю, как это сделать).

Мне это нужно, потому что мне нужно, чтобы priority_queue выполнял своего рода argsort, а не sort (да, приоритетная очередь оптимальна для того, что я хочу делать), и мне нужно, чтобы она была быстрой.

Возможно ли это в Cython, возможно, с помощью оборачивать очередь нестандартным образом или вообще не оборачивать?

В качестве примера предположим, что я хочу стабильно отсортировать vector[int[:]] по одному из элементов. Фактический алгоритм намного сложнее.

Я решаю отсортировать его, добавляя элемент за элементом в priority_queue. Однако я не знаю, как это сделать.

Моя фактическая операция похожа на этот вопрос, однако я объединяю отсортированные int[:]s одномерных элементов по определенному элементу, где исходные списки также упорядочены по этому элементу.

Я не против преобразования объектов в буфер/указатель.


person Hameer Abbasi    schedule 27.01.2018    source источник
comment
А что вы ставите в приоритетную очередь? Python-объекты? Небольшой пример кода прояснит, чего вы пытаетесь достичь.   -  person ead    schedule 27.01.2018
comment
Также было бы полезно знать, что вы хотите в качестве компаратора? Это не будет слишком сложно, если вы захотите использовать функцию cdef (т. е. без связанных переменных) или класс, который вы готовы написать на C++. Было бы намного сложнее использовать общий вызов Python.   -  person DavidW    schedule 28.01.2018
comment
@DavidW На самом деле это компаратор одномерных объектов MemoryView. Просто копирует определенный элемент обоих. . И у меня нет проблем с написанием его как cdef, хотя я бы предпочел не переходить на C++, если это возможно.   -  person Hameer Abbasi    schedule 28.01.2018
comment
На самом деле довольно сложно создавать контейнеры C++ для объектов Python (таких как memoryviews), поэтому это не звучит как хорошая идея, если у вас уже нет чего-то работающего. Однако возможно, что кто-то другой может не согласиться и предложить полезное решение.   -  person DavidW    schedule 28.01.2018
comment
@DavidW У меня нет ничего, что работало бы с банкоматом, но я хочу, чтобы это работало. Однако я мог просто получить доступ к буферу данных файла MemoryView. В любом случае, я использую Cython MemoryView, а не Python... Я понимаю, что это все еще объект Python, но мы можем обойтись без GIL и т. д.   -  person Hameer Abbasi    schedule 28.01.2018
comment
Использование указателя на буфер данных прекрасно, за исключением того, что он не выполняет подсчет ссылок, поэтому вам нужно убедиться, что вы также держите ссылку Python на все виды памяти (чтобы остановить их освобождение)   -  person DavidW    schedule 28.01.2018


Ответы (1)


Можно сделать эту работу, но я действительно не рекомендую это. Основные проблемы:

  • Контейнеры C++ не могут легко хранить объекты Python (такие как memoryviews), если вы не готовы написать код оболочки подсчета ссылок (на C++)
  • You can get a C pointer to the first element of a memoryview, but:
    • you must ensure that a reference to the underlying object (that owns the memory) is kept, otherwise Python will free it and you'll be using accessing invalid memory.
    • указатель теряет всю информацию о длине массива.
  • Вы довольно ограничены в отношении компараторов, которые вы можете использовать (они должны быть выражены как функция cdef) - например, я написал один, который сравнивает второй элемент массива, но потребуется перекомпиляция, чтобы перейти к сравнению третьего элемент.

Поэтому мой совет - найти другой способ сделать это. Однако:

Вам нужно написать очень маленький файл C++ для typedef типа очереди с приоритетом, который вы хотите. Это использует std::function в качестве компаратора, и я предположил, что вы хотите сохранить longs. Этот файл необходим, потому что поддержка шаблонов Cython довольно ограничена.

// cpp_priority_queue.hpp
#include <functional>
#include <queue>

using intp_std_func_prority_queue = std::priority_queue<long*,std::vector<long*>,std::function<bool(long*,long*)>>;

Затем вы не можете использовать оболочку libcpp.queue.priority_queue, поставляемую с Cython. Вместо этого пишите свои, оборачивая нужные вам функции ("priority_queue_wrap.pyx")

# distutils: language = c++

from libcpp cimport bool

cdef extern from "cpp_priority_queue.hpp":
    cdef cppclass intp_std_func_prority_queue:
        intp_std_func_prority_queue(...) # get Cython to accept any arguments and let C++ deal with getting them right
        void push(long*)
        long* top()
        void pop()
        bool empty()

cdef bool compare_2nd_element(long* a, long* b):
    # note - no protection if allocated memory isn't long enough
    return a[1] < b[1]


def example_function(list _input):
    # takes a list of "memoryviewable" objects
    cdef intp_std_func_prority_queue queue = intp_std_func_prority_queue(compare_2nd_element) # cdef function is convertable to function pointer

    cdef long[::1] list_element_mview
    cdef long* queue_element


    for x in _input:
        #print(x)
        list_element_mview = x
        assert list_element_mview.shape[0] >= 2 # check we have enough elements to compare the second one
        queue.push(&list_element_mview[0]) # push a pointer to the first element

    while not queue.empty():
        queue_element = queue.top(); queue.pop()
        print(queue_element[0],queue_element[1]) # print the first two elements (we don't know that we have any more)

Затем я создал пример функции, которая просматривает список объектов, совместимых с memoryview, преобразует их в указатели и добавляет в очередь. Наконец, он проходит через очередь по порядку и печатает то, что может. Обратите внимание, что список ввода переживает очередь!

Наконец, быстрая тестовая функция Python, которая создает соответствующий список:

import priority_queue_wrap
import numpy as np

a = np.vstack([np.arange(20),np.random.randint(0,10,size=(20,))])
l = [a[:,n].copy() for n in range(a.shape[1]) ]

print(l)
priority_queue_wrap.example_function(l)

Таким образом, объекты Python + Cython + C++ — это беспорядок: я не рекомендую делать это таким образом (но вы можете попробовать!)

person DavidW    schedule 28.01.2018
comment
Вау, спасибо за невероятно подробный ответ. Есть ли способ сравнить произвольный элемент? - person Hameer Abbasi; 28.01.2018
comment
Лучшее, что я могу придумать, не написав больше C++, - это иметь глобальную переменную element и выполнять a[element] < b[element] в вашей функции сравнения. Это немного грязно, хотя. Это основное ограничение использования функции cdef — они не могут сохранять состояние. - person DavidW; 28.01.2018
comment
Можем ли мы определить функцию C++, которая возвращает лямбду или что-то в этом роде? - person Hameer Abbasi; 28.01.2018