Trình tạo dưới dạng đối số hàm


81

Bất cứ ai có thể giải thích tại sao việc truyền một trình tạo làm đối số vị trí duy nhất cho một hàm dường như có các quy tắc đặc biệt?

Nếu chúng ta có:

>>> def f(*args):
>>>    print "Success!"
>>>    print args
  1. Điều này hoạt động, như mong đợi.

    >>> f(1, *[2])
    Success!
    (1, 2)
    
  2. Điều này không hoạt động, như mong đợi.

    >>> f(*[2], 1)
      File "<stdin>", line 1
    SyntaxError: only named arguments may follow *expression
    
  3. Điều này hoạt động, như mong đợi

    >>> f(1 for x in [1], *[2])
    Success! 
    (generator object <genexpr> at 0x7effe06bdcd0>, 2)
    
  4. Điều này hoạt động, nhưng tôi không hiểu tại sao. Nó sẽ không thất bại theo cách tương tự như 2)

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

@DanD. f((*[2, 3]), 1)đưa ra lỗi cú pháp tại *- bạn có thể vui lòng giải thích thêm đề xuất của mình không? Ngoài ra, câu hỏi không phải là "làm thế nào để làm cho nó hoạt động", mà là "tại sao nó hoạt động như thế này?"
J0HN

1
Không phải là một bản sao chính xác, nhưng khá giống nhau: stackoverflow.com/questions/12720450/… . TL; DR có vẻ như đó là một chi tiết triển khai - nó chỉ hoạt động như vậy.
J0HN

2
Lưu ý: trường hợp 2 sẽ hoạt động trong python 3.5+ (do PEP 448 )
Bakuriu

1
Python 3.5 đã ra mắt và bây giờ nó cho biết rằng trường hợp 3 (thực ra cũng là trường hợp 4) đã được sửa. Có gì mới trong Python 3.5
Antti Haapala

Câu trả lời:


76

Cả 3. và 4. phải là lỗi cú pháp trên tất cả các phiên bản Python. Tuy nhiên, bạn đã tìm thấy một lỗi ảnh hưởng đến phiên bản Python 2.5 - 3.4 và lỗi này sau đó đã được đăng lên trình theo dõi sự cố Python . Do lỗi, một biểu thức trình tạo không có dấu ngoặc đơn được chấp nhận làm đối số cho một hàm nếu nó chỉ được đi kèm với *argsvà / hoặc **kwargs. Trong khi Python 2.6+ cho phép cả hai trường hợp 3. và 4., Python 2.5 chỉ cho phép trường hợp 3. - nhưng cả hai đều chống lại ngữ pháp được ghi lại :

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

tức là các tài liệu nói một chức năng gọi bao gồm primary(các biểu thức đánh giá lại để một callable), tiếp theo là, trong dấu ngoặc đơn, hoặc một danh sách đối số hoặc chỉ là một biểu hiện phát unparenthesized; và trong danh sách đối số, tất cả các biểu thức trình tạo phải nằm trong dấu ngoặc đơn.


Lỗi này (mặc dù có vẻ như nó chưa được biết đến), đã được sửa trong các cơ sở tiên quyết của Python 3.5. Trong Python 3.5, dấu ngoặc đơn luôn được yêu cầu xung quanh một biểu thức trình tạo, trừ khi nó là đối số duy nhất của hàm:

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

Điều này hiện đã được ghi lại trong What's New in Python 3.5 , nhờ DeTeReR đã phát hiện ra lỗi này.


Phân tích lỗi

Đã có một thay đổi được thực hiện đối với Python 2.6 cho phép sử dụng các đối số từ khóa sau *args :

Việc cung cấp các đối số từ khóa sau đối số * args cho một lệnh gọi hàm cũng trở nên hợp pháp.

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

Trước đây đây sẽ là một lỗi cú pháp. (Đóng góp bởi Amaury Forgeot d'Arc; số 3473.)


Tuy nhiên, ngữ pháp Python 2.6 không có bất kỳ sự phân biệt nào giữa các đối số từ khóa, đối số vị trí hoặc biểu thức trình tạo trần - tất cả chúng đều thuộc loại argumentđối với trình phân tích cú pháp.

Theo các quy tắc Python, một biểu thức trình tạo phải được đặt trong ngoặc đơn nếu nó không phải là đối số duy nhất của hàm. Điều này được xác thực trong 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;
}

Tuy nhiên, hàm này hoàn toàn không xem xét *args- nó đặc biệt chỉ tìm kiếm các đối số vị trí thông thường và các đối số từ khóa.

Xa hơn nữa trong cùng một hàm, có một thông báo lỗi được tạo ra cho đối số không phải từ khóa sau từ khóa đối số :

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

Nhưng điều này lại áp dụng cho các đối số không phải là biểu thức trình tạo không dấu ngoặc đơn như được minh chứng bằng else ifcâu lệnh :

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

Do đó, một biểu thức bộ tạo không có dấu ngoặc đơn được phép bỏ qua.


Bây giờ trong Python 3.5, người ta có thể sử dụng *argsbất kỳ đâu trong một lệnh gọi hàm, vì vậy Ngữ pháp đã được thay đổi để phù hợp với điều này:

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

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

forvòng lặp đã được thay đổi thành

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

Do đó, sửa lỗi.

Tuy nhiên, thay đổi vô tình là các công trình trông hợp lệ

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

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

trong đó trình tạo không có dấu ngoặc đơn trước *argshoặc **kwargshiện đã ngừng hoạt động.


Để xác định lỗi này, tôi đã thử các phiên bản Python khác nhau. Trong 2,5, bạn sẽ nhận được 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])

Và điều này đã được khắc phục trước một số phát hành trước của 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

Tuy nhiên, biểu thức trình tạo dấu ngoặc đơn, nó hoạt động trong Python 3.5, nhưng nó không hoạt động không trong Python 3.4:

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

Và đây là manh mối. Trong Python 3.5, *splattingđược tổng quát hóa; bạn có thể sử dụng nó ở bất kỳ đâu trong một lệnh gọi hàm:

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

Vì vậy, lỗi thực tế (trình tạo hoạt động với *starkhông có dấu ngoặc đơn) thực sự đã được sửa trong Python 3.5 và lỗi có thể được tìm thấy trong đó những gì đã thay đổi giữa Python 3.4 và 3.5


1
Nó không cố định trong 3.5 - chỉ cần đặt parens xung quanh trình tạo và hành vi vẫn giống nhau.
viraptor

1
@viraptor điểm tốt, trong 3,4 các ngoặc biểu hiện cho một lỗi
Antti Haapala

Huh? Chạy trên 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]))là lỗi cú pháp trên Python 3.4. Nó hợp lệ trong Python 3.5.
Antti Haapala

Tôi sẽ mạ vàng câu trả lời này nếu tôi có thể, cảm ơn vì đã bao gồm nguồn C liên quan!
Nick Sweeting
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.