1. ホーム
  2. python

[解決済み] 関数の引数としてのジェネレータ

2023-07-17 23:12:02

質問

なぜジェネレータを関数への唯一の位置引数として渡すと、特別なルールがあるように見えるのか、誰か説明できますか?

もし、私たちが

def f(*args):
    print "Success!"
    print args

  1. これは予想通り、動作します。

    >>> f(1, *[2])
    Success!
    (1, 2)
    
    
  2. これは予想通りうまくいきません。

    >>> f(*[2], 1)
      File "<stdin>", line 1
    SyntaxError: only named arguments may follow *expression
    
    
  3. これは予想通り、動作します

    >>> f(1 for x in [1], *[2])
    Success! 
    (generator object <genexpr> at 0x7effe06bdcd0>, 2)
    
    
  4. これは動作しますが、なぜか理解できません。2)と同じように失敗してはいけないのでしょうか?

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

どのように解決するのですか?

3.と4.の両方が表示されない。 は全てのPythonのバージョンでシンタックスエラーになります。 しかし、あなたは Python バージョン 2.5 - 3.4 に影響するバグを見つけ、そのバグがその後 Python issue tracker に投稿されました。 . このバグのために、親レスポンス化されていないジェネレータ式は、関数への引数に *args**kwargs . Python 2.6+ ではケース 3. と 4. の両方が許可されていましたが、 Python 2.5 ではケース 3. のみでした。 の文書化された文法に反しています。 :

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

つまり、ドキュメントによれば、関数呼び出しは以下のものから構成されます。 primary (callableとして評価される式)と、その後に括弧で囲まれた のどちらかです。 引数リスト または は、括弧で囲まれていないジェネレータ式です。 で、引数リスト内では、すべてのジェネレータ式は括弧でくくられなければなりません。


このバグは(知られていなかったようですが)、Python 3.5 prereleases で修正されました。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 for i in [42], *a)
  File "<stdin>", line 1
SyntaxError: Generator expression must be parenthesized if not sole argument

これは現在、文書化された Python 3.5 の新機能 に記載されるようになりました。このバグを発見した DeTeReR に感謝します。


バグの解析

Python 2.6では、以下のような変更がありました。 キーワード引数の使用を許可する の後に *args :

また、関数呼び出しの *args の後にキーワード引数を与えることも合法になりました。

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

以前は、これは構文エラーだったでしょう。(Contributed by Amaury Forgeot d'Arc、3473号)。


ただし、Python 2.6 の 文法 はキーワード引数、位置引数、あるいは素のジェネレータ式の区別をしません - これらはすべて argument というタイプです。

Pythonの規則に従って、ジェネレータ式は、それが関数への唯一の引数でない場合、括弧でくくられなければなりません。これは 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;
}

しかし、この関数は ではなく を考慮しません。 *args を全く考慮せず、特に通常の位置引数とキーワード引数のみを探します。

同じ関数のさらに下のほうで、エラーメッセージが キーワード引数の後に非キーワード引数 :

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

しかし、これはまた、引数が ではない のように親レス化されていないジェネレータ式は によって証明されます。 else if ステートメント :

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

このように、親レスポンス化されていないジェネレータ式が通過することが許可されました。


Python 3.5では、この問題を解決するために *args を関数呼び出しのどこでも使うことができるようになりました。 は 文法 はこれに対応するために変更されました。

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

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

とし for ループが変更され から

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

こうしてバグを修正する。

しかし、不注意な変更として、有効な外観の構文である

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

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

ここで、親レスポンス化されていないジェネレータは、その前に *args または **kwargs は動作しなくなりました。


このバグを見つけるために、私はさまざまなPythonのバージョンを試しました。2.5 では、次のようになります。 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])

そして、これは 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

しかし、括弧付きのジェネレータ式は、Python 3.5では動作するが、Python 3.4では動作しない。

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

そして、これがヒントです。Python 3.5では *splatting は一般化され、関数呼び出しのどこでも使うことができます。

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

というわけで、実際のバグ(ジェネレータが *star を括弧無しで使う) は確かに Python 3.5 で修正され、バグは Python 3.4 と 3.5 の間で何が変わったか、ということで見つけることができました。