2009-10-30 27 views
32

Tôi đã có một thời gian rất khó khăn với sự hiểu biết nguyên nhân gốc rễ của một vấn đề trong một thuật toán. Sau đó, bằng cách đơn giản hóa các chức năng từng bước tôi phát hiện ra rằng việc đánh giá các đối số mặc định trong Python không hoạt động như tôi mong đợi.Tại sao các đối số mặc định được đánh giá tại thời gian định nghĩa bằng Python?

Mã này là như sau:

class Node(object): 
    def __init__(self, children = []): 
     self.children = children 

Vấn đề là mọi trường hợp cổ phiếu lớp Node cùng children thuộc tính, nếu các thuộc tính không được đưa ra một cách rõ ràng, chẳng hạn như:

>>> n0 = Node() 
>>> n1 = Node() 
>>> id(n1.children) 
Out[0]: 25000176 
>>> id(n0.children) 
Out[0]: 25000176 

Tôi không hiểu logic của quyết định thiết kế này? Tại sao các nhà thiết kế Python quyết định rằng các đối số mặc định được đánh giá ở thời gian định nghĩa? Điều này có vẻ rất trực quan đối với tôi.

+0

Dự đoán của tôi sẽ là hiệu suất. Hãy tưởng tượng đánh giá lại mỗi khi một hàm được gọi nếu nó được gọi là 15 triệu lần một ngày. –

Trả lời

38

Phương án thay thế sẽ khá nặng - lưu trữ "giá trị đối số mặc định" trong đối tượng hàm như "khối" của mã được thực hiện lặp đi lặp lại mỗi lần hàm được gọi mà không có giá trị được chỉ định cho đối số đó - và sẽ làm cho nó khó khăn hơn nhiều để có được ràng buộc sớm (ràng buộc tại thời gian def), mà thường là những gì bạn muốn. Ví dụ: bằng Python khi nó tồn tại:

def ack(m, n, _memo={}): 
    key = m, n 
    if key not in _memo: 
    if m==0: v = n + 1 
    elif n==0: v = ack(m-1, 1) 
    else: v = ack(m-1, ack(m, n-1)) 
    _memo[key] = v 
    return _memo[key] 

... viết một chức năng ghi nhớ như trên là một nhiệm vụ khá cơ bản.Tương tự như vậy:

for i in range(len(buttons)): 
    buttons[i].onclick(lambda i=i: say('button %s', i)) 

... đơn giản i=i, dựa trên (thời gian định nghĩa) sớm ràng buộc của các giá trị arg mặc định, là một cách đơn giản để có được trivially đầu ràng buộc. Vì vậy, quy tắc hiện tại đơn giản, đơn giản và cho phép bạn làm tất cả những gì bạn muốn theo cách rất dễ giải thích và hiểu: nếu bạn muốn gắn kết muộn giá trị của một biểu thức, hãy đánh giá biểu thức đó trong thân hàm; nếu bạn muốn kết nối sớm, hãy đánh giá nó như là giá trị mặc định của một arg.

Cách khác, buộc ràng buộc trễ cho cả hai trường hợp, sẽ không cung cấp tính linh hoạt này và sẽ buộc bạn phải trải qua các vòng lặp (chẳng hạn như gói hàm của bạn vào nhà máy đóng cửa) mỗi khi bạn cần ràng buộc sớm, như ở trên ví dụ - nhưng nặng hơn boilerplate trọng lượng buộc trên lập trình bởi quyết định thiết kế giả thiết này (ngoài những "vô hình" của việc tạo ra và liên tục đánh giá khối khắp nơi).

Nói cách khác, "Nên có một, và tốt nhất là chỉ một cách rõ ràng để làm điều đó:" khi bạn muốn kết buộc muộn, đã có một cách hoàn toàn rõ ràng để đạt được nó (vì tất cả các chức năng của mã chỉ được thực thi tại thời điểm gọi, rõ ràng là mọi thứ được đánh giá bị trễ); có đánh giá mặc định-arg sản xuất ràng buộc sớm mang đến cho bạn một cách rõ ràng để đạt được ràng buộc sớm (cộng! -) thay vì cho HAI cách rõ ràng để có được ràng buộc muộn và không có cách rõ ràng để có được ràng buộc sớm (một trừ! -).

[1]: "Mặc dù cách này có thể không rõ ràng lúc đầu trừ khi bạn là người Hà Lan".

+0

câu trả lời xuất sắc, 1 từ tôi. Một lỗi đánh máy rất nhỏ: phải là trả về _memo [key] với dấu gạch dưới hàng đầu. – Francesco

+0

@Francesco, tx để chỉ ra lỗi đánh máy (và tôi tưởng tượng tx @novelocrat để sửa chữa nhanh chóng! -). –

+0

Liệu chi phí vẫn có thể bị cấm trong trường hợp sâu thay vì đánh giá chậm? –

0

Định nghĩa hàm Python chỉ là mã, giống như tất cả các mã khác; chúng không phải là "huyền diệu" theo cách mà một số ngôn ngữ. Ví dụ, trong Java bạn có thể tham khảo "bây giờ" một cái gì đó định nghĩa "sau":

public static void foo() { bar(); } 
public static void main(String[] args) { foo(); } 
public static void bar() {} 

nhưng trong Python

def foo(): bar() 
foo() # boom! "bar" has no binding yet 
def bar(): pass 
foo() # ok 

Vì vậy, đối số mặc định được đánh giá vào lúc này rằng dòng mã được đánh giá!

+1

Tương tự không hợp lệ. Tương đương pythonic với mẫu java của bạn đang chèn 'if __name__ == '__main__': main()' vào cuối tập tin – hasen

7

Cách giải quyết cho điều này, discussed here (và rất rắn), là:

class Node(object): 
    def __init__(self, children = None): 
     self.children = [] if children is None else children 

Đối với lý do tại sao tìm kiếm một câu trả lời từ von Löwis, nhưng nó có khả năng bởi vì định nghĩa hàm làm cho một đối tượng đang do kiến trúc của Python, và có thể không có một cơ sở để làm việc với các kiểu tham chiếu như thế này trong các đối số mặc định.

+1

Hi Jed, có thể có một số vấn đề (hiếm) khi đầu vào khác hơn [] có thể xảy ra Sai. Sau đó, một đầu vào hợp pháp có thể được chuyển thành []. Tất nhiên điều này không thể xảy ra miễn là chilren phải là một danh sách. – Juergen

+0

... của quên: Tổng quát hơn sẽ là "nếu trẻ em là Không ..." – Juergen

+0

"nếu trẻ em là Không: trẻ em = []" (theo sau là "self.children = children" ở đây) là tương đương (gần như- --Các giá trị biến đổi sẽ khác nhau) và dễ đọc hơn nhiều. –

7

Tất nhiên trong trường hợp của bạn khó hiểu. Nhưng bạn phải xem, đánh giá mặc định args mỗi lần sẽ đặt một gánh nặng thời gian chạy nặng trên hệ thống.

Ngoài ra bạn nên biết, rằng trong trường hợp của các loại container vấn đề này có thể xảy ra - nhưng bạn có thể phá vỡ nó bằng cách làm điều rõ ràng:

def __init__(self, children = None): 
    if children is None: 
     children = [] 
    self.children = children 
+3

bạn cũng có thể rút ngắn nó thành 'self.children = children hoặc []' thay vì có câu lệnh if. –

+0

Nếu tôi gọi nó bằng (trẻ em = Không) thì sao? Sau đó nó sẽ tạo không chính xác trẻ em = []. Để sửa lỗi này, cần phải sử dụng giá trị sentinel. –

+0

Trong trường hợp này tôi âm thầm giả định rằng Không có giá trị sentinel thích hợp. Tất nhiên, nếu None có thể là một giá trị hợp lệ (trong trường hợp của trẻ em (rất có thể là một danh sách những thứ) không), một giá trị sentinel khác nhau phải được sử dụng. Nếu không có giá trị chuẩn nào tồn tại, hãy sử dụng một đối tượng được tạo đặc biệt cho điều này. – Juergen

4

này xuất phát từ tầm quan trọng của python về cú pháp và thực hiện đơn giản. một tuyên bố def xảy ra tại một thời điểm nhất định trong quá trình thực hiện. Khi trình thông dịch python đạt đến điểm đó, nó sẽ đánh giá mã trong dòng đó và sau đó tạo một đối tượng mã từ phần thân của hàm, hàm sẽ chạy sau này, khi bạn gọi hàm.

Đó là sự phân chia đơn giản giữa khai báo chức năng và nội dung chức năng. Việc khai báo được thực hiện khi nó đạt được trong mã. Cơ thể được thực hiện tại thời gian gọi. Lưu ý rằng việc khai báo được thực hiện mỗi khi nó được đạt tới, vì vậy bạn có thể tạo nhiều hàm bằng cách lặp.

funcs = [] 
for x in xrange(5): 
    def foo(x=x, lst=[]): 
     lst.append(x) 
     return lst 
    funcs.append(foo) 
for func in funcs: 
    print "1: ", func() 
    print "2: ", func() 

Năm chức năng riêng biệt được tạo ra, với một danh sách riêng được tạo mỗi lần khai báo hàm được thực thi. Trên mỗi vòng lặp thông qua funcs, cùng một chức năng được thực thi hai lần trên mỗi lần truyền qua, sử dụng cùng một danh sách mỗi lần.Điều này sẽ cho kết quả:

1: [0] 
2: [0, 0] 
1: [1] 
2: [1, 1] 
1: [2] 
2: [2, 2] 
1: [3] 
2: [3, 3] 
1: [4] 
2: [4, 4] 

Những người khác đã đưa cho bạn những cách giải quyết, sử dụng param = None, và gán một danh sách trong cơ thể nếu giá trị là Không, đó là hoàn toàn thành ngữ python. Đó là một chút xấu xí, nhưng sự đơn giản là mạnh mẽ, và cách giải quyết không quá đau đớn.

Edited thêm: Để thảo luận thêm về điều này, xem bài viết effbot ở đây: http://effbot.org/zone/default-values.htm, và các tài liệu tham khảo ngôn ngữ, ở đây: http://docs.python.org/reference/compound_stmts.html#function

10

Vấn đề là thế này.

Quá đắt để đánh giá chức năng dưới dạng bộ khởi tạo mỗi lần hàm được gọi là.

  • 0 là một chữ đơn giản. Đánh giá nó một lần, sử dụng nó mãi mãi.

  • int là một chức năng (như danh sách) sẽ phải được đánh giá mỗi khi được yêu cầu làm bộ khởi tạo.

Cấu trúc [] là chữ, như 0, có nghĩa là "đối tượng chính xác này".

Vấn đề là một số người hy vọng rằng nó có nghĩa là list như trong "đánh giá chức năng này cho tôi, xin vui lòng, để có được đối tượng đó là initializer".

Sẽ là một gánh nặng để thêm câu hỏi if cần thiết để thực hiện đánh giá này mọi lúc. Tốt hơn là lấy tất cả các đối số dưới dạng chữ và không thực hiện bất kỳ đánh giá chức năng bổ sung nào như là một phần của việc cố gắng thực hiện đánh giá chức năng.

Ngoài ra, về cơ bản hơn, về mặt kỹ thuật, có thể là không thể để triển khai mặc định đối số làm đánh giá chức năng.

Hãy xem xét, trong một khoảnh khắc kinh dị đệ quy của loại hình tròn này. Giả sử rằng thay vì các giá trị mặc định là literals, chúng ta cho phép chúng là các hàm được đánh giá mỗi khi giá trị mặc định của tham số được yêu cầu.

[Điều này sẽ song song với đường collections.defaultdict công trình.]

def aFunc(a=another_func): 
    return a*2 

def another_func(b=aFunc): 
    return b*3 

giá trị của another_func() là gì? Để nhận được mặc định cho b, nó phải đánh giá aFunc, yêu cầu đánh giá là another_func. Rất tiếc.

+0

+1 cho kinh dị thông tư – hasen

+3

Tôi nhận được phần "nó sẽ là tốn kém", nhưng phần "nó không thể" tôi không nhận được nó. Nó không thể là không thể khi có các ngôn ngữ động được giải thích khác làm điều đó –

5

Tôi nghĩ điều này cũng phản trực giác, cho đến khi tôi biết cách Python triển khai đối số mặc định.

Một chức năng là một đối tượng. Tại thời gian tải, Python tạo đối tượng hàm, đánh giá các giá trị mặc định trong câu lệnh def, đặt chúng thành một bộ và thêm bộ dữ liệu đó làm thuộc tính của hàm có tên func_defaults. Sau đó, khi một hàm được gọi, nếu cuộc gọi không cung cấp giá trị, Python sẽ lấy giá trị mặc định ra khỏi func_defaults.

Ví dụ:

>>> class C(): 
     pass 

>>> def f(x=C()): 
     pass 

>>> f.func_defaults 
(<__main__.C instance at 0x0298D4B8>,) 

Vì vậy, tất cả các cuộc gọi đến f mà không cung cấp một cuộc tranh cãi sẽ sử dụng cùng một ví dụ của C, bởi vì đó là giá trị mặc định.

Theo như lý do tại sao Python thực hiện theo cách này: tốt, tuple có thể chứa các hàm sẽ được gọi mỗi khi giá trị đối số mặc định là cần thiết. Ngoài vấn đề hiệu suất hiển nhiên ngay lập tức, bạn bắt đầu đi vào một trường hợp đặc biệt, như lưu trữ các giá trị bằng chữ thay cho các hàm không thể thay đổi để tránh các cuộc gọi hàm không cần thiết. Và tất nhiên có những hàm ý hiệu suất lớn.

Hành vi thực tế thực sự đơn giản. Và có một workaround tầm thường, trong trường hợp bạn muốn một giá trị mặc định được sản xuất bởi một cuộc gọi chức năng trong thời gian chạy:

def f(x = None): 
    if x == None: 
     x = g() 
0

Bởi vì nếu họ đã có, sau đó ai đó sẽ gửi một câu hỏi thắc mắc tại sao nó wasn' cách khác xung quanh :-p

Giả sử bây giờ đã có. Làm thế nào bạn sẽ thực hiện các hành vi hiện tại nếu cần thiết?Thật dễ dàng để tạo các đối tượng mới bên trong một hàm, nhưng bạn không thể "không tạo" chúng (bạn có thể xóa chúng, nhưng nó không giống nhau).

Các vấn đề liên quan