Função de gerador vazio do Python


98

Em python, pode-se definir facilmente uma função iteradora, colocando a palavra-chave yield no corpo da função, como:

def gen():
    for i in range(100):
        yield i

Como posso definir uma função de gerador que não produz nenhum valor (gera 0 valores), o código a seguir não funciona, pois python não pode saber que é suposto ser um gerador e não uma função normal:

def empty():
    pass

Eu poderia fazer algo como

def empty():
    if False:
        yield None

Mas isso seria muito feio. Existe alguma maneira legal de realizar uma função iteradora vazia?

Respostas:


132

Você pode usar returnuma vez em um gerador; ele interrompe a iteração sem produzir nada e, portanto, fornece uma alternativa explícita para deixar a função sair do escopo. Portanto, use yieldpara transformar a função em um gerador, mas precedê-lo com returnpara encerrar o gerador antes de produzir qualquer coisa.

>>> def f():
...     return
...     yield
... 
>>> list(f())
[]

Não tenho certeza se é muito melhor do que o que você tem - ele apenas substitui uma ifinstrução no-op por uma instrução no-op yield. Mas é mais idiomático. Observe que apenas usar yieldnão funciona.

>>> def f():
...     yield
... 
>>> list(f())
[None]

Por que não apenas usar iter(())?

Esta pergunta pergunta especificamente sobre uma função de gerador vazia . Por esse motivo, considero que seja uma questão sobre a consistência interna da sintaxe do Python, em vez de uma questão sobre a melhor maneira de criar um iterador vazio em geral.

Se a dúvida for realmente sobre a melhor maneira de criar um iterador vazio, você pode concordar com o Zectbumo sobre o uso iter(()). Porém, é importante observar que iter(())não retorna uma função! Ele retorna diretamente um iterável vazio. Suponha que você esteja trabalhando com uma API que espera um chamável que retorna um iterável. Você terá que fazer algo assim:

def empty():
    return iter(())

(O crédito deve ir para Unutbu por fornecer a primeira versão correta desta resposta.)

Agora, você pode achar o acima mais claro, mas posso imaginar situações em que seria menos claro. Considere este exemplo de uma longa lista de definições de funções geradoras (inventadas):

def zeros():
    while True:
        yield 0

def ones():
    while True:
        yield 1

...

No final dessa longa lista, prefiro ver algo com um yield, como este:

def empty():
    return
    yield

ou, no Python 3.3 e superior (conforme sugerido pelo DSM ), isto:

def empty():
    yield from ()

A presença da yieldpalavra-chave deixa claro à primeira vista que se trata apenas de mais uma função geradora, exatamente como todas as outras. Demora um pouco mais para ver que a iter(())versão está fazendo a mesma coisa.

É uma diferença sutil, mas honestamente acho que as yieldfunções baseadas em-são mais legíveis e sustentáveis.


Isso é realmente melhor do que, if False: yieldmas ainda um tanto confuso para pessoas que não conhecem esse padrão
Konstantin Weitz

1
Eca, algo depois do retorno? Eu esperava algo assim itertools.empty().
Grault

1
@Jesdisciple, bem, returnsignifica algo diferente dentro dos geradores. É mais parecido break.
remetente

Gosto desta solução porque é (relativamente) concisa e não faz nenhum trabalho extra como comparar com False.
Pi Marillion

A resposta de Unutbu não é uma "verdadeira função geradora" como você mencionou, pois ela retorna um iterador.
Zectbumo de

72
iter(())

Você não precisa de um gerador. Vamos, pessoal!


3
Eu definitivamente gosto mais dessa resposta. É rápido, fácil de escrever, rápido na execução e mais atraente para mim do que iter([])pelo simples fato de que ()é uma constante enquanto []pode instanciar um novo objeto de lista na memória toda vez que é chamado.
Mumbleskates de

Para mim, essa também parece a solução mais elegante.
Matthias CM Troffaes

2
Voltando a este tópico, sinto-me compelido a salientar que, se você quiser um verdadeiro substituto imediato para uma função de gerador, deverá escrever algo como empty = lambda: iter(())ou def empty(): return iter(()).
remetente

Se você precisa de um gerador, você também pode usar (_ for _ in ()) como outros sugeriram
Zectbumo

1
@Zectbumo, ainda não é uma função geradora . É apenas um gerador. Uma função de gerador retorna um novo gerador sempre que é chamada.
remetente de

50

Python 3.3 (porque estou em um yield fromchute, e porque @senderle roubou meu primeiro pensamento):

>>> def f():
...     yield from ()
... 
>>> list(f())
[]

Mas tenho que admitir que estou tendo dificuldade em encontrar um caso de uso para o qual iter([])ou (x)range(0)não funcione igualmente bem.


Eu realmente gosto dessa sintaxe. o rendimento de é incrível!
Konstantin Weitz

2
Acho que isso é muito mais legível para um novato do que return; yieldou if False: yield None.
abarnert de

1
Esta é a solução mais elegante
Maksym Ganenko,

"Mas eu tenho que admitir, estou tendo dificuldade em encontrar um caso de uso para o qual iter([])ou (x)range(0)não funcione igualmente bem." -> Não tenho certeza do que (x)range(0)é, mas um caso de uso pode ser um método que deve ser substituído por um gerador completo em algumas das classes herdadas. Para fins de consistência, você deseja que até mesmo o básico, do qual os outros herdam, retorne um gerador exatamente como aqueles que o substituíram.
Vedran Šego

19

Outra opção é:

(_ for _ in ())

3
Eu gosto disso também. Posso sugerir uma tupla vazia em vez de uma lista vazia? Dessa forma, você consegue dobrar constantemente .
remetente

1
Obrigado. Eu não sabia disso.
Ben Reynwar

Ao contrário de outras opções, o Pycharm considera isso consistente com as dicas de tipo padrão usadas para geradores, comoGenerator[str, Any, None]
Michał Jabłoński

4

Deve ser uma função geradora? Se não, que tal

def f():
    return iter([])


1

Como disse @senderle, use isto:

def empty():
    return
    yield

Estou escrevendo esta resposta principalmente para compartilhar outra justificativa para isso.

Uma razão para escolher esta solução acima das outras é que ela é ótima no que diz respeito ao intérprete.

>>> import dis
>>> def empty_yield_from():
...     yield from ()
... 
>>> def empty_iter():
...     return iter(())
... 
>>> def empty_return():
...     return
...     yield
...
>>> def noop():
...     pass
...
>>> dis.dis(empty_yield_from)
  2           0 LOAD_CONST               1 (())
              2 GET_YIELD_FROM_ITER
              4 LOAD_CONST               0 (None)
              6 YIELD_FROM
              8 POP_TOP
             10 LOAD_CONST               0 (None)
             12 RETURN_VALUE
>>> dis.dis(empty_iter)
  2           0 LOAD_GLOBAL              0 (iter)
              2 LOAD_CONST               1 (())
              4 CALL_FUNCTION            1
              6 RETURN_VALUE
>>> dis.dis(empty_return)
  2           0 LOAD_CONST               0 (None)
              2 RETURN_VALUE
>>> dis.dis(noop)
  2           0 LOAD_CONST               0 (None)
              2 RETURN_VALUE

Como podemos ver, o empty_returntem exatamente o mesmo bytecode de uma função vazia regular; o resto executa uma série de outras operações que não mudam o comportamento de qualquer maneira. A única diferença entre empty_returne noopé que o primeiro tem o sinalizador gerador definido:

>>> dis.show_code(noop)
Name:              noop
Filename:          <stdin>
Argument count:    0
Positional-only arguments: 0
Kw-only arguments: 0
Number of locals:  0
Stack size:        1
Flags:             OPTIMIZED, NEWLOCALS, NOFREE
Constants:
   0: None
>>> dis.show_code(empty_return)
Name:              empty_return
Filename:          <stdin>
Argument count:    0
Positional-only arguments: 0
Kw-only arguments: 0
Number of locals:  0
Stack size:        1
Flags:             OPTIMIZED, NEWLOCALS, GENERATOR, NOFREE
Constants:
   0: None

Obviamente, a força desse argumento depende muito da implementação específica do Python em uso; um intérprete alternativo suficientemente inteligente pode perceber que as outras operações não significam nada útil e otimizá-las. No entanto, mesmo se essas otimizações estiverem presentes, elas exigem que o intérprete gaste tempo realizando-as e se proteja contra suposições de otimização sendo quebradas, como o iteridentificador no escopo global sendo rebote para outra coisa (mesmo que isso provavelmente indique um bug se realmente aconteceu). No caso de empty_returnsimplesmente não haver nada para otimizar, mesmo o relativamente ingênuo CPython não perderá tempo com operações espúrias.


0
generator = (item for item in [])
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.