Generator als Funktionsargument

81

Kann jemand erklären, warum das Übergeben eines Generators als einziges Positionsargument an eine Funktion spezielle Regeln zu haben scheint?

Wenn wir haben:

>>> def f(*args):
>>>    print "Success!"
>>>    print args
  1. Dies funktioniert wie erwartet.

    >>> f(1, *[2])
    Success!
    (1, 2)
    
  2. Dies funktioniert nicht wie erwartet.

    >>> f(*[2], 1)
      File "<stdin>", line 1
    SyntaxError: only named arguments may follow *expression
    
  3. Dies funktioniert wie erwartet

    >>> f(1 for x in [1], *[2])
    Success! 
    (generator object <genexpr> at 0x7effe06bdcd0>, 2)
    
  4. Das funktioniert, aber ich verstehe nicht warum. Sollte es nicht genauso scheitern wie 2)

    >>> f(*[2], 1 for x in [1])                                               
    Success!
    (generator object <genexpr> at 0x7effe06bdcd0>, 2)
    
DeTeReR
quelle
@ DanD. f((*[2, 3]), 1)gibt Syntaxfehler bei *- könnten Sie bitte Ihren Vorschlag weiter erläutern? Die Frage ist auch nicht "wie es funktioniert", sondern "warum funktioniert es so?"
J0HN
1
Kein genaues Duplikat, aber ziemlich ähnlich: stackoverflow.com/questions/12720450/… . TL; DR scheint ein Implementierungsdetail zu sein - es funktioniert einfach so.
J0HN
2
Hinweis: Fall 2 sollte in Python 3.5+ funktionieren (aufgrund des PEP 448 )
Bakuriu
1
Python 3.5 ist aus und es wird jetzt mitgeteilt, dass der Fall 3 (eigentlich auch der Fall 4) behoben wurde. Was ist neu in Python 3.5
Antti Haapala

Antworten:

76

Sowohl 3. als auch 4. sollten in allen Python-Versionen Syntaxfehler sein. Sie haben jedoch einen Fehler gefunden, der die Python-Versionen 2.5 - 3.4 betrifft und der anschließend im Python-Issue-Tracker veröffentlicht wurde . Aufgrund des Fehlers wurde ein nicht in Klammern stehender Generatorausdruck als Argument für eine Funktion akzeptiert, wenn er nur von *argsund / oder begleitet wurde **kwargs. Während Python 2.6+ beide Fälle 3. und 4. erlaubte, erlaubte Python 2.5 nur Fall 3. - doch beide waren gegen die dokumentierte Grammatik :

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

dh die Dokumentation sagt ein Funktionsaufruf besitzt primary(der Ausdruck, der zu einer auswertet aufrufbaren), gefolgt von der in Klammern entweder einer Argumentliste oder nur ein unparenthesized Generator Ausdruck; und innerhalb der Argumentliste müssen alle Generatorausdrücke in Klammern stehen.


Dieser Fehler (obwohl er anscheinend nicht bekannt war) wurde in Python 3.5-Vorabversionen behoben. In Python 3.5 sind immer Klammern um einen Generatorausdruck erforderlich, es sei denn, dies ist das einzige Argument für die Funktion:

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

Dies ist jetzt in den Neuigkeiten in Python 3.5 dokumentiert , da DeTeReR diesen Fehler entdeckt hat.


Analyse des Fehlers

In Python 2.6 wurde eine Änderung vorgenommen, die die Verwendung von Schlüsselwortargumenten nach *args folgenden Bedingungen ermöglichte :

Es ist auch legal geworden, Schlüsselwortargumente nach einem * args-Argument für einen Funktionsaufruf bereitzustellen.

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

Bisher war dies ein Syntaxfehler. (Beitrag von Amaury Forgeot d'Arc; Ausgabe 3473.)


Die Python 2.6- Grammatik unterscheidet jedoch nicht zwischen Schlüsselwortargumenten, Positionsargumenten oder bloßen Generatorausdrücken - sie sind alle vom Typ argumentfür den Parser.

Gemäß den Python-Regeln muss ein Generatorausdruck in Klammern gesetzt werden, wenn dies nicht das einzige Argument für die Funktion ist. Dies wird validiert in 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;
}

Allerdings ist diese Funktion nicht die betrachtet *argsüberhaupt - es speziell schaut nur für gewöhnliche Positionsargumente und Keyword - Argumente.

Weiter unten in derselben Funktion wird eine Fehlermeldung für Nicht-Schlüsselwort arg nach Schlüsselwort arg generiert :

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

Dies gilt jedoch wiederum für Argumente, bei denen es sich nicht um nicht parästhesierte Generatorausdrücke handelt, wie aus der else ifAussage hervorgeht :

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

Somit wurde ein nicht parästhesierter Generatorausdruck passieren gelassen.


In Python 3.5 kann man jetzt die *argsbeliebige Stelle in einem Funktionsaufruf verwenden, daher wurde die Grammatik geändert, um dies zu berücksichtigen:

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

und

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

und die forSchleife wurde geändert in

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

So wird der Fehler behoben.

Die unbeabsichtigte Änderung ist jedoch, dass die Konstruktionen gültig aussehen

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

und

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

wo ein nicht parästhesierter Generator vorausgeht *argsoder **kwargsjetzt nicht mehr funktioniert.


Um diesen Fehler zu finden, habe ich verschiedene Python-Versionen ausprobiert. In 2.5 erhalten Sie 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])

Und dies wurde vor einer Vorabversion von Python 3.5 behoben:

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

Der Generatorausdruck in Klammern funktioniert jedoch in Python 3.5, jedoch nicht in Python 3.4:

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

Und das ist der Hinweis. In Python 3.5 *splattingist das verallgemeinert; Sie können es überall in einem Funktionsaufruf verwenden:

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

Der eigentliche Fehler (Generator, der *starohne Klammern arbeitet) wurde tatsächlich in Python 3.5 behoben, und der Fehler konnte darin gefunden werden, was sich zwischen Python 3.4 und 3.5 geändert hat

Antti Haapala
quelle
1
Es ist nicht in 3.5 behoben - legen Sie einfach Parens um den Generator und das Verhalten ist das gleiche.
Viraptor
1
@viraptor guter Punkt, in 3.4 gibt der Ausdruck in Klammern einen Fehler
Antti Haapala
huh? Laufen am 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]))ist ein Syntaxfehler in Python 3.4. Es ist gültig in Python 3.5.
Antti Haapala
Ich würde diese Antwort vergolden, wenn ich könnte, danke für die Aufnahme der relevanten C-Quelle!
Nick Sweeting