web-dev-qa-db-fra.com

Générateur comme argument de fonction

Quelqu'un peut-il expliquer pourquoi le passage d'un générateur comme seul argument positionnel à une fonction semble avoir des règles spéciales?

Si nous avons:

>>> def f(*args):
>>>    print "Success!"
>>>    print args
  1. Cela fonctionne, comme prévu.

    >>> f(1, *[2])
    Success!
    (1, 2)
    
  2. Cela ne fonctionne pas, comme prévu.

    >>> f(*[2], 1)
      File "<stdin>", line 1
    SyntaxError: only named arguments may follow *expression
    
  3. Cela fonctionne, comme prévu

    >>> f(1 for x in [1], *[2])
    Success! 
    (generator object <genexpr> at 0x7effe06bdcd0>, 2)
    
  4. Cela fonctionne, mais je ne comprends pas pourquoi. Ne devrait-il pas échouer de la même manière que 2)

    >>> f(*[2], 1 for x in [1])                                               
    Success!
    (generator object <genexpr> at 0x7effe06bdcd0>, 2)
    
81
DeTeReR

3. et 4. devrait être des erreurs de syntaxe sur toutes les versions Python. Cependant, vous avez trouvé un bogue qui affecte Python versions 2.5 - 3.4, et qui a ensuite été posté sur le Python tracker problème =. En raison du bogue, une expression de générateur non entre parenthèses a été acceptée comme argument d'une fonction si elle n'était accompagnée que de *args et/ou **kwargs. Alors que Python 2.6+ autorisait les deux cas 3. et 4., Python 2.5 n'autorisait que le cas 3. - pourtant tous les deux étaient contre le grammaire documentée :

call    ::=     primary "(" [argument_list [","]
                            | expression genexpr_for] ")"

c'est-à-dire que la documentation dit qu'un appel de fonction comprend primary (l'expression qui correspond à un appelable), suivi, entre parenthèses, soit d'une liste d'arguments ou juste une expression de générateur non entre parenthèses; et dans la liste d'arguments, toutes les expressions de générateur doivent être entre parenthèses.


Ce bug (bien qu'il semble qu'il n'ait pas été connu), avait été corrigé dans Python 3.5 versions préliminaires. Dans Python 3,5 parenthèses sont toujours requises autour d'une expression de générateur) , sauf s'il s'agit du seul argument de la fonction:

Python 3.5.0a4+ (default:a3f2b171b765, May 19 2015, 16:14:41) 
[GCC 4.9.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> f(1 for i in [42], *a)
  File "<stdin>", line 1
SyntaxError: Generator expression must be parenthesized if not sole argument

Ceci est maintenant documenté dans le Quoi de neuf dans Python 3.5 , grâce à DeTeReR repérant ce bogue.


Analyse du bug

Une modification a été apportée à Python 2.6 qui autorise l'utilisation d'arguments de mots clés après*args :

Il est également devenu légal de fournir des arguments de mot-clé après un argument * args à un appel de fonction.

>>> def f(*args, **kw):
...     print args, kw
...
>>> f(1,2,3, *(4,5,6), keyword=13)
(1, 2, 3, 4, 5, 6) {'keyword': 13}

Auparavant, cela aurait été une erreur de syntaxe. (Contribution d’Amaury Forgeot d’Arc; numéro 3473.)


Cependant, le Python 2.6 grammaire ne fait aucune distinction entre les arguments de mots clés, les arguments de position ou les expressions de générateur nues - ils sont tous de type argument à l'analyseur.

Selon les règles Python, une expression de générateur doit être mise entre parenthèses si elle n'est pas le seul argument de la fonction. Ceci est validé dans les Python/ast.c :

for (i = 0; i < NCH(n); i++) {
    node *ch = CHILD(n, i);
    if (TYPE(ch) == argument) {
        if (NCH(ch) == 1)
            nargs++;
        else if (TYPE(CHILD(ch, 1)) == gen_for)
            ngens++;
        else
            nkeywords++;
    }
}
if (ngens > 1 || (ngens && (nargs || nkeywords))) {
    ast_error(n, "Generator expression must be parenthesized "
              "if not sole argument");
    return NULL;
}

Cependant cette fonction ne pas considère le *args du tout - il ne recherche spécifiquement que les arguments positionnels ordinaires et les arguments de mots clés.

Plus bas dans la même fonction, un message d'erreur est généré pour arg non-mot-clé après arg mot-clé :

if (TYPE(ch) == argument) {
    expr_ty e;
    if (NCH(ch) == 1) {
        if (nkeywords) {
            ast_error(CHILD(ch, 0),
                      "non-keyword arg after keyword arg");
            return NULL;
        }
        ...

Mais cela s'applique à nouveau aux arguments qui sont pas expressions de générateur non entre parenthèses comme comme en témoigne le else if instruction :

else if (TYPE(CHILD(ch, 1)) == gen_for) {
    e = ast_for_genexp(c, ch);
    if (!e)
        return NULL;
    asdl_seq_SET(args, nargs++, e);
}

Ainsi, une expression de générateur sans parenthèse a été autorisée à glisser.


Maintenant dans Python 3.5 on peut utiliser le *args n'importe où dans un appel de fonction, donc Grammaire a été modifié pour tenir compte de ceci:

arglist: argument (',' argument)*  [',']

et

argument: ( test [comp_for] |
            test '=' test |
            '**' test |
            '*' test )

et la boucle for a été modifiée en

for (i = 0; i < NCH(n); i++) {
    node *ch = CHILD(n, i);
    if (TYPE(ch) == argument) {
        if (NCH(ch) == 1)
            nargs++;
        else if (TYPE(CHILD(ch, 1)) == comp_for)
            ngens++;
        else if (TYPE(CHILD(ch, 0)) == STAR)
            nargs++;
        else
            /* TYPE(CHILD(ch, 0)) == DOUBLESTAR or keyword argument */
            nkeywords++;
    }
}

Fixant ainsi le bug.

Cependant, le changement involontaire est que les constructions valides

func(i for i in [42], *args)

et

func(i for i in [42], **kwargs)

où un générateur sans parenthèse précède *args ou **kwargs a cessé de fonctionner.


Pour localiser ce bogue, j'ai essayé différentes versions Python. En 2.5, vous obtiendrez SyntaxError:

Python 2.5.5 (r255:77872, Nov 28 2010, 16:43:48) 
[GCC 4.4.5] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> f(*[1], 2 for x in [2])
  File "<stdin>", line 1
    f(*[1], 2 for x in [2])

Et cela a été corrigé avant une version préliminaire de Python 3.5:

Python 3.5.0a4+ (default:a3f2b171b765, May 19 2015, 16:14:41) 
[GCC 4.9.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> f(*[1], 2 for x in [2])
  File "<stdin>", line 1
SyntaxError: Generator expression must be parenthesized if not sole argument

Cependant, l'expression du générateur entre parenthèses, elle fonctionne dans Python 3.5, mais elle ne fonctionne pas non dans Python 3.4:

f(*[1], (2 for x in [2]))

Et c'est l'indice. Dans Python 3.5 le *splatting est généralisé; vous pouvez l'utiliser n'importe où dans un appel de fonction:

>>> print(*range(5), 42)
0 1 2 3 4 42

Donc, le bug réel (générateur fonctionnant avec *star sans parenthèses) était en effet corrigé dans Python 3.5, et le bug pouvait être trouvé en ce qui changeait entre Python = 3,4 et 3,5

74
Antti Haapala