Python, разбивающий выход генератора на две части - PullRequest
0 голосов
/ 07 ноября 2018

У меня есть доступ к генератору, который выдает два значения:

def get_document_values():
    docs = query_database()  # returns a cursor to database documents
    for doc in docs:
        # doc is a dictionary with ,say, {'x': 1, 'y': 99}
        yield doc['x'], doc['y']

У меня есть другая функция, process_x, которую я не могу изменить, которая может принять генератор в качестве входных данных, который обрабатывает все x для всех документов (если получен кортеж, то он просто обрабатывает первый элемент кортеж и игнорирует другие элементы):

X = process_x(get_document_values())  # This processes x but ignores y

Однако мне нужно также сохранить все значения y из генератора. Мое единственное решение - выполнить get_document_values дважды:

Y = [y for x,y in get_document_values()]  #Throw away x
X = process_x(get_document_values())      #Throw away y

Технически это работает, но когда нужно обработать много документов, возможно, что новый документ будет вставлен в базу данных, а длины X и Y будут другими. Между X и Y должно быть взаимно-однозначное сопоставление, и мне нужно было бы вызывать get_document_values только один раз вместо двух.

Я рассмотрел что-то вроде:

Y = []

def process_y(doc_generator):
    global Y
    for x,y in doc_generator:
        Y.append(y)
        yield x

X = process_x(process_y(get_document_values()))

Но:

  1. Это не кажется питоническим
  2. Y необходимо объявить как глобальную переменную

Я надеюсь, что есть более чистый, более питонский способ сделать это.

Обновление

На самом деле, get_document_values будет возвращать значения x, которые слишком велики для коллективного хранения в памяти, а process_x фактически уменьшает эту потребность в памяти. Таким образом, невозможно кэшировать все x. Кэширование всего y нормально, хотя.

Ответы [ 4 ]

0 голосов
/ 07 ноября 2018

Вероятно, не pythonic, но вы можете немного обмануть, если допустимо немного изменить основной генератор и использовать его атрибут функции:

from random import randrange
def get_vals():
        # mock creation of a x/y dict list
        docs =[{k: k+str(randrange(50)) for k in ('x','y')} for _ in range(10)]
        # create a function list attribute
        get_vals.y = []
        for doc in docs:
            # store the y value into the attribute
            get_vals.y.append(doc['y'])
            yield doc['x'], doc['y']  
            # if doc['y'] is purely for storage, you  might opt to not yield it at all.

Проверьте это:

# mock the consuming of generator for process_x            
for i in get_vals():
    print(i)    
# ('x13', 'y9'), ('x15', 'y40'), ('x41', 'y49')...

# access the ys stored in get_val function attribute after consumption
print(get_vals.y)
# ['y9', 'y40', 'y49', ...]

# instantiate the generator a second time a la process_x...
for i in get_vals():
    print(i)
# ('x18', 'y0'), ('x6', 'y35'), ('x24', 'y45')...

# access the cached y again
print(get_vals.y)
# ['y0', 'y35', 'y45', ...] 
  1. Это в основном кэширует ваши значения y, поскольку генератор выводит свой x для каждого вызова.
  2. И это исключает ваше ключевое слово global 1012 *
  3. И вы можете быть уверены, что отображение x / y является точным.

Некоторые могут посчитать это хаком, но я хотел бы думать об этом как о функции, поскольку все в Python - это объект, который позволяет вам сойти с рук с этим ...

0 голосов
/ 07 ноября 2018

Вы можете использовать itertools.tee , чтобы сделать два итератора из одного, затем использовать один итератор для process_x и второй для другой цели

0 голосов
/ 07 ноября 2018

Нет необходимости сохранять данные:

def process_entry(x, y):
    process_x((x,))
    return y

ys = itertools.starmap(process_entry, your_generator)

Просто помните, что только , когда вы получаете значение y, обрабатывается соответствующее ему значение x.

Если вы используете оба, верните оба в виде кортежа:

def process_entry(x, y):
    return next(process_x((x,))), y
0 голосов
/ 07 ноября 2018

Вы кэшируете все значения в список уже при вызове:

all_values = [(x,y) for x,y in get_document_values()] #or list(get_document_values())

Вы можете получить итератор для y значений с помощью:

Y = map(itemgetter(1), all_values)

А для x простое использование:

X = process_x(map(itemgetter(0), all_values))

Другой вариант - разделить генератор, например:

def get_document_values(getter):
    docs = query_database()  # returns a cursor to database documents
    for doc in docs:
        # doc is a dictionary with ,say, {'x': 1, 'y': 99}
        yield getter(doc)

from operator import itemgetter
X = process_x(get_document_values(itemgetter("x")))
Y = list(get_document_values(itemgetter("y")))

Таким образом, вам придется выполнить запрос дважды, если вы найдете способ выполнить запрос один раз и дублировать курсор, то вы также можете абстрагировать его:

def get_document_values(cursor, getter):
    for doc in cursor:
        # doc is a dictionary with ,say, {'x': 1, 'y': 99}
        yield getter(doc)
...