Ít nhất là ngạc nhiên và một cuộc tranh cãi mặc định


2594

Bất cứ ai cũng mày mò với Python đủ lâu đã bị cắn (hoặc bị xé thành từng mảnh) bởi vấn đề sau:

def foo(a=[]):
    a.append(5)
    return a

Người mới dùng Python sẽ mong đợi hàm này luôn trả về một danh sách chỉ có một phần tử : [5]. Thay vào đó, kết quả rất khác biệt và rất đáng kinh ngạc (đối với người mới):

>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
>>> foo()
[5, 5, 5, 5]
>>> foo()

Một người quản lý của tôi đã có lần gặp gỡ đầu tiên với tính năng này và gọi nó là "lỗ hổng thiết kế ấn tượng" của ngôn ngữ. Tôi đã trả lời rằng hành vi đó có một lời giải thích cơ bản, và nó thực sự rất khó hiểu và bất ngờ nếu bạn không hiểu nội bộ. Tuy nhiên, tôi không thể trả lời (cho chính mình) câu hỏi sau: lý do ràng buộc đối số mặc định ở định nghĩa hàm và không phải khi thực hiện chức năng là gì? Tôi nghi ngờ hành vi có kinh nghiệm có một cách sử dụng thực tế (ai thực sự sử dụng biến tĩnh trong C, không có lỗi sinh sản?)

Chỉnh sửa :

Baczek đã làm một ví dụ thú vị. Cùng với hầu hết các bình luận của bạn và của Utaal nói riêng, tôi đã nói rõ hơn:

>>> def a():
...     print("a executed")
...     return []
... 
>>>            
>>> def b(x=a()):
...     x.append(5)
...     print(x)
... 
a executed
>>> b()
[5]
>>> b()
[5, 5]

Đối với tôi, dường như quyết định thiết kế có liên quan đến nơi đặt phạm vi của các tham số: bên trong hàm hoặc "cùng" với nó?

Thực hiện liên kết bên trong hàm có nghĩa xlà được ràng buộc một cách hiệu quả với mặc định đã chỉ định khi hàm được gọi, không được xác định, một cái gì đó sẽ có một lỗ hổng sâu: defdòng sẽ là "lai" theo nghĩa là một phần của liên kết (của đối tượng hàm) sẽ xảy ra ở định nghĩa và một phần (gán tham số mặc định) tại thời điểm gọi hàm.

Hành vi thực tế phù hợp hơn: mọi thứ của dòng đó được đánh giá khi dòng đó được thực thi, nghĩa là tại định nghĩa hàm.



4
Tôi không nghi ngờ những lập luận có thể thay đổi vi phạm nguyên tắc kinh ngạc ít nhất đối với một người bình thường và tôi đã thấy những người mới bắt đầu bước tới đó, sau đó anh hùng thay thế danh sách gửi thư bằng các bộ thư. Tuy nhiên, các đối số có thể thay đổi vẫn phù hợp với Python Zen (Pep 20) và rơi vào mệnh đề "hiển nhiên đối với tiếng Hà Lan" (được hiểu / khai thác bởi các lập trình viên python lõi cứng). Cách giải quyết được đề xuất với chuỗi doc là tốt nhất, nhưng khả năng chống lại chuỗi doc và mọi tài liệu (bằng văn bản) hiện nay không quá phổ biến. Cá nhân, tôi thích một người trang trí (giả sử @fixed_defaults).
Serge

5
Lập luận của tôi khi tôi bắt gặp điều này là: "Tại sao bạn cần tạo một hàm trả về một biến đổi có thể tùy ý có thể là một biến đổi mà bạn sẽ chuyển sang hàm đó? để làm cả hai với một hàm? Và tại sao trình thông dịch phải được viết lại để cho phép bạn làm điều đó mà không cần thêm ba dòng vào mã của bạn? " Bởi vì chúng ta đang nói về việc viết lại cách trình thông dịch xử lý các định nghĩa và gợi ý hàm ở đây. Đó là rất nhiều để làm cho một trường hợp sử dụng hầu như không cần thiết.
Alan Leuthard

12
"Người mới Python sẽ mong chức năng này luôn trả về một danh sách chỉ có một phần tử : [5]." Tôi là người mới làm quen với Python và tôi sẽ không mong đợi điều này, vì rõ ràng foo([1])sẽ quay trở lại [1, 5]chứ không phải [5]. Điều bạn muốn nói là một người mới sẽ mong đợi hàm được gọi không có tham số sẽ luôn trả về [5].
symplectomorphic

2
Câu hỏi này hỏi "Tại sao điều này [sai cách] được thực hiện như vậy?" Nó không hỏi "Cách nào đúng?" , được bao phủ bởi [ Tại sao sử dụng arg = Không khắc phục được sự cố đối số mặc định có thể thay đổi của Python? ] * ( stackoverflow.com/questions/10676729/ mang ). Người dùng mới hầu như luôn ít quan tâm đến cái trước và nhiều hơn về cái sau, vì vậy đôi khi đó là một liên kết / bản sao rất hữu ích để trích dẫn.
smci

Câu trả lời:


1612

Trên thực tế, đây không phải là một lỗi thiết kế, và nó không phải là do nội bộ, hoặc hiệu suất.
Nó đơn giản xuất phát từ thực tế là các hàm trong Python là các đối tượng hạng nhất và không chỉ là một đoạn mã.

Ngay khi bạn nghĩ về cách này, thì nó hoàn toàn có ý nghĩa: một hàm là một đối tượng được đánh giá theo định nghĩa của nó; tham số mặc định là loại "dữ liệu thành viên" và do đó trạng thái của chúng có thể thay đổi từ cuộc gọi này sang cuộc gọi khác - chính xác như trong bất kỳ đối tượng nào khác.

Trong mọi trường hợp, Effbot có một lời giải thích rất hay về lý do cho hành vi này trong Giá trị tham số mặc định trong Python .
Tôi thấy nó rất rõ ràng và tôi thực sự khuyên bạn nên đọc nó để có kiến ​​thức tốt hơn về cách các đối tượng chức năng hoạt động.


80
Đối với bất kỳ ai đọc câu trả lời trên, tôi thực sự khuyên bạn nên dành thời gian để đọc qua bài viết Effbot được liên kết. Cũng như tất cả các thông tin hữu ích khác, phần về cách tính năng ngôn ngữ này có thể được sử dụng để lưu trữ kết quả / ghi nhớ rất tiện dụng để biết!
Cam Jackson

85
Ngay cả khi đó là một đối tượng hạng nhất, người ta vẫn có thể hình dung một thiết kế trong đó cho mỗi giá trị mặc định được lưu trữ cùng với đối tượng và được đánh giá lại mỗi khi hàm được gọi. Tôi không nói rằng sẽ tốt hơn, chỉ là các chức năng là đối tượng hạng nhất không hoàn toàn loại trừ nó.
gerrit

312
Xin lỗi, nhưng bất cứ điều gì được coi là "WTF lớn nhất trong Python" chắc chắn là một lỗ hổng thiết kế . Đây là một nguồn lỗi cho tất cả mọi người tại một số điểm, bởi vì không ai mong đợi hành vi đó lúc đầu - điều đó có nghĩa là nó không nên được thiết kế theo cách đó để bắt đầu. Tôi không quan tâm những gì họ phải nhảy qua, họ nên thiết kế Python để các đối số mặc định là không tĩnh.
BlueRaja - Daniel Pflughoeft

192
Cho dù đó có phải là một lỗi thiết kế hay không, câu trả lời của bạn dường như ngụ ý rằng hành vi này là cần thiết, tự nhiên và rõ ràng cho rằng các chức năng là đối tượng hạng nhất và đơn giản là không phải vậy. Python đã đóng cửa. Nếu bạn thay thế đối số mặc định bằng một phép gán trên dòng đầu tiên của hàm, nó sẽ đánh giá biểu thức mỗi cuộc gọi (có khả năng sử dụng các tên được khai báo trong phạm vi kèm theo). Không có lý do nào cả rằng sẽ không thể hoặc hợp lý khi có các đối số mặc định được đánh giá mỗi khi hàm được gọi theo cùng một cách.
Đánh dấu Amery

24
Thiết kế không trực tiếp làm theo functions are objects. Trong mô hình của bạn, đề xuất sẽ là triển khai các giá trị mặc định của hàm làm thuộc tính chứ không phải thuộc tính.
bukzor

273

Giả sử bạn có đoạn mã sau

fruits = ("apples", "bananas", "loganberries")

def eat(food=fruits):
    ...

Khi tôi thấy tuyên bố về việc ăn uống, điều đáng ngạc nhiên nhất là nghĩ rằng nếu tham số đầu tiên không được đưa ra, thì nó sẽ bằng với bộ dữ liệu ("apples", "bananas", "loganberries")

Tuy nhiên, được cho là sau này trong mã, tôi làm một cái gì đó như

def some_random_function():
    global fruits
    fruits = ("blueberries", "mangos")

sau đó nếu các tham số mặc định bị ràng buộc khi thực thi chức năng thay vì khai báo hàm thì tôi sẽ ngạc nhiên (theo cách rất tệ) khi phát hiện ra rằng trái cây đã bị thay đổi. Điều này sẽ khiến IMO đáng kinh ngạc hơn là phát hiện ra rằng foochức năng của bạn ở trên đang làm thay đổi danh sách.

Vấn đề thực sự nằm ở các biến có thể thay đổi và tất cả các ngôn ngữ đều có vấn đề này ở một mức độ nào đó. Đây là một câu hỏi: giả sử trong Java tôi có đoạn mã sau:

StringBuffer s = new StringBuffer("Hello World!");
Map<StringBuffer,Integer> counts = new HashMap<StringBuffer,Integer>();
counts.put(s, 5);
s.append("!!!!");
System.out.println( counts.get(s) );  // does this work?

Bây giờ, bản đồ của tôi có sử dụng giá trị của StringBufferkhóa khi nó được đặt vào bản đồ hay nó lưu trữ khóa theo tham chiếu? Dù bằng cách nào, một người nào đó ngạc nhiên; hoặc người đã cố gắng đưa đối tượng ra khỏi Mapgiá trị bằng cách sử dụng một giá trị giống hệt với giá trị mà họ đặt vào hoặc người dường như không thể truy xuất đối tượng của họ mặc dù khóa họ đang sử dụng là cùng một đối tượng đã được sử dụng để đưa nó vào bản đồ (đây thực sự là lý do tại sao Python không cho phép các loại dữ liệu tích hợp có thể thay đổi của nó được sử dụng làm khóa từ điển).

Ví dụ của bạn là một trường hợp tốt trong trường hợp người mới sử dụng Python sẽ ngạc nhiên và bị cắn. Nhưng tôi lập luận rằng nếu chúng ta "sửa" điều này, thì điều đó sẽ chỉ tạo ra một tình huống khác khi chúng bị cắn thay vào đó, và điều đó thậm chí sẽ ít trực quan hơn. Hơn nữa, đây luôn là trường hợp khi xử lý các biến có thể thay đổi; bạn luôn gặp phải trường hợp ai đó có thể trực giác mong đợi một hoặc hành vi ngược lại tùy thuộc vào mã họ đang viết.

Cá nhân tôi thích cách tiếp cận hiện tại của Python: các đối số hàm mặc định được đánh giá khi hàm được xác định và đối tượng đó luôn là mặc định. Tôi cho rằng họ có thể sử dụng một trường hợp đặc biệt bằng cách sử dụng một danh sách trống, nhưng loại vỏ đặc biệt đó sẽ gây ra sự ngạc nhiên hơn nữa, chưa kể đến việc không tương thích ngược.


30
Tôi nghĩ đó là vấn đề tranh luận. Bạn đang hành động trên một biến toàn cầu. Bất kỳ đánh giá nào được thực hiện ở bất kỳ đâu trong mã của bạn liên quan đến biến toàn cầu của bạn bây giờ (chính xác) sẽ đề cập đến ("quả việt quất", "quả xoài"). tham số mặc định có thể giống như bất kỳ trường hợp nào khác.
Stefano Borini

47
Trên thực tế, tôi không nghĩ rằng tôi đồng ý với ví dụ đầu tiên của bạn. Tôi không chắc chắn tôi thích ý tưởng sửa đổi trình khởi tạo như thế ngay từ đầu, nhưng nếu tôi làm vậy, tôi hy vọng nó sẽ hoạt động chính xác như bạn mô tả - thay đổi giá trị mặc định thành ("blueberries", "mangos").
Ben Trống

12
Các thông số mặc định giống như bất kỳ trường hợp khác. Điều bất ngờ là tham số này là biến toàn cục chứ không phải biến cục bộ. Đến lượt nó là do mã được thực thi tại định nghĩa hàm, không gọi. Một khi bạn có được điều đó, và điều tương tự cũng xảy ra với các lớp học, nó hoàn toàn rõ ràng.
Lennart Regebro

17
Tôi tìm thấy ví dụ sai lệch hơn là rực rỡ. Nếu some_random_function()gắn vào fruitsthay vì gán cho nó, hành vi của eat() sẽ thay đổi. Quá nhiều cho các thiết kế tuyệt vời hiện tại. Nếu bạn sử dụng một đối số mặc định được tham chiếu ở nơi khác và sau đó sửa đổi tham chiếu từ bên ngoài hàm, bạn sẽ yêu cầu sự cố. WTF thực là khi mọi người xác định một đối số mặc định mới (một danh sách bằng chữ hoặc một lệnh gọi đến hàm tạo) và vẫn nhận được bit.
alexis

13
Bạn chỉ cần tuyên bố rõ ràng globalvà gán lại bộ dữ liệu - hoàn toàn không có gì đáng ngạc nhiên nếu eathoạt động khác đi sau đó.
dùng3467349

241

Phần có liên quan của tài liệu :

Các giá trị tham số mặc định được ước tính từ trái sang phải khi định nghĩa hàm được thực thi. Điều này có nghĩa là biểu thức được ước tính một lần, khi hàm được xác định và giá trị tương tự được tính toán trước đó được sử dụng cho mỗi cuộc gọi. Điều này đặc biệt quan trọng để hiểu khi một tham số mặc định là một đối tượng có thể thay đổi, chẳng hạn như danh sách hoặc từ điển: nếu chức năng sửa đổi đối tượng (ví dụ: bằng cách thêm một mục vào danh sách), giá trị mặc định có hiệu lực được sửa đổi. Đây thường không phải là những gì đã được dự định. Một cách khác là sử dụng Nonelàm mặc định và kiểm tra rõ ràng cho nó trong phần thân của hàm, ví dụ:

def whats_on_the_telly(penguin=None):
    if penguin is None:
        penguin = []
    penguin.append("property of the zoo")
    return penguin

180
Các cụm từ "đây không phải là những gì đã được dự định" và "một cách xoay quanh vấn đề này" có mùi giống như chúng đang ghi lại một lỗ hổng thiết kế.
bukzor

4
@Matthew: Tôi biết rõ, nhưng nó không đáng để mắc phải. Nói chung, bạn sẽ thấy các hướng dẫn về kiểu dáng và các linters đánh dấu vô điều kiện các giá trị mặc định có thể thay đổi là sai vì lý do này. Cách rõ ràng để làm điều tương tự là nhét một thuộc tính vào hàm ( function.data = []) hoặc tốt hơn là tạo một đối tượng.
bukzor

6
@bukzor: Cạm bẫy cần được lưu ý và ghi lại, đó là lý do tại sao câu hỏi này hay và đã nhận được rất nhiều sự ủng hộ. Đồng thời, những cạm bẫy không nhất thiết phải được loại bỏ. Có bao nhiêu người mới bắt đầu Python đã chuyển một danh sách cho một hàm đã sửa đổi nó và đã bị sốc khi thấy những thay đổi hiển thị trong biến ban đầu? Các loại đối tượng có thể thay đổi là tuyệt vời, khi bạn hiểu cách sử dụng chúng. Tôi đoán nó chỉ sôi sục với ý kiến ​​về cạm bẫy đặc biệt này.
Matthew

33
Cụm từ "đây không phải là những gì được dự định" có nghĩa là "không phải những gì lập trình viên thực sự muốn xảy ra", chứ không phải "không phải là những gì Python phải làm."
Holdenweb

4
@keepenweb Wow, tôi đến bữa tiệc muộn. Với bối cảnh, bukzor hoàn toàn đúng: họ đang ghi lại hành vi / hậu quả không "có chủ đích" khi họ quyết định ngôn ngữ sẽ thực hiện định nghĩa của hàm. Vì đó là hậu quả không lường trước của sự lựa chọn thiết kế của họ, đó là một lỗ hổng thiết kế. Nếu nó không phải là một lỗi thiết kế, thậm chí sẽ không cần phải cung cấp "một cách xung quanh điều này".
code_dredd

118

Tôi không biết gì về hoạt động bên trong của trình thông dịch Python (và tôi cũng không phải là chuyên gia về trình biên dịch và phiên dịch), vì vậy đừng đổ lỗi cho tôi nếu tôi đề xuất bất cứ điều gì không thể hoặc không thể.

Với điều kiện các đối tượng python có thể thay đổi, tôi nghĩ rằng điều này nên được tính đến khi thiết kế các đối số mặc định. Khi bạn khởi tạo một danh sách:

a = []

bạn mong đợi để có được một danh sách mới được tham chiếu bởi a.

Tại sao nên a=[]vào

def x(a=[]):

khởi tạo một danh sách mới về định nghĩa hàm và không gọi Giống như bạn đang hỏi "nếu người dùng không cung cấp đối số thì hãy khởi tạo một danh sách mới và sử dụng nó như thể nó được tạo bởi người gọi". Tôi nghĩ rằng điều này là mơ hồ thay vào đó:

def x(a=datetime.datetime.now()):

người dùng, bạn có muốn amặc định cho datetime tương ứng với khi bạn xác định hoặc thực thi xkhông? Trong trường hợp này, như trong phần trước, tôi sẽ giữ hành vi tương tự như thể "gán" đối số mặc định là hướng dẫn đầu tiên của hàm ( datetime.now()được gọi là gọi hàm). Mặt khác, nếu người dùng muốn ánh xạ thời gian định nghĩa, anh ta có thể viết:

b = datetime.datetime.now()
def x(a=b):

Tôi biết, tôi biết: đó là một đóng cửa. Ngoài ra, Python có thể cung cấp một từ khóa để buộc ràng buộc thời gian định nghĩa:

def x(static a=b):

11
Bạn có thể làm: def x (a = Không): Và sau đó, nếu a là Không, hãy đặt a = datetime.datetime.now ()
Anon

20
Cảm ơn vì điều này. Tôi thực sự không thể đặt ngón tay của mình vào lý do tại sao điều này làm tôi khó chịu. Bạn đã làm nó đẹp với tối thiểu fuzz và nhầm lẫn. Khi một người nào đó đến từ lập trình hệ thống trong C ++ và đôi khi "dịch" các tính năng ngôn ngữ một cách ngây thơ, người bạn giả dối này đã đá tôi trong sự mềm yếu của cái đầu lớn, giống như các thuộc tính của lớp. Tôi hiểu tại sao mọi thứ lại theo cách này, nhưng tôi không thể không thích nó, bất kể điều gì tích cực có thể đến từ nó. Ít nhất nó trái ngược với kinh nghiệm của tôi, đến nỗi tôi có lẽ (hy vọng) sẽ không bao giờ quên nó ...
AndreasT

5
@Andreas khi bạn sử dụng Python đủ lâu, bạn sẽ bắt đầu thấy Python hợp lý như thế nào khi diễn giải mọi thứ theo cách thuộc tính của lớp - đó chỉ là do các hạn chế và hạn chế cụ thể của các ngôn ngữ như C ++ (và Java và C # ...) rằng nó có ý nghĩa đối với nội dung của class {}khối được hiểu là thuộc về các thể hiện :) Nhưng khi các lớp là đối tượng hạng nhất, rõ ràng điều tự nhiên là nội dung của chúng (trong bộ nhớ) phản ánh nội dung của chúng (trong mã).
Karl Knechtel

6
Cấu trúc tiêu chuẩn là không có sự châm biếm hoặc giới hạn trong cuốn sách của tôi. Tôi biết nó có thể vụng về và xấu xí, nhưng bạn có thể gọi nó là "định nghĩa" của một cái gì đó. Các ngôn ngữ động có vẻ hơi giống người vô chính phủ đối với tôi: Chắc chắn mọi người đều rảnh rỗi, nhưng bạn cần cấu trúc để khiến ai đó dọn rác và mở đường. Đoán tôi già ... :)
AndreasT

4
Định nghĩa chức năng được thực hiện tại thời gian tải mô-đun. Phần thân hàm được thực thi tại thời điểm gọi hàm. Đối số mặc định là một phần của định nghĩa hàm, không phải của thân hàm. (Nó trở nên phức tạp hơn đối với các hàm lồng nhau.)
Lutz Prechelt 30/03/2015

84

Chà, lý do khá đơn giản là các ràng buộc được thực hiện khi mã được thực thi và định nghĩa hàm được thực thi, à ... khi các hàm được định nghĩa.

So sánh điều này:

class BananaBunch:
    bananas = []

    def addBanana(self, banana):
        self.bananas.append(banana)

Mã này bị chính xác xảy ra bất ngờ tương tự. chuối là một thuộc tính của lớp và do đó, khi bạn thêm mọi thứ vào nó, nó sẽ được thêm vào tất cả các thể hiện của lớp đó. Lý do hoàn toàn giống nhau.

Đó chỉ là "Cách thức hoạt động" và làm cho nó hoạt động khác trong trường hợp chức năng có thể sẽ phức tạp và trong trường hợp lớp có thể là không thể, hoặc ít nhất là làm chậm việc khởi tạo đối tượng rất nhiều, vì bạn sẽ phải giữ mã lớp và thực hiện nó khi các đối tượng được tạo ra.

Vâng, thật bất ngờ. Nhưng một khi đồng xu giảm, nó hoàn toàn phù hợp với cách thức hoạt động của Python nói chung. Trên thực tế, đó là một trợ giúp giảng dạy tốt và một khi bạn hiểu tại sao điều này xảy ra, bạn sẽ mò mẫm con trăn tốt hơn nhiều.

Điều đó nói rằng nó sẽ nổi bật trong bất kỳ hướng dẫn Python tốt nào. Bởi vì như bạn đề cập, mọi người đều gặp phải vấn đề này sớm hay muộn.


Làm thế nào để bạn xác định một thuộc tính lớp khác nhau cho mỗi phiên bản của một lớp?
Kieveli

19
Nếu nó khác nhau cho mỗi trường hợp thì đó không phải là một thuộc tính lớp. Các thuộc tính lớp là các thuộc tính trên LỚP. Do đó tên. Do đó chúng giống nhau cho tất cả các trường hợp.
Lennart Regebro

1
Làm thế nào để bạn xác định một thuộc tính trong một lớp khác nhau cho mỗi phiên bản của một lớp? (Được định nghĩa lại cho những người không thể xác định rằng một người không quen thuộc với các kết luận đặt tên của Python có thể hỏi về các biến thành viên bình thường của một lớp).
Kieveli

@Kievieli: Bạn đang nói về các biến thành viên bình thường của một lớp. :-) Bạn xác định các thuộc tính thể hiện bằng cách nói self.attribution = value trong bất kỳ phương thức nào. Ví dụ __init __ ().
Lennart Regebro

@Kieveli: Hai câu trả lời: bạn không thể, bởi vì bất kỳ điều gì bạn xác định ở cấp lớp sẽ là thuộc tính lớp và bất kỳ trường hợp nào truy cập thuộc tính đó sẽ truy cập cùng thuộc tính lớp; bạn có thể, / sort of /, bằng cách sử dụng propertys - thực sự là các hàm cấp lớp hoạt động như các thuộc tính bình thường nhưng lưu thuộc tính trong thể hiện thay vì lớp (bằng cách sử dụng self.attribute = valuenhư Lennart đã nói).
Ethan Furman

66

Tại sao bạn không hướng nội?

Tôi thực sự ngạc nhiên khi không ai thực hiện nội tâm sâu sắc được cung cấp bởi Python ( 23áp dụng) trên các thiết bị gọi.

Cho một hàm nhỏ đơn giản funcđược định nghĩa là:

>>> def func(a = []):
...    a.append(5)

Khi Python gặp nó, điều đầu tiên nó sẽ làm là biên dịch nó để tạo một codeđối tượng cho hàm này. Trong khi bước biên dịch này được thực hiện, Python đánh giá * và sau đó lưu trữ các đối số mặc định (một danh sách trống []ở đây) trong chính đối tượng hàm . Như câu trả lời hàng đầu đã đề cập: danh sách ahiện có thể được coi là thành viên của hàm func.

Vì vậy, chúng ta hãy thực hiện một số hướng nội, trước và sau để kiểm tra xem danh sách được mở rộng bên trong đối tượng hàm như thế nào . Tôi đang sử dụng Python 3.xcho điều này, đối với Python 2 cũng áp dụng tương tự (sử dụng __defaults__hoặc func_defaultstrong Python 2; có, hai tên cho cùng một thứ).

Chức năng trước khi thực hiện:

>>> def func(a = []):
...     a.append(5)
...     

Sau khi Python thực thi định nghĩa này, nó sẽ lấy bất kỳ tham số mặc định nào được chỉ định ( a = []ở đây) và nhồi nhét chúng trong __defaults__thuộc tính cho đối tượng hàm (phần có liên quan: Callables):

>>> func.__defaults__
([],)

Ok, vì vậy một danh sách trống như là một mục duy nhất __defaults__, đúng như mong đợi.

Chức năng sau khi thực hiện:

Bây giờ chúng ta hãy thực hiện chức năng này:

>>> func()

Bây giờ, chúng ta hãy gặp __defaults__lại những người đó :

>>> func.__defaults__
([5],)

Kinh ngạc? Giá trị bên trong đối tượng thay đổi! Các cuộc gọi liên tiếp đến hàm bây giờ sẽ đơn giản nối thêm vào listđối tượng nhúng đó:

>>> func(); func(); func()
>>> func.__defaults__
([5, 5, 5, 5],)

Vì vậy, bạn có nó, lý do tại sao 'lỗ hổng' này xảy ra, là bởi vì các đối số mặc định là một phần của đối tượng hàm. Không có gì lạ xảy ra ở đây, tất cả chỉ là một chút ngạc nhiên.

Giải pháp phổ biến để chống lại điều này là sử dụng Nonelàm mặc định và sau đó khởi tạo trong thân hàm:

def func(a = None):
    # or: a = [] if a is None else a
    if a is None:
        a = []

Vì thân hàm được thực thi một lần nữa mỗi lần, bạn luôn nhận được một danh sách trống mới nếu không có đối số nào được thông qua a.


Để xác minh thêm rằng danh sách trong __defaults__giống như danh sách được sử dụng trong hàm, funcbạn chỉ cần thay đổi chức năng của mình để trả về iddanh sách ađược sử dụng bên trong thân hàm. Sau đó, so sánh nó với danh sách trong __defaults__(vị trí [0]trong __defaults__) và bạn sẽ thấy chúng thực sự tham chiếu đến cùng thể hiện danh sách như thế nào:

>>> def func(a = []): 
...     a.append(5)
...     return id(a)
>>>
>>> id(func.__defaults__[0]) == func()
True

Tất cả với sức mạnh của nội tâm!


* Để xác minh rằng Python đánh giá các đối số mặc định trong quá trình biên dịch hàm, hãy thử thực hiện các thao tác sau:

def bar(a=input('Did you just see me without calling the function?')): 
    pass  # use raw_input in Py2

như bạn sẽ thấy, input()được gọi trước khi quá trình xây dựng hàm và ràng buộc nó với tên barđược thực hiện.


1
id(...)cần thiết cho xác minh cuối cùng đó, hoặc isnhà điều hành sẽ trả lời cùng một câu hỏi?
das-g

1
@ das-g issẽ làm tốt thôi, tôi chỉ sử dụng id(val)vì tôi nghĩ nó có thể trực quan hơn.
Dimitris Fasarakis Hilliard

Sử dụng Nonenhư mặc định hạn chế nghiêm trọng tính hữu ích của __defaults__nội tâm, vì vậy tôi không nghĩ rằng nó hoạt động tốt như là một biện pháp bảo vệ __defaults__công việc theo cách nó làm. Đánh giá lười biếng sẽ làm nhiều hơn để giữ mặc định chức năng hữu ích từ cả hai bên.
Brilliand

58

Tôi đã từng nghĩ rằng việc tạo các đối tượng trong thời gian chạy sẽ là cách tiếp cận tốt hơn. Bây giờ tôi ít chắc chắn hơn, vì bạn đã mất một số tính năng hữu ích, mặc dù nó có thể đáng giá bất kể chỉ đơn giản là để ngăn chặn sự nhầm lẫn của người mới. Những nhược điểm của việc này là:

1. Hiệu suất

def foo(arg=something_expensive_to_compute())):
    ...

Nếu đánh giá thời gian cuộc gọi được sử dụng, thì hàm đắt tiền được gọi mỗi khi hàm của bạn được sử dụng mà không có đối số. Bạn sẽ phải trả một mức giá đắt cho mỗi cuộc gọi hoặc cần lưu trữ thủ công giá trị bên ngoài, gây ô nhiễm không gian tên của bạn và thêm tính dài dòng.

2. Buộc tham số ràng buộc

Một mẹo hữu ích là liên kết các tham số của lambda với ràng buộc hiện tại của một biến khi lambda được tạo. Ví dụ:

funcs = [ lambda i=i: i for i in range(10)]

Điều này trả về một danh sách các hàm trả về 0,1,2,3 ... tương ứng. Nếu hành vi được thay đổi, thay vào đó chúng sẽ liên kết ivới giá trị thời gian cuộc gọi của i, vì vậy bạn sẽ nhận được một danh sách các hàm được trả về 9.

Cách duy nhất để thực hiện điều này nếu không sẽ là tạo thêm một bao đóng với ràng buộc i, nghĩa là:

def make_func(i): return lambda: i
funcs = [make_func(i) for i in range(10)]

3. Hướng nội

Hãy xem xét mã:

def foo(a='test', b=100, c=[]):
   print a,b,c

Chúng tôi có thể nhận thông tin về các đối số và mặc định bằng cách sử dụng inspectmô-đun, trong đó

>>> inspect.getargspec(foo)
(['a', 'b', 'c'], None, None, ('test', 100, []))

Thông tin này rất hữu ích cho những thứ như tạo tài liệu, lập trình siêu dữ liệu, trang trí, v.v.

Bây giờ, giả sử hành vi của các mặc định có thể được thay đổi để điều này tương đương với:

_undefined = object()  # sentinel value

def foo(a=_undefined, b=_undefined, c=_undefined)
    if a is _undefined: a='test'
    if b is _undefined: b=100
    if c is _undefined: c=[]

Tuy nhiên, chúng tôi đã mất khả năng hướng nội và xem các đối số mặc định là gì . Bởi vì các đối tượng chưa được xây dựng, chúng ta không thể nắm giữ chúng mà không thực sự gọi hàm. Điều tốt nhất chúng ta có thể làm là lưu trữ mã nguồn và trả lại dưới dạng chuỗi.


1
bạn cũng có thể đạt được sự hướng nội nếu với mỗi hàm có một hàm để tạo đối số mặc định thay vì giá trị. mô-đun kiểm tra sẽ chỉ gọi chức năng đó.
yairchu

@SilentGhost: Tôi đang nói về việc nếu hành vi được thay đổi để tạo lại nó - tạo nó một lần là hành vi hiện tại và tại sao vấn đề mặc định có thể thay đổi tồn tại.
Brian

1
@yairchu: Giả sử việc xây dựng là an toàn (vì vậy không có tác dụng phụ). Hướng nội các đối số không nên làm bất cứ điều gì, nhưng đánh giá mã tùy ý cuối cùng cũng có thể có hiệu lực.
Brian

1
Một thiết kế ngôn ngữ khác nhau thường chỉ có nghĩa là viết những thứ khác nhau. Ví dụ đầu tiên của bạn có thể dễ dàng được viết là: _Exensive = đắt (); def foo (arg = _recensive), nếu bạn đặc biệt không muốn nó được đánh giá lại.
Glenn Maynard

@Glenn - đó là những gì tôi đã đề cập với "bộ nhớ cache biến bên ngoài" - nó dài dòng hơn một chút và cuối cùng bạn sẽ có thêm các biến trong không gian tên của mình.
Brian

55

5 điểm khi bảo vệ Python

  1. Đơn giản : Hành vi đơn giản theo nghĩa sau: Hầu hết mọi người chỉ rơi vào cái bẫy này một lần chứ không phải vài lần.

  2. Tính nhất quán : Python luôn vượt qua các đối tượng, không phải tên. Rõ ràng, tham số mặc định là một phần của tiêu đề hàm (không phải thân hàm). Do đó, nó phải được đánh giá tại thời gian tải mô-đun (và chỉ tại thời gian tải mô-đun, trừ khi được lồng), không phải tại thời gian gọi chức năng.

  3. Tính hữu dụng : Như Frederik Lundh đã chỉ ra trong phần giải thích về "Giá trị tham số mặc định trong Python" , hành vi hiện tại có thể khá hữu ích cho lập trình nâng cao. (Sử dụng một cách tiết kiệm.)

  4. Tài liệu đầy đủ : Trong tài liệu Python cơ bản nhất, hướng dẫn, vấn đề được thông báo lớn là "Cảnh báo quan trọng" trong phần phụ đầu tiên của Phần "Thông tin thêm về Xác định Hàm" . Cảnh báo thậm chí sử dụng in đậm, hiếm khi được áp dụng bên ngoài các tiêu đề. RTFM: Đọc hướng dẫn sử dụng tốt.

  5. Học siêu tốc : Rơi vào bẫy thực sự là một khoảnh khắc rất hữu ích (ít nhất là nếu bạn là người học phản xạ), vì sau đó bạn sẽ hiểu rõ hơn về điểm "Tính nhất quán" ở trên và điều đó sẽ dạy cho bạn rất nhiều về Python.


18
Phải mất một năm tôi mới thấy hành vi này làm rối tung mã sản xuất của mình, cuối cùng tôi đã loại bỏ một tính năng hoàn chỉnh cho đến khi tôi tình cờ gặp phải lỗi thiết kế này. Tôi đang sử dụng Django. Vì môi trường dàn dựng không có nhiều yêu cầu, lỗi này không bao giờ có bất kỳ tác động nào đến QA. Khi chúng tôi phát trực tiếp và nhận được nhiều yêu cầu đồng thời - một số chức năng tiện ích bắt đầu ghi đè lên các tham số của nhau! Làm lỗ hổng bảo mật, lỗi và những gì không.
oriadam

7
@oriadam, không có ý xúc phạm, nhưng tôi tự hỏi làm thế nào bạn học Python mà không gặp phải điều này trước đây. Bây giờ tôi chỉ học Python và cạm bẫy có thể này được đề cập trong hướng dẫn chính thức của Python ngay bên cạnh đề cập đầu tiên về các đối số mặc định. (Như đã đề cập tại điểm 4 của câu trả lời này.) Tôi cho rằng đạo đức là-thay unsympathetically-để đọc các tài liệu chính thức của ngôn ngữ mà bạn sử dụng để tạo ra các phần mềm sản xuất.
tự đại diện

Ngoài ra, sẽ rất ngạc nhiên (với tôi) nếu một hàm có độ phức tạp không xác định được gọi ngoài hàm gọi mà tôi đang thực hiện.
Vatine

52

Hành vi này được giải thích dễ dàng bởi:

  1. Khai báo hàm (lớp, v.v.) chỉ được thực hiện một lần, tạo tất cả các đối tượng giá trị mặc định
  2. mọi thứ đều được thông qua

Vì thế:

def x(a=0, b=[], c=[], d=0):
    a = a + 1
    b = b + [1]
    c.append(1)
    print a, b, c
  1. a không thay đổi - mọi lệnh gọi tạo đối tượng int mới - đối tượng mới được in
  2. b không thay đổi - mảng mới được xây dựng từ giá trị mặc định và được in
  3. c thay đổi - thao tác được thực hiện trên cùng một đối tượng - và nó được in

(Trên thực tế, add là một ví dụ tồi, nhưng số nguyên vẫn không thay đổi vẫn là điểm chính của tôi.)
Anon

Nhận ra điều đó với sự thất vọng của tôi sau khi kiểm tra để thấy rằng, với b được đặt thành [], b .__ thêm __ ([1]) trả về [1] nhưng cũng để b vẫn [] mặc dù danh sách có thể thay đổi. Lỗi của tôi.
Anon

@ANon: có __iadd__, nhưng nó không hoạt động với int. Tất nhiên. :-)
Veky

35

Những gì bạn đang hỏi là tại sao điều này:

def func(a=[], b = 2):
    pass

không tương đương nội bộ với điều này:

def func(a=None, b = None):
    a_default = lambda: []
    b_default = lambda: 2
    def actual_func(a=None, b=None):
        if a is None: a = a_default()
        if b is None: b = b_default()
    return actual_func
func = func()

ngoại trừ trường hợp gọi func rõ ràng (Không, Không), chúng tôi sẽ bỏ qua.

Nói cách khác, thay vì đánh giá các tham số mặc định, tại sao không lưu trữ từng tham số và đánh giá chúng khi hàm được gọi?

Một câu trả lời có lẽ ở ngay đó - nó sẽ biến mọi chức năng với các tham số mặc định thành một bao đóng một cách hiệu quả. Ngay cả khi tất cả được ẩn đi trong trình thông dịch và không phải là sự đóng cửa hoàn toàn, dữ liệu vẫn phải được lưu trữ ở đâu đó. Nó sẽ chậm hơn và sử dụng nhiều bộ nhớ hơn.


6
Không cần phải đóng cửa - một cách tốt hơn để nghĩ về nó chỉ đơn giản là tạo mã byte tạo mặc định dòng mã đầu tiên - sau tất cả, bạn đang biên dịch phần thân tại thời điểm đó - dù sao cũng không có sự khác biệt thực sự giữa mã trong các đối số và mã trong cơ thể.
Brian

10
Đúng, nhưng nó vẫn làm chậm Python, và nó thực sự sẽ rất đáng ngạc nhiên, trừ khi bạn làm tương tự với các định nghĩa lớp, điều này sẽ làm cho nó chậm một cách ngu ngốc vì bạn sẽ phải chạy lại định nghĩa cả lớp mỗi khi bạn khởi tạo một lớp học. Như đã đề cập, sửa chữa sẽ đáng ngạc nhiên hơn vấn đề.
Lennart Regebro

Đồng ý với Lennart. Như Guido rất thích nói, đối với mọi tính năng ngôn ngữ hoặc thư viện chuẩn, có ai đó sử dụng nó.
Jason Baker

6
Thay đổi nó bây giờ sẽ là điên rồ - chúng ta chỉ đang khám phá lý do tại sao nó là như vậy. Nếu bắt đầu đánh giá mặc định muộn, nó sẽ không gây ngạc nhiên. Điều chắc chắn là sự khác biệt cốt lõi của việc phân tích cú pháp sẽ có tác dụng càn quét và có lẽ nhiều điều tối nghĩa, ảnh hưởng đến toàn bộ ngôn ngữ.
Glenn Maynard

35

1) Vấn đề được gọi là "Đối số mặc định có thể thay đổi" nói chung là một ví dụ đặc biệt chứng minh rằng:
"Tất cả các chức năng với vấn đề này cũng gặp phải vấn đề tác dụng phụ tương tự trên tham số thực tế ",
đó là trái với quy tắc lập trình chức năng, thường không thể khắc phục và nên được cố định cả hai cùng nhau.

Thí dụ:

def foo(a=[]):                 # the same problematic function
    a.append(5)
    return a

>>> somevar = [1, 2]           # an example without a default parameter
>>> foo(somevar)
[1, 2, 5]
>>> somevar
[1, 2, 5]                      # usually expected [1, 2]

Giải pháp : một bản sao
Một giải pháp tuyệt đối an toàn là trước tiên copyhoặc deepcopyđối tượng đầu vào và sau đó làm bất cứ điều gì với bản sao.

def foo(a=[]):
    a = a[:]     # a copy
    a.append(5)
    return a     # or everything safe by one line: "return a + [5]"

Nhiều kiểu biến đổi dựng sẵn có một phương thức sao chép như some_dict.copy()hoặc some_set.copy()hoặc có thể được sao chép dễ dàng như somelist[:]hoặc list(some_list). Mọi đối tượng cũng có thể được sao chép bằng copy.copy(any_object)hoặc kỹ lưỡng hơn bằng cách copy.deepcopy()(hữu ích sau nếu đối tượng có thể thay đổi được cấu thành từ các đối tượng có thể thay đổi). Một số đối tượng về cơ bản dựa trên các tác dụng phụ như đối tượng "tập tin" và không thể được sao chép một cách có ý nghĩa bằng bản sao. sao chép

Ví dụ vấn đề cho một câu hỏi SO tương tự

class Test(object):            # the original problematic class
  def __init__(self, var1=[]):
    self._var1 = var1

somevar = [1, 2]               # an example without a default parameter
t1 = Test(somevar)
t2 = Test(somevar)
t1._var1.append([1])
print somevar                  # [1, 2, [1]] but usually expected [1, 2]
print t2._var1                 # [1, 2, [1]] but usually expected [1, 2]

Nó không nên được lưu trong bất kỳ thuộc tính công khai nào của một thể hiện được trả về bởi hàm này. (Giả sử rằng các thuộc tính riêng của cá thể không nên được sửa đổi từ bên ngoài lớp này hoặc các lớp con theo quy ước. Tức là_var1 là thuộc tính riêng)

Kết luận:
Các đối tượng tham số đầu vào không nên được sửa đổi tại chỗ (bị đột biến) cũng như không nên liên kết chúng vào một đối tượng được hàm trả về. (Nếu chúng tôi lập trình trước mà không có tác dụng phụ được khuyến khích mạnh mẽ. Hãy xem Wiki về "tác dụng phụ" (Hai đoạn đầu tiên có liên quan trong bối cảnh này.).)

2)
Chỉ khi tác dụng phụ đối với tham số thực tế là bắt buộc nhưng không mong muốn đối với tham số mặc định thì giải pháp hữu ích là def ...(var1=None): if var1 is None: var1 = [] Khác ..

3) Trong một số trường hợp , hành vi có thể thay đổi của các tham số mặc định hữu ích .


5
Tôi hy vọng bạn biết rằng Python không phải là ngôn ngữ lập trình chức năng.
Veky

6
Vâng, Python là một ngôn ngữ đa paragigm với một số tính năng chức năng. ("Đừng làm cho mọi vấn đề trông giống như một cái đinh chỉ vì bạn có một cái búa.") Nhiều trong số chúng nằm trong Python tốt nhất. Python có một chương trình chức năng HOWTO thú vị Các tính năng khác là đóng và cà ri, không được đề cập ở đây.
hynekcer

1
Tôi cũng nói thêm, ở giai đoạn cuối này, ngữ nghĩa chuyển nhượng của Python đã được thiết kế rõ ràng để tránh sao chép dữ liệu khi cần thiết, do đó việc tạo các bản sao (và đặc biệt là các bản sao sâu) sẽ ảnh hưởng xấu đến cả thời gian chạy và sử dụng bộ nhớ. Do đó, chúng chỉ nên được sử dụng khi cần thiết, nhưng những người mới đến thường gặp khó khăn khi hiểu điều đó.
Holdenweb

1
@keepenweb Tôi đồng ý. Một bản sao tạm thời là cách thông thường nhất và đôi khi là cách khả thi duy nhất để bảo vệ dữ liệu có thể thay đổi ban đầu khỏi một chức năng không liên quan có thể sửa đổi chúng. May mắn thay, một chức năng sửa đổi dữ liệu một cách bất hợp lý được coi là một lỗi và do đó không phổ biến.
hynekcer

Tôi đồng ý với câu trả lời này. Và tôi không hiểu tại sao def f( a = None )cấu trúc được khuyến nghị khi bạn thực sự có ý nghĩa khác. Sao chép là ok, vì bạn không nên thay đổi đối số. Và khi bạn làm if a is None: a = [1, 2, 3], bạn vẫn sao chép danh sách.
koddo

30

Điều này thực sự không liên quan gì đến các giá trị mặc định, ngoài ra nó thường xuất hiện dưới dạng một hành vi không mong muốn khi bạn viết các hàm với các giá trị mặc định có thể thay đổi.

>>> def foo(a):
    a.append(5)
    print a

>>> a  = [5]
>>> foo(a)
[5, 5]
>>> foo(a)
[5, 5, 5]
>>> foo(a)
[5, 5, 5, 5]
>>> foo(a)
[5, 5, 5, 5, 5]

Không có giá trị mặc định trong tầm nhìn trong mã này, nhưng bạn nhận được chính xác cùng một vấn đề.

Vấn đề là rằng foođược sửa đổi một biến có thể thay đổi được thông qua vào từ người gọi, khi người gọi không mong đợi điều này. Mã như thế này sẽ ổn nếu hàm được gọi là như thế append_5; sau đó người gọi sẽ gọi hàm để sửa đổi giá trị mà họ truyền vào và hành vi sẽ được mong đợi. Nhưng một hàm như vậy sẽ rất khó có thể đưa ra một đối số mặc định và có thể sẽ không trả về danh sách (vì người gọi đã có một tham chiếu đến danh sách đó; một đối số mà nó vừa truyền vào).

Bản gốc của bạn foo, với một đối số mặc định, không nên sửa đổi acho dù nó được truyền rõ ràng hay có giá trị mặc định. Mã của bạn nên để các đối số có thể thay đổi một mình trừ khi rõ ràng từ ngữ cảnh / tên / tài liệu mà các đối số được cho là được sửa đổi. Sử dụng các giá trị có thể thay đổi được truyền vào dưới dạng đối số làm tạm thời cục bộ là một ý tưởng cực kỳ tồi tệ, cho dù chúng ta có ở trong Python hay không và có các đối số mặc định có liên quan hay không.

Nếu bạn cần thao tác triệt để tạm thời cục bộ trong quá trình tính toán một cái gì đó và bạn cần bắt đầu thao tác của mình từ một giá trị đối số, bạn cần tạo một bản sao.


7
Mặc dù có liên quan, tôi nghĩ đây là hành vi khác biệt (như chúng ta mong đợi appendsẽ thay đổi a"tại chỗ"). Rằng một biến đổi mặc định không được khởi tạo lại trên mỗi cuộc gọi là bit "bất ngờ" ... ít nhất là đối với tôi. :)
Andy Hayden

2
@AndyHayden nếu hàm được dự kiến ​​sẽ sửa đổi đối số, tại sao nó có ý nghĩa để có một mặc định?
Đánh dấu tiền chuộc

@MarkRansom ví dụ duy nhất tôi có thể nghĩ là cache={}. Tuy nhiên, tôi nghi ngờ "sự ngạc nhiên tối thiểu" này xuất hiện là khi bạn không mong đợi (hoặc muốn) chức năng mà bạn đang gọi để thay đổi đối số.
Andy Hayden

1
@AndyHayden Tôi đã để lại câu trả lời của riêng mình ở đây với sự mở rộng tình cảm đó. Cho tôi biết bạn nghĩ gì. Tôi có thể thêm ví dụ của bạn cache={}vào nó cho đầy đủ.
Đánh dấu tiền chuộc

1
@AndyHayden Điểm của câu trả lời của tôi là nếu bạn từng ngạc nhiên khi vô tình làm thay đổi giá trị mặc định của một đối số, thì bạn có một lỗi khác, đó là mã của bạn có thể vô tình làm thay đổi giá trị của trình gọi khi mặc định không được sử dụng. Và lưu ý rằng việc sử dụng Nonevà gán mặc định thực sự nếu đối số None không giải quyết được vấn đề đó (tôi coi đó là mô hình chống vì lý do đó). Nếu bạn sửa lỗi khác bằng cách tránh làm thay đổi giá trị đối số cho dù chúng có mặc định hay không thì bạn sẽ không bao giờ để ý hoặc quan tâm đến hành vi "đáng kinh ngạc" này.
Ben

27

Đã là chủ đề bận rộn, nhưng từ những gì tôi đọc ở đây, những điều sau đây đã giúp tôi nhận ra cách thức hoạt động bên trong:

def bar(a=[]):
     print id(a)
     a = a + [1]
     print id(a)
     return a

>>> bar()
4484370232
4484524224
[1]
>>> bar()
4484370232
4484524152
[1]
>>> bar()
4484370232 # Never change, this is 'class property' of the function
4484523720 # Always a new object 
[1]
>>> id(bar.func_defaults[0])
4484370232

2
thực tế điều này có thể hơi khó hiểu đối với người mới vì a = a + [1]quá tải a... hãy xem xét thay đổi nó b = a + [1] ; print id(b)và thêm một dòng a.append(2). Điều đó sẽ làm cho rõ ràng hơn rằng +trên hai danh sách luôn tạo ra một danh sách mới (được gán cho b), trong khi một sửa đổi avẫn có thể có cùng một danh sách id(a).
Jorn Hees

25

Đó là một tối ưu hóa hiệu suất. Kết quả của chức năng này, bạn nghĩ cuộc gọi nào trong hai chức năng này nhanh hơn?

def print_tuple(some_tuple=(1,2,3)):
    print some_tuple

print_tuple()        #1
print_tuple((1,2,3)) #2

Tôi sẽ cho bạn một gợi ý. Đây là phần tháo gỡ (xem http://docs.python.org/l Library / dis.html ):

#1

0 LOAD_GLOBAL              0 (print_tuple)
3 CALL_FUNCTION            0
6 POP_TOP
7 LOAD_CONST               0 (None)
10 RETURN_VALUE

#2

 0 LOAD_GLOBAL              0 (print_tuple)
 3 LOAD_CONST               4 ((1, 2, 3))
 6 CALL_FUNCTION            1
 9 POP_TOP
10 LOAD_CONST               0 (None)
13 RETURN_VALUE

Tôi nghi ngờ hành vi có kinh nghiệm có một cách sử dụng thực tế (ai thực sự sử dụng biến tĩnh trong C, không có lỗi sinh sản?)

Như bạn có thể thấy, có một lợi ích hiệu suất khi sử dụng đối số mặc định không thay đổi. Điều này có thể tạo sự khác biệt nếu đó là một hàm được gọi thường xuyên hoặc đối số mặc định mất nhiều thời gian để xây dựng. Ngoài ra, hãy nhớ rằng Python không C. Trong C, bạn có các hằng số khá miễn phí. Trong Python bạn không có lợi ích này.


24

Python: Đối số mặc định có thể thay đổi

Các đối số mặc định được đánh giá tại thời điểm hàm được biên dịch thành đối tượng hàm. Khi được sử dụng bởi hàm, nhiều lần bởi hàm đó, chúng là và vẫn là cùng một đối tượng.

Khi chúng có thể thay đổi, khi bị đột biến (ví dụ, bằng cách thêm một phần tử vào nó), chúng vẫn bị đột biến trong các cuộc gọi liên tiếp.

Họ ở lại đột biến bởi vì họ là cùng một đối tượng mỗi lần.

Mã tương đương:

Vì danh sách được liên kết với hàm khi đối tượng hàm được biên dịch và khởi tạo, nên:

def foo(mutable_default_argument=[]): # make a list the default argument
    """function that uses a list"""

gần như chính xác tương đương với điều này:

_a_list = [] # create a list in the globals

def foo(mutable_default_argument=_a_list): # make it the default argument
    """function that uses a list"""

del _a_list # remove globals name binding

Trình diễn

Đây là một minh chứng - bạn có thể xác minh rằng chúng là cùng một đối tượng mỗi lần chúng được tham chiếu bởi

  • thấy rằng danh sách được tạo trước khi hàm hoàn thành biên dịch thành đối tượng hàm,
  • quan sát rằng id giống nhau mỗi khi danh sách được tham chiếu,
  • quan sát rằng danh sách vẫn thay đổi khi hàm sử dụng nó được gọi là lần thứ hai,
  • quan sát thứ tự đầu ra được in từ nguồn (mà tôi thuận tiện đánh số cho bạn):

example.py

print('1. Global scope being evaluated')

def create_list():
    '''noisily create a list for usage as a kwarg'''
    l = []
    print('3. list being created and returned, id: ' + str(id(l)))
    return l

print('2. example_function about to be compiled to an object')

def example_function(default_kwarg1=create_list()):
    print('appending "a" in default default_kwarg1')
    default_kwarg1.append("a")
    print('list with id: ' + str(id(default_kwarg1)) + 
          ' - is now: ' + repr(default_kwarg1))

print('4. example_function compiled: ' + repr(example_function))


if __name__ == '__main__':
    print('5. calling example_function twice!:')
    example_function()
    example_function()

và chạy nó với python example.py:

1. Global scope being evaluated
2. example_function about to be compiled to an object
3. list being created and returned, id: 140502758808032
4. example_function compiled: <function example_function at 0x7fc9590905f0>
5. calling example_function twice!:
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a']
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a', 'a']

Điều này có vi phạm nguyên tắc "Ít ngạc nhiên nhất" không?

Thứ tự thực hiện này thường gây nhầm lẫn cho người dùng mới của Python. Nếu bạn hiểu mô hình thực thi Python, thì nó trở nên khá được mong đợi.

Hướng dẫn thông thường cho người dùng Python mới:

Nhưng đây là lý do tại sao hướng dẫn thông thường cho người dùng mới là tạo các đối số mặc định của họ như thế này:

def example_function_2(default_kwarg=None):
    if default_kwarg is None:
        default_kwarg = []

Điều này sử dụng singleton đơn lẻ làm đối tượng canh gác để cho biết chức năng cho dù chúng ta có nhận được một đối số khác ngoài mặc định hay không. Nếu chúng tôi không có đối số, thì chúng tôi thực sự muốn sử dụng một danh sách trống mới [], làm mặc định.

Như phần hướng dẫn về luồng điều khiển nói:

Nếu bạn không muốn mặc định được chia sẻ giữa các cuộc gọi tiếp theo, bạn có thể viết hàm như thế này:

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

24

Câu trả lời ngắn nhất có lẽ là "định nghĩa là thực thi", do đó toàn bộ lập luận không có ý nghĩa nghiêm ngặt. Như một ví dụ dễ hiểu hơn, bạn có thể trích dẫn điều này:

def a(): return []

def b(x=a()):
    print x

Hy vọng rằng nó đủ để cho thấy rằng việc không thực thi các biểu thức đối số mặc định tại thời điểm thực thi của defcâu lệnh là không dễ dàng hoặc không có ý nghĩa, hoặc cả hai.

Tôi đồng ý rằng đó là một gotcha khi bạn cố gắng sử dụng các hàm tạo mặc định.


20

Một cách giải quyết đơn giản bằng cách sử dụng Không có

>>> def bar(b, data=None):
...     data = data or []
...     data.append(b)
...     return data
... 
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3, [34])
[34, 3]
>>> bar(3, [34])
[34, 3]

19

Hành vi này không đáng ngạc nhiên nếu bạn cân nhắc những điều sau:

  1. Hành vi của các thuộc tính lớp chỉ đọc khi thử gán, và đó
  2. Hàm là các đối tượng (được giải thích tốt trong câu trả lời được chấp nhận).

Vai trò của (2) đã được đề cập rộng rãi trong chủ đề này. (1) có thể là yếu tố gây ngạc nhiên, vì hành vi này không "trực quan" khi đến từ các ngôn ngữ khác.

(1) được mô tả trong hướng dẫn Python trên các lớp . Trong nỗ lực gán giá trị cho thuộc tính lớp chỉ đọc:

... tất cả các biến được tìm thấy bên ngoài phạm vi trong cùng là chỉ đọc ( một nỗ lực ghi vào biến đó sẽ chỉ tạo ra một biến cục bộ mới trong phạm vi trong cùng, không thay đổi tên bên ngoài có tên giống hệt nhau ).

Nhìn lại ví dụ ban đầu và xem xét các điểm trên:

def foo(a=[]):
    a.append(5)
    return a

Đây foolà một đối tượng và alà một thuộc tính của foo(có sẵn tạifoo.func_defs[0] ). Vì alà một danh sách, acó thể thay đổi và do đó là một thuộc tính đọc-ghi của foo. Nó được khởi tạo vào danh sách trống như được chỉ định bởi chữ ký khi hàm được khởi tạo và có sẵn để đọc và ghi miễn là đối tượng hàm tồn tại.

Gọi foomà không ghi đè mặc định sử dụng giá trị của mặc định đó từ foo.func_defs. Trong trường hợp này, foo.func_defs[0]được sử dụng cho aphạm vi mã của đối tượng hàm. Thay đổi để athay đổifoo.func_defs[0] , là một phần của foođối tượng và vẫn tồn tại giữa quá trình thực thi mã foo.

Bây giờ, so sánh ví dụ này với ví dụ từ tài liệu mô phỏng hành vi đối số mặc định của các ngôn ngữ khác , sao cho mặc định chữ ký hàm được sử dụng mỗi khi hàm được thực thi:

def foo(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

Lấy (1)(2) , người ta có thể thấy lý do tại sao điều này thực hiện hành vi mong muốn:

  • Khi foođối tượng hàm được khởi tạo,foo.func_defs[0] được đặt thành None, một đối tượng bất biến.
  • Khi chức năng được thực thi với mặc định (không có tham số nào được chỉ định Ltrong lệnh gọi hàm), foo.func_defs[0](None ) có sẵn trong phạm vi cục bộ như L.
  • Trên L = [] , bài tập không thể thành công tại foo.func_defs[0], vì thuộc tính đó là chỉ đọc.
  • Mỗi (1) , một biến cục bộ mới cũng được đặt tên Lđược tạo trong phạm vi cục bộ và được sử dụng cho phần còn lại của lệnh gọi hàm. foo.func_defs[0]do đó vẫn không thay đổi cho các yêu cầu trong tương lai của foo.

19

Tôi sẽ trình bày một cấu trúc thay thế để chuyển một giá trị danh sách mặc định cho một hàm (nó hoạt động tốt như nhau với từ điển).

Như những người khác đã nhận xét rộng rãi, tham số danh sách được liên kết với hàm khi nó được định nghĩa trái ngược với khi nó được thực thi. Vì danh sách và từ điển có thể thay đổi, nên mọi thay đổi đối với tham số này sẽ ảnh hưởng đến các lệnh gọi khác đến chức năng này. Do đó, các cuộc gọi tiếp theo đến chức năng sẽ nhận được danh sách chia sẻ này có thể đã bị thay đổi bởi bất kỳ cuộc gọi nào khác đến chức năng. Tệ hơn nữa, hai tham số đang sử dụng tham số chia sẻ của chức năng này cùng một lúc không biết đến những thay đổi được thực hiện bởi cái kia.

Phương pháp sai (có thể ...) :

def foo(list_arg=[5]):
    return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
# The value of 6 appended to variable 'a' is now part of the list held by 'b'.
>>> b
[5, 6, 7]  

# Although 'a' is expecting to receive 6 (the last element it appended to the list),
# it actually receives the last element appended to the shared list.
# It thus receives the value 7 previously appended by 'b'.
>>> a.pop()             
7

Bạn có thể xác minh rằng chúng là một và cùng một đối tượng bằng cách sử dụng id:

>>> id(a)
5347866528

>>> id(b)
5347866528

"Python hiệu quả: 59 cách cụ thể để viết Python tốt hơn" của Per Brett Slatkin, Mục 20: Sử dụng Nonevà tài liệu để chỉ định các đối số mặc định động (trang 48)

Quy ước để đạt được kết quả mong muốn trong Python là cung cấp giá trị mặc định Nonevà ghi lại hành vi thực tế trong chuỗi doc.

Việc thực hiện này đảm bảo rằng mỗi cuộc gọi đến hàm sẽ nhận được danh sách mặc định hoặc nếu không thì danh sách được truyền cho hàm.

Phương pháp ưa thích :

def foo(list_arg=None):
   """
   :param list_arg:  A list of input values. 
                     If none provided, used a list with a default value of 5.
   """
   if not list_arg:
       list_arg = [5]
   return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
>>> b
[5, 7]

c = foo([10])
c.append(11)
>>> c
[10, 11]

Có thể có các trường hợp sử dụng hợp pháp cho 'Phương thức sai', theo đó, lập trình viên dự định tham số danh sách mặc định sẽ được chia sẻ, nhưng đây có thể là ngoại lệ nhiều hơn quy tắc.


17

Các giải pháp ở đây là:

  1. Sử dụng Nonelàm giá trị mặc định của bạn (hoặc nonce object) và bật giá trị đó để tạo giá trị của bạn khi chạy; hoặc là
  2. Sử dụng lambdalàm tham số mặc định của bạn và gọi nó trong khối thử để lấy giá trị mặc định (đây là loại trừu tượng lambda dành cho).

Tùy chọn thứ hai là tốt vì người dùng của chức năng có thể chuyển qua một cuộc gọi, có thể đã tồn tại (chẳng hạn như một type)


16

Khi chúng tôi làm điều này:

def foo(a=[]):
    ...

... chúng tôi gán đối số acho một tên không tên sách , nếu người gọi không vượt qua giá trị của a.

Để làm cho mọi thứ đơn giản hơn cho cuộc thảo luận này, hãy tạm thời đặt tên cho danh sách không tên. Thế còn pavlo?

def foo(a=pavlo):
   ...

Bất cứ lúc nào, nếu người gọi không cho chúng tôi biết đó alà gì , chúng tôi sẽ sử dụng lại pavlo.

Nếu pavlocó thể thay đổi (có thể sửa đổi) và fookết thúc sửa đổi nó, một hiệu ứng chúng ta nhận thấy lần sau foođược gọi mà không chỉ định a.

Vì vậy, đây là những gì bạn thấy (Hãy nhớ, pavlođược khởi tạo thành []):

 >>> foo()
 [5]

Bây giờ, pavlolà [5].

Gọi foo()lại sửa đổi pavlomột lần nữa:

>>> foo()
[5, 5]

Chỉ định akhi gọi foo()đảm bảo pavlokhông bị chạm.

>>> ivan = [1, 2, 3, 4]
>>> foo(a=ivan)
[1, 2, 3, 4, 5]
>>> ivan
[1, 2, 3, 4, 5]

Vì vậy, pavlovẫn còn [5, 5].

>>> foo()
[5, 5, 5]

16

Đôi khi tôi khai thác hành vi này như là một thay thế cho mẫu sau:

singleton = None

def use_singleton():
    global singleton

    if singleton is None:
        singleton = _make_singleton()

    return singleton.use_me()

Nếu singletonchỉ được sử dụng bởi use_singleton, tôi thích mẫu sau thay thế:

# _make_singleton() is called only once when the def is executed
def use_singleton(singleton=_make_singleton()):
    return singleton.use_me()

Tôi đã sử dụng điều này để khởi tạo các lớp máy khách truy cập các tài nguyên bên ngoài và cũng để tạo các ký tự hoặc danh sách để ghi nhớ.

Vì tôi không nghĩ rằng mô hình này là nổi tiếng, tôi đã đưa ra một nhận xét ngắn để bảo vệ chống lại những hiểu lầm trong tương lai.


2
Tôi thích thêm một trình trang trí để ghi nhớ và đặt bộ nhớ cache vào chính đối tượng hàm.
Stefano Borini

Ví dụ này không thay thế mẫu phức tạp hơn mà bạn hiển thị, bởi vì bạn gọi _make_singletonvào thời gian def trong ví dụ đối số mặc định, nhưng tại thời điểm cuộc gọi trong ví dụ toàn cầu. Một sự thay thế thực sự sẽ sử dụng một số loại hộp có thể thay đổi cho giá trị đối số mặc định, nhưng việc thêm đối số sẽ tạo cơ hội để truyền các giá trị thay thế.
Yann Vernier

15

Bạn có thể làm tròn điều này bằng cách thay thế đối tượng (và do đó, buộc bằng phạm vi):

def foo(a=[]):
    a = list(a)
    a.append(5)
    return a

Xấu xí, nhưng nó hoạt động.


3
Đây là một giải pháp hay trong trường hợp bạn đang sử dụng phần mềm tạo tài liệu tự động để ghi lại các loại đối số mà hàm mong đợi. Đặt a = Không và sau đó đặt thành [] nếu a là Không giúp người đọc hiểu nhanh về những gì được mong đợi.
Michael Scott Cuthbert

Ý tưởng tuyệt vời: việc đóng lại tên đó đảm bảo nó không bao giờ có thể được sửa đổi. Tôi thực sự thích điều đó.
Holdenweb

Đây chính xác là cách để làm điều đó. Python không tạo một bản sao của tham số, do đó, tùy thuộc vào bạn để tạo bản sao rõ ràng. Khi bạn có một bản sao, bạn có thể sửa đổi tùy ý mà không có bất kỳ tác dụng phụ không mong muốn nào.
Đánh dấu tiền chuộc

13

Có thể đúng là:

  1. Ai đó đang sử dụng mọi tính năng ngôn ngữ / thư viện và
  2. Chuyển đổi hành vi ở đây sẽ không được khuyến khích, nhưng

nó hoàn toàn phù hợp để giữ cả hai tính năng trên mà vẫn tạo ra một điểm khác:

  1. Đây là một tính năng khó hiểu và thật không may trong Python.

Các câu trả lời khác, hoặc ít nhất một số trong số chúng tạo ra điểm 1 và 2 nhưng không phải là 3, hoặc tạo điểm 3 và hạ điểm 1 và 2. Nhưng cả ba đều đúng.

Có thể đúng là việc chuyển ngựa giữa dòng ở đây sẽ yêu cầu phá vỡ đáng kể và có thể có nhiều vấn đề hơn được tạo ra bằng cách thay đổi Python để xử lý trực quan đoạn mở của Stefano. Và có thể đúng là ai đó biết rõ nội bộ Python có thể giải thích một hậu quả của tôi. Tuy nhiên,

Hành vi hiện tại không phải là Pythonic và Python thành công vì rất ít về ngôn ngữ vi phạm nguyên tắc ít gây ngạc nhiên ở bất cứ đâu ở gầnđiều này thật tệ Đó là một vấn đề thực sự, liệu có nên khôn ngoan khi nhổ nó hay không. Đó là một lỗ hổng thiết kế. Nếu bạn hiểu ngôn ngữ tốt hơn nhiều bằng cách cố gắng tìm ra hành vi, tôi có thể nói rằng C ++ thực hiện tất cả những điều này và hơn thế nữa; bạn học được rất nhiều bằng cách điều hướng, ví dụ, lỗi con trỏ tinh tế. Nhưng đây không phải là Pythonic: những người quan tâm đến Python đủ kiên trì khi đối mặt với hành vi này là những người bị cuốn hút bởi ngôn ngữ này vì Python có ít bất ngờ hơn nhiều so với ngôn ngữ khác. Dabblers và người tò mò trở thành Pythonistas khi họ ngạc nhiên về việc mất ít thời gian để làm một cái gì đó hoạt động - không phải vì một thiết kế fl - ý tôi là, câu đố logic ẩn - cắt giảm trực giác của các lập trình viên bị thu hút bởi Python bởi vì nó chỉ hoạt động .


6
-1 Mặc dù quan điểm phòng thủ, đây không phải là một câu trả lời, tôi không đồng ý với nó. Quá nhiều trường hợp ngoại lệ đặc biệt quên đi trường hợp góc riêng của họ.
Marcin

3
Vì vậy, thật là "ngu dốt một cách đáng kinh ngạc" khi nói rằng trong Python, sẽ có ý nghĩa hơn đối với một đối số mặc định của [] vẫn là [] mỗi khi hàm được gọi?
Christos Hayward

3
Và thật thiếu hiểu biết khi coi đó là một thành ngữ không may đặt một đối số mặc định thành Không có, và sau đó trong phần thân của phần thiết lập hàm nếu argument == none: argument = []? Có phải là không biết gì khi xem thành ngữ này là không may vì mọi người thường muốn những gì một người mới ngây thơ mong đợi, rằng nếu bạn gán f (argument = []), đối số sẽ tự động mặc định thành giá trị của []?
Christos Hayward

3
Nhưng trong Python, một phần của tinh thần ngôn ngữ là bạn không cần phải lặn quá nhiều; mảng.sort () hoạt động và hoạt động bất kể bạn hiểu về cách sắp xếp, big-O và hằng số. Vẻ đẹp của Python trong cơ chế sắp xếp mảng, để đưa ra một trong vô số ví dụ, là bạn không bắt buộc phải đi sâu vào bên trong. Và nói một cách khác, vẻ đẹp của Python là người ta không bắt buộc phải đi sâu vào thực hiện để có được thứ gì đó chỉ hoạt động. Và có một cách giải quyết (... if argument == none: argument = []), FAIL.
Christos Hayward

3
Là một độc lập, câu lệnh x=[]có nghĩa là "tạo một đối tượng danh sách trống và liên kết tên 'x' với nó." Vì vậy, trong def f(x=[]), một danh sách trống cũng được tạo ra. Nó không luôn luôn bị ràng buộc với x, vì vậy thay vào đó, nó bị ràng buộc với người thay thế mặc định. Sau này khi f () được gọi, mặc định được kéo ra và ràng buộc với x. Vì chính danh sách trống đã bị xóa đi, nên danh sách đó là thứ duy nhất có sẵn để liên kết với x, cho dù có bất cứ điều gì đã bị mắc kẹt bên trong nó hay không. Làm sao có thể khác được?
Jerry B

10

Đây không phải là một lỗ hổng thiết kế . Bất cứ ai đi qua đây là làm điều gì đó sai.

Có 3 trường hợp tôi thấy bạn có thể gặp phải vấn đề này ở đâu:

  1. Bạn có ý định sửa đổi đối số là tác dụng phụ của hàm. Trong trường hợp này, không bao giờ có ý nghĩa để có một đối số mặc định. Ngoại lệ duy nhất là khi bạn lạm dụng danh sách đối số để có các thuộc tính hàm, ví dụ:cache={} , và bạn sẽ không được gọi hàm với một đối số thực tế.
  2. Bạn có ý định để lại đối số không được sửa đổi, nhưng bạn đã vô tình làm sửa đổi nó. Đó là một lỗi, sửa nó.
  3. Bạn dự định sửa đổi đối số để sử dụng bên trong hàm, nhưng không mong đợi sửa đổi có thể xem được bên ngoài hàm. Trong trường hợp đó, bạn cần tạo một bản sao của đối số, cho dù đó là mặc định hay không! Python không phải là ngôn ngữ gọi theo giá trị nên nó không tạo ra bản sao cho bạn, bạn cần phải rõ ràng về nó.

Ví dụ trong câu hỏi có thể rơi vào loại 1 hoặc 3. Thật kỳ lạ khi cả hai đều sửa đổi danh sách đã qua và trả về nó; bạn nên chọn cái này hay cái khác


"Làm điều gì đó sai" là chẩn đoán. Điều đó nói rằng, tôi nghĩ rằng có những lúc was = Không có mẫu nào hữu ích, nhưng nói chung bạn không muốn sửa đổi nếu vượt qua một biến đổi trong trường hợp đó (2). Các cache={}mô hình thực sự là một giải pháp chỉ phỏng vấn, trong mã thực sự bạn có thể muốn @lru_cache!
Andy Hayden

9

"Lỗi" này đã cho tôi rất nhiều giờ làm việc ngoài giờ! Nhưng tôi bắt đầu thấy một tiềm năng sử dụng của nó (nhưng tôi vẫn thích nó ở thời điểm thực hiện)

Tôi sẽ cung cấp cho bạn những gì tôi thấy là một ví dụ hữu ích.

def example(errors=[]):
    # statements
    # Something went wrong
    mistake = True
    if mistake:
        tryToFixIt(errors)
        # Didn't work.. let's try again
        tryToFixItAnotherway(errors)
        # This time it worked
    return errors

def tryToFixIt(err):
    err.append('Attempt to fix it')

def tryToFixItAnotherway(err):
    err.append('Attempt to fix it by another way')

def main():
    for item in range(2):
        errors = example()
    print '\n'.join(errors)

main()

in như sau

Attempt to fix it
Attempt to fix it by another way
Attempt to fix it
Attempt to fix it by another way

8

Chỉ cần thay đổi chức năng là:

def notastonishinganymore(a = []): 
    '''The name is just a joke :)'''
    a = a[:]
    a.append(5)
    return a

7

Tôi nghĩ rằng câu trả lời cho câu hỏi này nằm ở cách python truyền dữ liệu cho tham số (truyền theo giá trị hoặc bằng tham chiếu), chứ không phải khả năng biến đổi hoặc cách python xử lý câu lệnh "def".

Giới thiệu ngắn gọn. Đầu tiên, có hai loại dữ liệu trong python, một loại là loại dữ liệu cơ bản đơn giản, như số và một loại dữ liệu khác là các đối tượng. Thứ hai, khi truyền dữ liệu cho các tham số, python chuyển loại dữ liệu cơ bản theo giá trị, nghĩa là tạo một bản sao cục bộ của giá trị cho một biến cục bộ, nhưng truyền đối tượng theo tham chiếu, tức là con trỏ tới đối tượng.

Thừa nhận hai điểm trên, hãy giải thích điều gì đã xảy ra với mã trăn. Đó chỉ là do chuyển qua tham chiếu cho các đối tượng, nhưng không liên quan gì đến biến đổi / bất biến, hoặc có thể cho rằng thực tế là câu lệnh "def" chỉ được thực thi một lần khi được định nghĩa.

[] là một đối tượng, vì vậy python chuyển tham chiếu của [] sang a, tức alà chỉ là một con trỏ tới [] nằm trong bộ nhớ dưới dạng đối tượng. Chỉ có một bản sao của [] với, tuy nhiên, nhiều tài liệu tham khảo về nó. Đối với foo () đầu tiên, danh sách [] được thay đổi thành 1 theo phương thức chắp thêm. Nhưng Lưu ý rằng chỉ có một bản sao của đối tượng danh sách và đối tượng này bây giờ trở thành 1 . Khi chạy foo () thứ hai, trang web effbot nói gì (các mục không được đánh giá nữa) là sai. ađược đánh giá là đối tượng danh sách, mặc dù bây giờ nội dung của đối tượng là 1 . Đây là hiệu ứng của việc vượt qua bằng cách tham khảo! Kết quả của foo (3) có thể dễ dàng xuất phát theo cùng một cách.

Để xác nhận thêm câu trả lời của tôi, chúng ta hãy xem hai mã bổ sung.

====== Số 2 ========

def foo(x, items=None):
    if items is None:
        items = []
    items.append(x)
    return items

foo(1)  #return [1]
foo(2)  #return [2]
foo(3)  #return [3]

[]là một đối tượng, vì vậy None(cái trước là có thể thay đổi trong khi cái sau là bất biến. Nhưng khả năng biến đổi không liên quan gì đến câu hỏi). Không có nơi nào trong không gian nhưng chúng tôi biết nó ở đó và chỉ có một bản sao của Không có ở đó. Vì vậy, mỗi khi foo được gọi, các mục được đánh giá (trái ngược với một số câu trả lời rằng nó chỉ được đánh giá một lần) là Không, để rõ ràng, tham chiếu (hoặc địa chỉ) của Không có. Sau đó, trong foo, mục được đổi thành [], nghĩa là trỏ đến một đối tượng khác có địa chỉ khác.

====== Số 3 =======

def foo(x, items=[]):
    items.append(x)
    return items

foo(1)    # returns [1]
foo(2,[]) # returns [2]
foo(3)    # returns [1,3]

Việc gọi foo (1) làm cho các mục trỏ đến một đối tượng danh sách [] với một địa chỉ, giả sử, 11111111. nội dung của danh sách được thay đổi thành 1 trong hàm foo trong phần tiếp theo, nhưng địa chỉ không bị thay đổi, vẫn là 11111111 Sau đó, foo (2, []) đang đến. Mặc dù [] trong foo (2, []) có cùng nội dung với tham số mặc định [] khi gọi foo (1), địa chỉ của chúng khác nhau! Vì chúng tôi cung cấp tham số rõ ràng, itemsphải lấy địa chỉ của cái mới này[] , giả sử 2222222 và trả lại sau khi thực hiện một số thay đổi. Bây giờ foo (3) được thực thi. từ chỉxđược cung cấp, các mục phải lấy lại giá trị mặc định của nó. Giá trị mặc định là gì? Nó được đặt khi xác định hàm foo: đối tượng danh sách nằm trong 11111111. Vì vậy, các mục được đánh giá là địa chỉ 11111111 có phần tử 1. Danh sách nằm ở 2222222 cũng chứa một phần tử 2, nhưng nó không được chỉ ra bởi các mục hơn. Do đó, một phụ lục 3 sẽ tạo ra items[1,3].

Từ những giải thích trên, chúng ta có thể thấy rằng trang web effbot được đề xuất trong câu trả lời được chấp nhận không thể đưa ra câu trả lời có liên quan cho câu hỏi này. Hơn nữa, tôi nghĩ rằng một điểm trong trang web effbot là sai. Tôi nghĩ rằng mã liên quan đến UI.Button là chính xác:

for i in range(10):
    def callback():
        print "clicked button", i
    UI.Button("button %s" % i, callback)

Mỗi nút có thể giữ một chức năng gọi lại riêng biệt sẽ hiển thị giá trị khác nhau i. Tôi có thể cung cấp một ví dụ để hiển thị điều này:

x=[]
for i in range(10):
    def callback():
        print(i)
    x.append(callback) 

Nếu chúng tôi thực hiện, x[7]()chúng tôi sẽ nhận được 7 như mong đợi và x[9]()sẽ cung cấp cho 9, một giá trị khác i.


5
Điểm cuối cùng của bạn là sai. Hãy thử nó và bạn sẽ thấy đó x[7]()9.
Duncan

2
"python vượt qua loại dữ liệu cơ bản theo giá trị, nghĩa là tạo một bản sao cục bộ của giá trị cho một biến cục bộ" là hoàn toàn không chính xác. Tôi ngạc nhiên khi một người rõ ràng có thể biết Python rất rõ, nhưng lại có sự hiểu lầm khủng khiếp như vậy về các nguyên tắc cơ bản. :-(
Veky

6

TLDR: Mặc định thời gian xác định là nhất quán và biểu cảm rõ ràng hơn.


Xác định hàm ảnh hưởng đến hai phạm vi: phạm vi xác định chứa hàm và phạm vi thực thi được chứa bởi hàm. Mặc dù khá rõ ràng làm thế nào các khối ánh xạ tới phạm vi, câu hỏi là nơi def <name>(<args=defaults>):thuộc về:

...                           # defining scope
def name(parameter=default):  # ???
    ...                       # execution scope

Các def namephần phải đánh giá trong phạm vi quy định - chúng tôi muốn namecó sẵn ở đó, sau khi tất cả. Đánh giá chức năng chỉ bên trong chính nó sẽ làm cho nó không thể truy cập.

parameterlà một tên không đổi, chúng ta có thể "đánh giá" nó cùng lúc với def name. Điều này cũng có lợi thế là nó tạo ra hàm với một chữ ký đã biết name(parameter=...):, thay vì để trần name(...):.

Bây giờ, khi nào cần đánh giá default?

Tính nhất quán đã nói "theo định nghĩa": mọi thứ khác def <name>(<args=defaults>):cũng được đánh giá tốt nhất theo định nghĩa. Trì hoãn các phần của nó sẽ là sự lựa chọn đáng kinh ngạc.

Hai lựa chọn không tương đương nhau: Nếu defaultđược đánh giá ở thời điểm xác định, nó vẫn có thể ảnh hưởng đến thời gian thực hiện. Nếu defaultđược đánh giá tại thời điểm thực hiện, nó không thể ảnh hưởng đến thời gian xác định. Chọn "tại định nghĩa" cho phép thể hiện cả hai trường hợp, trong khi chọn "tại thực thi" chỉ có thể diễn tả một:

def name(parameter=defined):  # set default at definition time
    ...

def name(parameter=default):     # delay default until execution time
    parameter = default if parameter is None else parameter
    ...

"Tính nhất quán đã nói" theo định nghĩa ": mọi thứ khác def <name>(<args=defaults>):cũng được đánh giá tốt nhất theo định nghĩa." Tôi không nghĩ rằng kết luận sau tiền đề. Chỉ vì hai thứ nằm trên cùng một dòng không có nghĩa là chúng nên được đánh giá trong cùng một phạm vi. defaultlà một điều khác biệt so với phần còn lại của dòng: đó là một biểu thức. Đánh giá một biểu thức là một quá trình rất khác với việc xác định hàm.
LarsH

@LarsH Các định nghĩa hàm được đánh giá bằng Python. Cho dù đó là từ một câu lệnh ( def) hoặc biểu thức ( lambda) không thay đổi mà việc tạo ra một hàm có nghĩa là đánh giá - đặc biệt là chữ ký của nó. Và mặc định là một phần của chữ ký của hàm. Điều đó không có nghĩa là mặc định phải được đánh giá ngay lập tức - ví dụ gợi ý loại có thể không. Nhưng nó chắc chắn gợi ý họ nên trừ khi có lý do chính đáng để không.
MisterMiyagi

OK, tạo một hàm có nghĩa là đánh giá theo một nghĩa nào đó, nhưng rõ ràng không phải theo nghĩa là mọi biểu thức bên trong nó được đánh giá tại thời điểm định nghĩa. Hầu hết không. Tôi không rõ ràng về mặt ý nghĩa của chữ ký được đặc biệt "đánh giá" tại thời điểm định nghĩa nhiều hơn phần thân hàm được "đánh giá" (được phân tích thành một biểu diễn phù hợp); trong khi các biểu thức trong thân hàm rõ ràng không được đánh giá theo nghĩa đầy đủ. Từ quan điểm này, tính nhất quán sẽ nói rằng các biểu thức trong chữ ký không nên được đánh giá "đầy đủ".
LarsH

Tôi không có nghĩa là bạn sai, chỉ có điều kết luận của bạn không tuân theo sự nhất quán một mình.
LarsH

@LarsH Mặc định không phải là một phần của cơ thể, tôi cũng không cho rằng tính nhất quán là tiêu chí duy nhất. Bạn có thể đưa ra một gợi ý làm thế nào để làm rõ câu trả lời?
MisterMiyagi

3

Mỗi câu trả lời khác giải thích lý do tại sao đây thực sự là một hành vi tốt đẹp và mong muốn, hoặc tại sao bạn không nên cần điều này. Của tôi là dành cho những người cứng đầu muốn thực hiện quyền uốn cong ngôn ngữ theo ý muốn của họ, không phải cách khác.

Chúng tôi sẽ "sửa" hành vi này với một trình trang trí sẽ sao chép giá trị mặc định thay vì sử dụng lại cùng một thể hiện cho mỗi đối số vị trí còn lại ở giá trị mặc định của nó.

import inspect
from copy import copy

def sanify(function):
    def wrapper(*a, **kw):
        # store the default values
        defaults = inspect.getargspec(function).defaults # for python2
        # construct a new argument list
        new_args = []
        for i, arg in enumerate(defaults):
            # allow passing positional arguments
            if i in range(len(a)):
                new_args.append(a[i])
            else:
                # copy the value
                new_args.append(copy(arg))
        return function(*new_args, **kw)
    return wrapper

Bây giờ hãy xác định lại chức năng của chúng tôi bằng cách sử dụng trang trí này:

@sanify
def foo(a=[]):
    a.append(5)
    return a

foo() # '[5]'
foo() # '[5]' -- as desired

Điều này đặc biệt gọn gàng cho các hàm có nhiều đối số. Đối chiếu:

# the 'correct' approach
def bar(a=None, b=None, c=None):
    if a is None:
        a = []
    if b is None:
        b = []
    if c is None:
        c = []
    # finally do the actual work

với

# the nasty decorator hack
@sanify
def bar(a=[], b=[], c=[]):
    # wow, works right out of the box!

Điều quan trọng cần lưu ý là giải pháp trên sẽ bị hỏng nếu bạn cố gắng sử dụng từ khóa args, như vậy:

foo(a=[4])

Trình trang trí có thể được điều chỉnh để cho phép điều đó, nhưng chúng tôi để điều này như một bài tập cho người đọc;)

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.