2009-12-09 20 views
48

permet de dire I ont deux fonctions:En python existe-t-il un moyen de vérifier si une fonction est une "fonction génératrice" avant de l'appeler?

def foo(): 
    return 'foo' 

def bar(): 
    yield 'bar' 

La première est une fonction normale, et la deuxième est une fonction de générateur. Maintenant, je veux écrire quelque chose comme ceci:

def run(func): 
    if is_generator_function(func): 
    gen = func() 
    gen.next() 
    #... run the generator ... 
    else: 
    func() 

Quelle sera une mise en œuvre simple de is_generator_function() ressembler? En utilisant le paquet types je peux tester si gen est un générateur, mais je souhaite le faire avant d'appeler func().

Considérons maintenant le cas suivant:

def goo(): 
    if False: 
    yield 
    else: 
    return 

Une invocation de goo() retourne un générateur. Je suppose que l'analyseur python sait que la fonction goo() a une déclaration de rendement, et je me demande s'il est possible d'obtenir facilement cette information.

Merci!

+1

Il est utile de noter que si une fonction contient une instruction 'yield', puis un' retour 'L'instruction à l'intérieur de cette fonction n'est pas autorisée à avoir un argument. Ce doit être juste 'return' qui termine le générateur. Bonne question! –

+0

Bon point, 'goo()' ne devrait pas être valide, cependant il l'est, au moins ici (Python 2.6.2). – Carlos

+4

Une note aux lecteurs actuels: @GregHewgill commentaire ci-dessus n'est plus juste, maintenant vous pouvez revenir avec argument (qui est passé sur la valeur attr de l'StopIteration) – wim

Répondre

55
>>> import inspect 
>>> 
>>> def foo(): 
... return 'foo' 
... 
>>> def bar(): 
... yield 'bar' 
... 
>>> print inspect.isgeneratorfunction(foo) 
False 
>>> print inspect.isgeneratorfunction(bar) 
True 
  • Nouveau dans la version Python 2,6
+31

Juste un commentaire de 2014 vous remerciant d'avoir fourni une réponse en 2011 sur une question de 2009 :) – wim

+2

Cela a résolu mon problème, mais ce n'est pas parfait. Si la fonction est un générateur enveloppé, comme 'partial (generator_fn, somearg = somevalue)' alors cela ne sera pas détecté. Pas plus qu'un lambda utilisé dans une circonstance similaire, comme 'lambda x: generator_fun (x, somearg = somevalue)'. Ceux-ci fonctionnent réellement comme prévu; Le code expérimentait avec une fonction d'aide qui peut enchaîner les générateurs, mais si une fonction normale est trouvée, elle va l'envelopper dans un "générateur d'élément unique". –

7
>>> def foo(): 
... return 'foo' 
... 
>>> def bar(): 
... yield 'bar' 
... 
>>> import dis 
>>> dis.dis(foo) 
    2   0 LOAD_CONST    1 ('foo') 
       3 RETURN_VALUE   
>>> dis.dis(bar) 
    2   0 LOAD_CONST    1 ('bar') 
       3 YIELD_VALUE   
       4 POP_TOP    
       5 LOAD_CONST    0 (None) 
       8 RETURN_VALUE   
>>> 

Comme vous le voyez, la principale différence est que le bytecode pour bar contiendra au moins un YIELD_VALUE opcode. Je recommande d'utiliser le module dis (en redirigeant sa sortie vers une instance StringIO et en vérifiant ses getvalue, bien sûr) car cela vous fournit une mesure de robustesse par rapport aux changements de bytecode - les valeurs numériques exactes des opcodes vont changer, mais la valeur symbolique désassemblée restera très stable ;-).

+0

Alex, comment vous sentez-vous d'appeler "blah" = func() "... puis vérifier si type (blah) est un générateur? et si ce n'est pas le cas, func() a déjà été appelé :-). Je pense que cela aurait été la façon dont j'aurais d'abord étudié comment faire cela :-). – Tom

+0

Était sur le point d'écrire la même chose mais l'übergod Python est arrivé en premier. :-) – paprika

+0

L'OP est très clair dans le titre de Q qu'il veut l'information ** avant d'appeler ** - montrant comment l'obtenir ** après ** l'appel ne répond pas à la question donnée avec les contraintes clairement exprimées. –

14

En fait, je me demande à quel point cette hypothétique is_generator_function() serait vraiment utile. Considérez:

def foo(): 
    return 'foo' 
def bar(): 
    yield 'bar' 
def baz(): 
    return bar() 
def quux(b): 
    if b: 
     return foo() 
    else: 
     return bar() 

Que dois-is_generator_function() retour pour baz et quux? baz() renvoie un générateur mais n'en est pas un lui-même et quux() peut renvoyer un générateur ou non.

+1

@Greg, absolument - tout callable peut renvoyer une itérative (qui peut notamment être un itérateur et en particulier un générateur, ou non), ou autre chose (ou rien du tout, en levant une exception), en fonction sur les arguments, les nombres aléatoires, ou la phase de la lune. Une fonction génératrice est un callable tel que "si elle retourne quelque chose plutôt que d'élever, retournera un objet générateur" (en attendant, tous les callables répondant à ces conditions ne sont pas des fonctions gnerator). Les cas d'utilisation sont donc difficiles à concocter. –

+0

Vous avez raison, c'est une sorte de question hypothétique. Il est venu pendant que je lisais la présentation de David Beazley sur les coroutines: http://www.dabeaz.com/coroutines/ – Carlos

+10

Faux, Vrai, Faux, Faux - respectivement. Il ne s'agit pas de savoir si la fonction renvoie une instance de générateur, mais de savoir s'il s'agit d'une fonction génératrice! – wim

1

J'ai mis en place un décorateur qui accroche la fonction décorée de retour/valeur donné. Sa base va:

import types 
def output(notifier): 
    def decorator(f): 
     def wrapped(*args, **kwargs): 
      r = f(*args, **kwargs) 
      if type(r) is types.GeneratorType: 
       for item in r: 
        # do something 
        yield item 
      else: 
       # do something 
       return r 
    return decorator 

Cela fonctionne parce que la fonction de décorateur est appelé inconditionnellement: il est la valeur de retour qui est testée.


EDIT: Suite au commentaire de Robert Lujo, j'ai fini avec quelque chose comme:

def middleman(f): 
    def return_result(r): 
     return r 
    def yield_result(r): 
     for i in r: 
      yield i 
    def decorator(*a, **kwa): 
     if inspect.isgeneratorfunction(f): 
      return yield_result(f(*a, **kwa)) 
     else: 
      return return_result(f(*a, **kwa)) 
    return decorator 
+0

J'ai eu un cas similaire et j'ai eu erreur: SyntaxError: 'return' avec l'argument à l'intérieur du générateur. Quand j'y pense, ça semble logique, la même fonction ne peut pas être fonction normale et fonction de générateur en même temps. Est-ce que cela fonctionne vraiment dans votre cas? –