Generador como argumento de función


81

¿Alguien puede explicar por qué pasar un generador como único argumento posicional a una función parece tener reglas especiales?

Si tenemos:

>>> def f(*args):
>>>    print "Success!"
>>>    print args
  1. Esto funciona, como se esperaba.

    >>> f(1, *[2])
    Success!
    (1, 2)
    
  2. Esto no funciona como se esperaba.

    >>> f(*[2], 1)
      File "<stdin>", line 1
    SyntaxError: only named arguments may follow *expression
    
  3. Esto funciona, como se esperaba

    >>> f(1 for x in [1], *[2])
    Success! 
    (generator object <genexpr> at 0x7effe06bdcd0>, 2)
    
  4. Esto funciona, pero no entiendo por qué. ¿No debería fallar de la misma manera que 2)

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

@DanD. f((*[2, 3]), 1)da un error de sintaxis en *- ¿podría explicar más su sugerencia? Además, la pregunta no es "cómo hacer que funcione", sino "¿por qué funciona así?"
J0HN

1
No es un duplicado exacto, pero es bastante similar: stackoverflow.com/questions/12720450/… . TL; DR parece que es un detalle de implementación, simplemente funciona así.
J0HN

2
Nota: el caso 2 debería funcionar en Python 3.5+ (debido al PEP 448 )
Bakuriu

1
Python 3.5 está disponible, y ahora dice que el caso 3 (en realidad también el caso 4) ha sido arreglado. Novedades de Python 3.5
Antti Haapala

Respuestas:


76

Tanto 3. como 4. deberían ser errores de sintaxis en todas las versiones de Python. Sin embargo, ha encontrado un error que afecta a las versiones 2.5 - 3.4 de Python y que posteriormente se publicó en el rastreador de problemas de Python . Debido al error, se aceptó una expresión generadora sin paréntesis como argumento para una función si estaba acompañada solo por *argsy / o **kwargs. Mientras que Python 2.6+ permitía ambos casos 3. y 4., Python 2.5 solo permitía el caso 3. - sin embargo, ambos estaban en contra de la gramática documentada :

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

es decir, la documentación dice una llamada de función dispone de primary(la expresión que se evalúa como una opción de rescate), seguidos por, entre paréntesis, o bien una lista de argumentos o sólo una expresión generador unparenthesized; y dentro de la lista de argumentos, todas las expresiones del generador deben estar entre paréntesis.


Este error (aunque parece que no se conocía), se había corregido en las versiones preliminares de Python 3.5. En Python 3.5 siempre se requieren paréntesis alrededor de una expresión generadora, a menos que sea el único argumento para la función:

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

Esto ahora está documentado en Novedades de Python 3.5 , gracias a que DeTeReR detectó este error.


Análisis del error

Se realizó un cambio en Python 2.6 que permitió el uso de argumentos de palabras clave después de *args :

También es legal proporcionar argumentos de palabras clave después de un argumento * args a una llamada de función.

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

Anteriormente, esto habría sido un error de sintaxis. (Contribuido por Amaury Forgeot d'Arc; número 3473.)


Sin embargo, la gramática de Python 2.6 no hace ninguna distinción entre argumentos de palabras clave, argumentos posicionales o expresiones generadoras simples; todos son de tipo argumentpara el analizador.

Según las reglas de Python, una expresión generadora debe estar entre paréntesis si no es el único argumento de la función. Esto está validado en 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;
}

Sin embargo, esta función no tiene en cuenta *argsen absoluto; específicamente, solo busca argumentos posicionales ordinarios y argumentos de palabras clave.

Más abajo en la misma función, hay un mensaje de error generado para arg que no son palabras clave después de la palabra clave arg :

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;
        }
        ...

Pero esto nuevamente se aplica a argumentos que no son expresiones generadoras sin paréntesis, como lo demuestra la else ifdeclaración :

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

Por tanto, se permitió que pasara una expresión generadora sin paréntesis.


Ahora, en Python 3.5, se puede usar *argsen cualquier lugar de una llamada de función, por lo que se cambió la gramática para adaptarse a esto:

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

y

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

y el forbucle se cambió a

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++;
    }
}

Arreglando así el error.

Sin embargo, el cambio inadvertido es que las construcciones de aspecto válido

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

y

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

donde un generador sin paréntesis precede *argso **kwargsahora dejó de funcionar.


Para localizar este error, probé varias versiones de Python. En 2.5 obtendrías 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])

Y esto se solucionó antes de alguna versión preliminar 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

Sin embargo, la expresión generadora entre paréntesis, funciona en Python 3.5, pero no funciona en Python 3.4:

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

Y esta es la pista. En Python 3.5 el *splattingestá generalizado; puede usarlo en cualquier lugar en una llamada de función:

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

Entonces, el error real (el generador funciona *starsin paréntesis) se corrigió en Python 3.5, y el error se pudo encontrar en lo que cambió entre Python 3.4 y 3.5


1
No está arreglado en 3.5, simplemente coloque los paréntesis alrededor del generador y el comportamiento es el mismo.
viraptor

1
@viraptor buen punto, en 3.4 la expresión entre paréntesis da un error
Antti Haapala

eh Ejecutando 3.4.3: f(*[1], 1 for x in [1])=>(<generator object <genexpr> at 0x7fa56c889288>, 1)
viraptor

@viraptor f(*[1], (1 for x in [1]))es un error de sintaxis en Python 3.4. Es válido en Python 3.5.
Antti Haapala

Doraría esta respuesta si pudiera, ¡gracias por incluir la fuente C relevante!
Nick Sweeting
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.