생성기가 처음부터 비어 있는지 어떻게 알 수 있습니까?


146

발전기에 peek,, hasNext등의 품목이없는 경우 간단한 테스트 방법이 isEmpty있습니까?


내가 틀렸다면 바로 잡으십시오. 그러나 어떤 제너레이터 에게도 일반적인 솔루션을 만들 수 있다면 , yield 문에 중단 점을 설정하고 "뒤로" 이동할 수 있는 능력과 같습니다. 이는 스택 프레임을 수율로 복제하고 StopIteration에서 복원하는 것을 의미합니까?

글쎄, 나는 그것들을 StopIteration을 복원할지 아닌지 추측하지만 적어도 StopIteration은 그것이 비어 있다고 말할 것입니다. 그래, 난 잠이 필요해 ...

4
나는 그가 왜 이것을 원하는지 알고 있다고 생각합니다. 템플릿을 사용하여 웹 개발을 수행하고 Cheetah와 같은 템플릿에 반환 값을 전달하는 경우 빈 목록 []은 편리하게 Falsey이므로 if를 확인하고 무언가를 위해 특별한 행동을 할 수 있습니다. 생성기는 요소가없는 경우에도 마찬가지입니다.
jpsimons

다음은 유스 케이스입니다 ... glob.iglob("filepattern")사용자 제공 와일드 카드 패턴을 사용하고 있으며 패턴이 파일과 일치하지 않으면 사용자에게 경고하고 싶습니다. 물론이 문제를 다양한 방법으로 해결할 수는 있지만 반복기가 비어 있는지 여부를 깨끗하게 테스트하는 것이 유용합니다.
LarsH

:이 솔루션에 사용 될 수 있습니다 stackoverflow.com/a/11467686/463758
balki

답변:


53

귀하의 질문에 대한 간단한 답변 : 아니오, 간단한 방법은 없습니다. 해결 방법이 많이 있습니다.

생성기가 무엇인지에 따라 간단한 방법이 없어야 합니다 . 시퀀스를 메모리에 유지하지 않고 일련의 값을 출력하는 방법 . 따라서 뒤로 순회가 없습니다.

원하는 경우 has_next 함수를 작성하거나 멋진 데코레이터로 메소드로 생성기에 쓸 수도 있습니다.


2
충분히 공평합니다. 나는 발전기의 길이를 찾는 방법이 없다는 것을 알고 있었지만 처음에 아무것도 생성하지 않는다면 찾기 방법을 놓쳤을 것이라고 생각했다.
Dan

1
아, 그리고 참고로, 나는 "멋진 데코레이터"제안을 구현하려고 시도했다. 단단한. 분명히 copy.deepcopy는 생성기에서 작동하지 않습니다.
David Berger

47
나는 "간단한 방법이어서는 안된다"는 것에 동의 할 수 있을지 모르겠다. 컴퓨터 과학에는 시퀀스를 메모리에 보관하지 않고 일련의 값을 출력하도록 설계되었지만 프로그래머가 "큐"에서 다른 값을 제거하지 않고 다른 값이 있는지 묻도록 허용하는 추상화가 많이 있습니다. "뒤로 순회"를 요구하지 않고 단일 엿보기와 같은 것이 있습니다. 반복자 디자인이 그러한 기능을 제공해야한다는 것은 아니지만, 반드시 유용합니다. 어쩌면 첫 번째 값이 엿보기 후에 변경 될 수 있다는 근거로 반대하고 있습니까?
LarsH

9
나는 전형적인 구현이 필요할 때까지 값을 계산하지 않는다는 근거로 반대하고 있습니다. 인터페이스가이 작업을 수행하도록 강요 할 수 있지만 경량 구현에는 적합하지 않을 수 있습니다.
David Berger

6
@ S. 시퀀스가 ​​비어 있는지 여부를 알기 위해 전체 시퀀스를 생성 할 필요가 없습니다. 하나의 요소에 충분한 저장 공간이 충분합니다. 내 대답을 참조하십시오.
Mark Ransom

99

암시:

def peek(iterable):
    try:
        first = next(iterable)
    except StopIteration:
        return None
    return first, itertools.chain([first], iterable)

용법:

res = peek(mysequence)
if res is None:
    # sequence is empty.  Do stuff.
else:
    first, mysequence = res
    # Do something with first, maybe?
    # Then iterate over the sequence:
    for element in mysequence:
        # etc.

2
에서 첫 번째 요소를 두 번 반환한다는 점은 확실하지 않습니다 return first, itertools.chain([first], rest).
njzk2

6
@ njzk2 "peek"작업을하려고했습니다 (따라서 함수 이름). wiki "피크는 데이터에서 값을 제거하지 않고 컬렉션의 최상위 값을 반환하는 연산입니다"
John Fouhy

생성기가 없음을 생성하도록 설계된 경우에는 작동하지 않습니다. def gen(): for pony in range(4): yield None if pony == 2 else pony
Paul

4
@Paul 반환 값을 자세히 살펴보십시오. 제너레이터가 완료되면 (즉, 복귀하지 None않고 올리는 StopIteration경우) 함수의 결과는 다음과 같습니다 None. 그렇지 않으면 튜플이지만 그렇지 않습니다 None.
기금 모니카의 소송

이것은 현재 프로젝트에 많은 도움이되었습니다. 파이썬의 표준 라이브러리 모듈 'mailbox.py'에 대한 코드에서 비슷한 예제를 발견했습니다. This method is for backward compatibility only. def next(self): """Return the next message in a one-time iteration.""" if not hasattr(self, '_onetime_keys'): self._onetime_keys = self.iterkeys() while True: try: return self[next(self._onetime_keys)] except StopIteration: return None except KeyError: continue
동료

29

간단한 방법은 생성기가 소진되거나 비어있는 경우 next ()에 선택적 매개 변수를 사용하는 것입니다. 예를 들면 다음과 같습니다.

iterable = some_generator()

_exhausted = object()

if next(iterable, _exhausted) == _exhausted:
    print('generator is empty')

편집 : mehtunguh의 의견에서 지적 된 문제가 수정되었습니다.


1
아닙니다. 이는 첫 번째로 산출 된 값이 참이 아닌 모든 발전기에 대해 올바르지 않습니다.
mehtunguh 2016 년

7
object()대신 class한 줄 더 짧게 만들려면 대신을 사용하십시오 . _exhausted = object(); if next(iterable, _exhausted) is _exhausted:
Messa

13

next(generator, None) is not None

또는 대체 None하지만 당신이 알고있는 값 은 발전기에 없습니다 .

편집 : 예, 생성기에서 1 개의 항목을 건너 뜁니다. 그러나 종종 유효성 검사 목적으로 만 생성기가 비어 있는지 확인한 다음 실제로 사용하지는 않습니다. 그렇지 않으면 나는 다음과 같은 것을한다 :

def foo(self):
    if next(self.my_generator(), None) is None:
        raise Exception("Not initiated")

    for x in self.my_generator():
        ...

그게 당신의 경우이 작품이며, 발전기는 A로부터 오는 기능 처럼 generator().


4
이것이 최선의 대답이 아닌 이유는 무엇입니까? 발전기가 None?를 반환하는 경우
Sait

8
아마도 이것은 비어 있는지 테스트하는 대신 실제로 발전기를 소비해야하기 때문입니다.
bfontaine

3
다음에 전화하는 순간 (제너레이터, 없음) 사용 가능한 경우 1 개의 항목을 건너 뛰기 때문에 나쁩니다.
Nathan Do

맞습니다, 당신은 당신의 유전자의 첫 번째 요소를 놓치게 될 것이고 또한 빈 것이 있는지 테스트하는 대신에 당신의 유전자를 소비 할 것입니다.
AJ

12

IMHO의 최선의 방법은 특별한 테스트를 피하는 것입니다. 대부분의 시간, 발전기의 사용 이다 테스트는 :

thing_generated = False

# Nothing is lost here. if nothing is generated, 
# the for block is not executed. Often, that's the only check
# you need to do. This can be done in the course of doing
# the work you wanted to do anyway on the generated output.
for thing in my_generator():
    thing_generated = True
    do_work(thing)

충분하지 않은 경우에도 명시 적 테스트를 수행 할 수 있습니다. 이 시점 thing에서 마지막으로 생성 된 값이 포함됩니다. 아무것도 생성되지 않은 경우 변수를 아직 정의하지 않은 경우 정의되지 않습니다. 의 값을 확인할 수 thing있지만 약간 신뢰할 수 없습니다. 대신 블록 내에 플래그를 설정하고 나중에 확인하십시오.

if not thing_generated:
    print "Avast, ye scurvy dog!"

3
이 솔루션은 전체 발전기를 소비하려고 시도하므로 무한 발전기에는 사용할 수 없습니다.
Viktor Stískala

@ ViktorStískala : 나는 당신의 요점이 보이지 않습니다. 무한 생성기가 결과를 생성했는지 테스트하는 것은 어리석은 일입니다.
vezult

다른 결과를 처리하지 않고 결과를 생성하는 데 쓸모가 없기 때문에 솔루션에 for 루프가 중단 될 수 있음을 지적하고 싶습니다. range(10000000)유한 생성기 (Python 3)이지만 생성 여부를 찾기 위해 모든 항목을 살펴볼 필요는 없습니다.
Viktor Stískala

1
@ ViktorStískala : 이해합니다. 그러나 요점은 이것입니다. 일반적으로 실제로 발전기 출력에서 ​​작동하려고합니다. 내 예제에서 아무 것도 생성되지 않으면 알 수 있습니다. 그렇지 않으면 생성 된 출력을 의도 한대로 작동합니다. "발전기 사용이 테스트입니다." 특별한 테스트가 필요 없거나 발전기 출력을 무의미하게 소비 할 필요가 없습니다. 이것을 명확히하기 위해 내 대답을 편집했습니다.
vezult

8

나는 나 자신을 사용하지 않을 것이라고, 특히 한 두 번째 솔루션을 제공 싫지만, 당신은 절대적 경우 이 작업을 수행하고 다른 답변에서와 같이 발전기를 소비하지 :

def do_something_with_item(item):
    print item

empty_marker = object()

try:
     first_item = my_generator.next()     
except StopIteration:
     print 'The generator was empty'
     first_item = empty_marker

if first_item is not empty_marker:
    do_something_with_item(first_item)
    for item in my_generator:
        do_something_with_item(item)

이제는이 솔루션이 마음에 들지 않습니다. 왜냐하면 이것이 발전기가 사용되는 방식이 아니라고 믿기 때문입니다.


4

나는이 게시물이 지금 5 세라는 것을 알고 있지만, 이것을하는 관용적 인 방법을 찾고있는 동안 그것을 발견했지만 내 솔루션이 게시되지 않았습니다. 후손을 위해 :

import itertools

def get_generator():
    """
    Returns (bool, generator) where bool is true iff the generator is not empty.
    """
    gen = (i for i in [0, 1, 2, 3, 4])
    a, b = itertools.tee(gen)
    try:
        a.next()
    except StopIteration:
        return (False, b)
    return (True, b)

물론, 많은 주석가들이 지적 하겠지만, 이것은 해키이며 특정 제한된 상황 (예 : 발전기가 부작용이없는 곳)에서만 작동합니다. YMMV.


1
이것은 gen각 항목에 대해 생성기를 한 번만 호출 하므로 부작용은 그리 나쁘지 않습니다. 그러나을 통해 생성기에서 가져온 모든 것을 복사하지만 저장 b하지는 않으므로 a메모리에 미치는 영향은 실행 list(gen)하고 확인하는 것과 유사 합니다.
Matthias Fripp

두 가지 문제가 있습니다. 1.이 itertool은 많은 임시 데이터를 저장해야 할 수도 있습니다 (일시적으로 저장해야하는 임시 데이터 양에 따라 다름). 일반적으로 한 반복자가 다른 반복자가 시작하기 전에 대부분 또는 모든 데이터를 사용하는 경우 tee () 대신 list ()를 사용하는 것이 더 빠릅니다. 2. 티 반복기는 스레드 세이프가 아닙니다. 원래 iterable이 스레드 세이프 인 경우에도 동일한 tee () 호출에서 리턴 된 반복자를 동시에 사용할 때 RuntimeError가 발생할 수 있습니다.
AJ

3

명백한 접근 방식에 대해 죄송하지만 가장 좋은 방법은 다음과 같습니다.

for item in my_generator:
     print item

이제 생성기를 사용하는 동안 생성기가 비어 있음을 감지했습니다. 물론 발전기가 비어 있으면 항목이 표시되지 않습니다.

이것은 코드에 정확하게 맞지 않을 수도 있지만 생성기의 관용구입니다. 반복하기 때문에 접근 방식을 약간 변경하거나 생성기를 전혀 사용하지 않을 수 있습니다.


아니면 ... 질문자는 빈 발전기를 감지하려고 시도 하는지에 대한 힌트를 줄 수 있습니까?
S.Lott

"제너레이터가 비어있어 아무것도 표시되지 않습니다"라는 의미입니까?
SilentGhost

로트 나는 동의한다. 왜 그런지 알 수 없습니다. 그러나 이유가 있었더라도 문제는 각 항목을 대신 사용하는 것이 더 좋을 것이라고 생각합니다.
Ali Afshar

1
생성기가 비어 있는지 프로그램에 알리지 않습니다.
Ethan Furman

3

발전기가 비어 있는지 확인하기 위해해야 ​​할 일은 다음 결과를 얻는 것입니다. 물론 그 결과를 사용할 준비 가되지 않았다면 나중에 다시 반환하기 위해 저장해야합니다.

다음은 __nonzero__테스트 를 추가하기 위해 기존 반복기에 추가 할 수있는 래퍼 클래스입니다 . 따라서 생성기가 비어 있는지 확인할 수 있습니다 if. 아마도 데코레이터로 바꿀 수도 있습니다.

class GenWrapper:
    def __init__(self, iter):
        self.source = iter
        self.stored = False

    def __iter__(self):
        return self

    def __nonzero__(self):
        if self.stored:
            return True
        try:
            self.value = next(self.source)
            self.stored = True
        except StopIteration:
            return False
        return True

    def __next__(self):  # use "next" (without underscores) for Python 2.x
        if self.stored:
            self.stored = False
            return self.value
        return next(self.source)

사용 방법은 다음과 같습니다.

with open(filename, 'r') as f:
    f = GenWrapper(f)
    if f:
        print 'Not empty'
    else:
        print 'Empty'

반복이 시작될 때뿐만 아니라 언제든지 공허함을 확인할 수 있습니다.


이것은 올바른 방향으로 향하고 있습니다. 필요한만큼 많은 결과를 저장하고 원하는만큼 미리 엿볼 수 있도록 수정해야합니다. 이상적으로는 스트림 헤드에 임의의 항목을 밀어 넣을 수 있습니다. 푸시 가능한 반복자는 자주 사용하는 매우 유용한 추상화입니다.
sfkleach 2016 년

@ sfkleach 나는 여러 번 엿보기를 위해 이것을 복잡하게 할 필요가 없다고 생각합니다.있는 그대로 유용하고 질문에 대답합니다. 이것은 오래된 질문이지만 여전히 가끔씩 보입니다. 따라서 자신의 답변을 남기고 싶다면 누군가가 유용하다고 생각할 수 있습니다.
Mark Ransom

Mark가 그의 솔루션이 질문에 대답하는 것이 옳습니다. 이것이 핵심입니다. 나는 그것을 더 잘 표현해야했다. 내가 의미하는 바는 푸시 백이 무한한 푸시 가능한 반복자가 내가 매우 유용한 관용구이며 구현이 훨씬 간단하다는 것입니다. 제안 된대로 변형 코드를 게시 할 것입니다.
sfkleach

2

Mark Ransom이 프롬프트하면 반복자를 래핑하여 미리 볼 수 있고 값을 다시 스트림으로 푸시하고 비어 있는지 확인할 수있는 클래스가 있습니다. 과거에 매우 유용했던 간단한 구현으로 간단한 아이디어입니다.

class Pushable:

    def __init__(self, iter):
        self.source = iter
        self.stored = []

    def __iter__(self):
        return self

    def __bool__(self):
        if self.stored:
            return True
        try:
            self.stored.append(next(self.source))
        except StopIteration:
            return False
        return True

    def push(self, value):
        self.stored.append(value)

    def peek(self):
        if self.stored:
            return self.stored[-1]
        value = next(self.source)
        self.stored.append(value)
        return value

    def __next__(self):
        if self.stored:
            return self.stored.pop()
        return next(self.source)

2

이 스레드에 빠졌고 매우 간단하고 읽기 쉬운 대답이 누락되었음을 깨달았습니다.

def is_empty(generator):
    for item in generator:
        return False
    return True

우리가 어떤 품목을 소비한다고 생각하지 않는다면 발전기에 첫 번째 품목을 다시 주입해야합니다.

def is_empty_no_side_effects(generator):
    try:
        item = next(generator)
        def my_generator():
            yield item
            yield from generator
        return my_generator(), False
    except StopIteration:
        return (_ for _ in []), True

예:

>>> g=(i for i in [])
>>> g,empty=is_empty_no_side_effects(g)
>>> empty
True
>>> g=(i for i in range(10))
>>> g,empty=is_empty_no_side_effects(g)
>>> empty
False
>>> list(g)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

1
>>> gen = (i for i in [])
>>> next(gen)
Traceback (most recent call last):
  File "<pyshell#43>", line 1, in <module>
    next(gen)
StopIteration

생성기의 끝에는 StopIteration사용자의 경우에 즉시 도달하기 때문에 예외가 발생하기 때문에 발생합니다. 그러나 일반적으로 다음 값이 있는지 확인해서는 안됩니다.

당신이 할 수있는 또 다른 일은 :

>>> gen = (i for i in [])
>>> if not list(gen):
    print('empty generator')

2
실제로 전체 발전기를 소비합니다. 슬프게도, 이것이 바람직하거나 바람직하지 않은 행동인지는 확실하지 않습니다.
S.Lott

"터치"생성기의 다른 방법으로 생각합니다.
SilentGhost

나는 이것이 오래된 것을 알고 있지만 'list ()'를 사용하는 것이 가장 좋은 방법은 될 수 없습니다. 생성 된 목록이 비어 있지 않지만 실제로는 크면 불필요하게 낭비입니다.
Chris_Rands

1

생성기를 사용 하기 전에 알아야 할 경우 간단한 방법은 없습니다. 생성기를 사용한 까지 기다릴 수 있으면 간단한 방법이 있습니다.

was_empty = True

for some_item in some_generator:
    was_empty = False
    do_something_with(some_item)

if was_empty:
    handle_already_empty_generator_case()

1

itertools.chain로 생성기를 감싸고 iterable의 끝을 두 번째 iterable로 나타내는 것을 넣은 다음 간단히 확인하십시오.

전의:

import itertools

g = some_iterable
eog = object()
wrap_g = itertools.chain(g, [eog])

이제 남은 것은 iterable의 끝에 추가 한 값을 확인하는 것입니다. 읽을 때 끝을 의미합니다.

for value in wrap_g:
    if value == eog: # DING DING! We just found the last element of the iterable
        pass # Do something

iterable에서 절대 발생하지 eog = object()않는다고 가정하는 대신 사용하십시오 float('-inf').
bfontaine

@bfontaine 좋은 생각
smac89

1

필자의 경우 항목을 병합하는 함수에 전달하기 전에 생성기 호스트가 채워 졌는지 알아야했습니다 zip(...). 해결책은 받아 들여진 대답과 비슷하지만 충분히 다릅니다.

정의:

def has_items(iterable):
    try:
        return True, itertools.chain([next(iterable)], iterable)
    except StopIteration:
        return False, []

용법:

def filter_empty(iterables):
    for iterable in iterables:
        itr_has_items, iterable = has_items(iterable)
        if itr_has_items:
            yield iterable


def merge_iterables(iterables):
    populated_iterables = filter_empty(iterables)
    for items in zip(*populated_iterables):
        # Use items for each "slice"

내 특정 문제에는 iterables가 비어 있거나 정확히 동일한 수의 항목이 있다는 속성이 있습니다.


1

이 솔루션 만 빈 반복 작업을하는 것으로 나타났습니다.

def is_generator_empty(generator):
    a, b = itertools.tee(generator)
    try:
        next(a)
    except StopIteration:
        return True, b
    return False, b

is_empty, generator = is_generator_empty(generator)

또는이 예외를 사용하지 않으려면 사용하십시오.

def is_generator_empty(generator):
    a, b = itertools.tee(generator)
    for item in a:
        return False, b
    return True, b

is_empty, generator = is_generator_empty(generator)

표시된 솔루션 에서는 다음 과 같은 빈 발전기에 사용할 수 없습니다

def get_empty_generator():
    while False:
        yield None 

generator = get_empty_generator()


0

다음은 루프가 실행되는지 확인하는 동안 무언가를 산출하는 동안 반복자를 반환하는 데 사용하는 간단한 접근 방식입니다.

        n = 0
        for key, value in iterator:
            n+=1
            yield key, value
        if n == 0:
            print ("nothing found in iterator)
            break

0

다음은 생성기를 감싸는 간단한 데코레이터이므로 비어 있으면 None을 반환합니다. 이것은 코드가 루프를 생성 하기 전에 생성기가 어떤 것을 생성하는지 여부를 코드가 알아야하는 경우에 유용 할 수 있습니다 .

def generator_or_none(func):
    """Wrap a generator function, returning None if it's empty. """

    def inner(*args, **kwargs):
        # peek at the first item; return None if it doesn't exist
        try:
            next(func(*args, **kwargs))
        except StopIteration:
            return None

        # return original generator otherwise first item will be missing
        return func(*args, **kwargs)

    return inner

용법:

import random

@generator_or_none
def random_length_generator():
    for i in range(random.randint(0, 10)):
        yield i

gen = random_length_generator()
if gen is None:
    print('Generator is empty')

이것이 유용한 한 가지 예는 템플릿 코드를 작성하는 것입니다. 즉 jinja2

{% if content_generator %}
  <section>
    <h4>Section title</h4>
    {% for item in content_generator %}
      {{ item }}
    {% endfor %
  </section>
{% endif %}

이것은 발전기 기능을 두 번 호출하므로 발전기의 시작 비용이 두 번 발생합니다. 예를 들어 생성기 함수가 데이터베이스 쿼리 인 경우에 상당 할 수 있습니다.
Ian Goldby

0

islice를 사용하면 첫 번째 반복까지만 확인하면 비어 있는지 확인할 수 있습니다.

itertools에서 가져 오기 islice

def isempty (iterable) :
    반환 목록 (islice (iterable, 1)) == []


죄송합니다,이 책은 소비적인 내용입니다 ... StopIteration (으)로 시도 / 캐치를 수행해야 함
Quin

0

any ()를 사용하는 것은 어떻습니까? 발전기와 함께 사용하면 정상적으로 작동합니다. 여기 에 대해 조금 설명하는 사람이 있습니다.


2
모든 생성기에 "any ()"를 사용할 수는 없습니다. 여러 데이터 프레임을 포함하는 생성기와 함께 사용하려고했습니다. "DataFrame의 진실 가치는 모호합니다."라는 메시지를 받았습니다. 에 (my_generator_of_df)
probitaille

any(generator)생성기가 bool기본 데이터 유형 (예 : int, string) 작업 으로 캐스트 할 수있는 값을 생성한다는 것을 알고있을 때 작동합니다. any(generator)생성기가 비어 있거나 생성기에 잘못된 값만있는 경우 (예 : 생성기가 0, ''(빈 문자열) 및 False를 생성하는 경우) 여전히 False가됩니다. 당신이 그것을 알고있는 한, 이것은 의도 된 행동 일 수도 아닐 수도 있습니다. :)
Daniel

0

cytoolz 에서 엿보기 기능을 사용하십시오 .

from cytoolz import peek
from typing import Tuple, Iterable

def is_empty_iterator(g: Iterable) -> Tuple[Iterable, bool]:
    try:
        _, g = peek(g)
        return g, False
    except StopIteration:
        return g, True

이 함수에 의해 리턴 된 반복자는 인수로 전달 된 원래 반복자와 동일합니다.


-2

sum 함수를 사용하여 해결했습니다. glob.iglob과 함께 사용한 예제는 아래를 참조하십시오 (생성기를 반환 함).

def isEmpty():
    files = glob.iglob(search)
    if sum(1 for _ in files):
        return True
    return False

* 이것은 아마도 HUGE 생성기에서는 작동하지 않지만 작은 목록에서는 훌륭하게 수행해야합니다

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.