Понимание вложенного списка с генераторами - PullRequest
0 голосов
/ 26 мая 2019

У меня странное поведение на python 3.7 с пониманием вложенного списка, которое включает генератор.

Это работает:

i = range(20)
n = [1, 2, 3]
result = [min(x + y for x in i) for y in n]

Работает , не работает , если i является генератором:

i = (p for p in range(20))
n = [1, 2, 3]
result = [min(x + y for x in i) for y in n]

Это поднимает ValueError: min() arg is an empty sequence

Теперь, даже если генератор i обернут в list, он все равно создает ту же ошибку:

i = (p for p in range(20))
n = [1, 2, 3]
result = [min(x + y for x in list(i)) for y in n]

Это ошибка Python или ожидаемое поведение? Если это ожидаемое поведение, можете ли вы объяснить, почему это не работает?

Ответы [ 3 ]

2 голосов
/ 26 мая 2019

В обоих ваших последних примерах вы пытаетесь снова выполнить итерацию на генераторе после его исчерпания.

В вашем последнем примере list(i) вычисляется снова для каждого значения y, поэтому i будет исчерпан после первого запуска.

Вы должны составить списокзначения, которые он выдает один раз раньше, например:

i = (p for p in range(20))
n = [1, 2, 3]
list_i = list(i)
result = [min(x + y for x in list_i) for y in n]
2 голосов
/ 26 мая 2019

В i = range(20) range(20) - это обещание создать генератор. Пока i = (p for p in range(20)) уже является генератором.

Теперь напишите ваше выражение списка как:

for y in [1, 2, 3]:
    print(min(x + y for x in i))
## 1
## ...
## ValueError: min() arg is an empty sequence 

Вы печатаете 1, но (генератор исчерпан при первом вызове) и тогда вы получите в следующем раунде ValueError: min() arg is an empty sequence потому что генератор i уже использовался в первом вызове цикла for для y как 1. Хотя, если i определено как range(20), каждый раз, когда вызывается for x in i, генератор воссоздается снова и снова.

Вы можете подражать тому, что делает range(20):

def gen():
    return (p for p in range(20))

for y in [1, 2, 3]:
    print(min(x + y for x in gen())) 
    # range() like gen() is a promise to generate the generator
## 1
## 2
## 3

Теперь генератор создается каждый раз заново.

Но на самом деле, range еще круче, если вы сделаете:

i = range(20)

for y in [1, 2, 3]:
    print(min(x + y for x in i))
## 1
## 2
## 3

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

Это на самом деле реализовано в Python с использованием класса и путем определения метода __iter__(). Который определяет поведение в интеграторах - здесь особенно ленивое поведение.

Чтобы имитировать это поведение, мы можем сгенерировать ленивый генератор (lazy_gen).

class lazy_gen:
    def __init__(self):
        pass

    def __iter__(self):    # everytime when used as an iterator
        return self.gen()  # recreate the generator # real lazy behavior

    def gen(self):
        return (p for p in range(20))

Который мы можем использовать как:

i = lazy_gen()

for y in [1, 2, 3]:
    print(min(x + y for x in i))
## 1
## 2
## 3

Так что это еще лучше отражает поведение range().

Другие языки (функциональные языки), такие как Lisp семейные языки (common-lisp, Racket, Scheme, Clojure), R или Haskell иметь лучший контроль над оценкой - таким образом, над ленивой оценкой и обещаниями. Но в Python для таких реализаций и тонкого контроля нужно прибегнуть к ООП.

Моя функция диапазона и класс

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

class Myrange:
    def __init__(self, start, end, step):
        self.start = start
        self.end = end
        self.step = step

    def __iter__(self):
        return self.generate_range()

    def generate_range(self):
        x = self.start - self.step
        while x + self.step < self.end:
            x = x + self.step
            yield x

    def __repr__(self):
        return "myrange({}, {})".format(self.start, self.end)



def myrange(start=None, end=None, step=1):
    if start is None and end is None:
        raise "Please provide at least one number for the range-limits."
    elif start is not None and end is None:
        _start = 0
        _end = start
    elif start is not None and end is not None:
        _start = start
        _end = end
    else:
        _start = 0
        _end = end
    _step = step
    return Myrange(_start, _end, _step)

Можно использовать точно так же, как функцию диапазона.

i = myrange(20)

n = [1, 2, 3]
result = [min(x + y for x in i) for y in n]

result 
## [1, 2, 3]

i 
## myrange(0, 20)  # representation of a Myrange object.

myrange(20)
## myrange(0, 20)

list(myrange(3, 10))
## [3, 4, 5, 6, 7, 8, 9]

list(myrange(0, 10))
## [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

list(myrange(10))
## [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

list(myrange(0, 10, 2))
## [0, 2, 4, 6, 8]

list(myrange(3, 10, 2))
## [3, 5, 7, 9]
1 голос
/ 26 мая 2019

Генератор очищается после первого цикла for для for x in i или for x in list(i), вместо этого вам необходимо предварительно преобразовать генератор в список (который по существу перебирает генератор и очищает его) и использовать этот список

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

In [14]: list(range(20)) ==  list(p for p in range(20))                                                                                                                             
Out[14]: True

Следовательно, обновленный код будет

#Create generator and convert to list
i = list(p for p in range(20))

n = [1, 2, 3]
#Use that list in the list comprehension
result = [min(x + y for x in i) for y in n]
print(result)

Вывод будет

[1, 2, 3]

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

n = [1, 2, 3]
result = [min(x + y for x in (p for p in range(20))) for y in n]
print(result)
#[1, 2, 3]
...