Câu hỏi “Ít ngạc nhiên nhất” và Đối số mặc định có thể thay đổi


Bất cứ ai leng keng 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 sử dụng Python sẽ mong đợi hàm này luôn trả về một danh sách chỉ với một phần tử: [5]. Kết quả là thay vì rất khác nhau, và rất đáng kinh ngạc (đối với một 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 một lần đã có cuộc gặp gỡ đầu tiên với tính năng này, và gọi nó là "một 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ự là 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 bản thân mình) câu hỏi sau: lý do để ràng buộc đối số mặc định ở định nghĩa hàm là gì và không thực hiện chức năng? Tôi nghi ngờ hành vi có kinh nghiệm có một sử dụng thực tế (những người thực sự sử dụng các biến tĩnh trong C, mà 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 biệt là của Utaal, tôi đã xây dựng thêm:

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

Với tôi, có vẻ như quyết định thiết kế liên quan đến vị trí đặt phạm vi tham số: bên trong hàm hoặc "cùng" với nó?

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

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


2057
2017-07-15 18:00


gốc


Câu hỏi bổ sung - Sử dụng tốt cho các đối số mặc định có thể thay đổi - Jonathan
Tôi không nghi ngờ các đối số có thể thay đổi vi phạm nguyên tắc ngạc nhiên nhất cho một người trung bình, và tôi đã thấy người mới bắt đầu bước vào đó, sau đó thay thế danh sách gửi thư bằng cách gửi 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 "rõ ràng cho Hà Lan" (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 đề nghị với chuỗi tài liệu là tốt nhất, nhưng sự kháng cự đối với các chuỗi tài liệu và bất kỳ tài liệu nào (viết) ngày nay không phổ biến lắm. Cá nhân, tôi muốn một trang trí (nói @fixed_defaults). - Serge
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 phải tạo ra một hàm trả về một biến thể mà có thể tùy chọn là một khả năng thay đổi mà bạn có thể chuyển sang hàm? Hoặc nó thay đổi một biến thể hoặc tạo một cái mới. Tại sao bạn cần để làm cả hai với một chức năng? Và tại sao nên thông dịch viên đượ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 mà thông dịch viên xử lý các định nghĩa chức năng và các gợi ý ở đây. Đó là rất nhiều để làm cho một trường hợp sử dụng không cần thiết. - Alan Leuthard
"Người mới sử dụng Python sẽ mong đợi hàm này luôn trả về một danh sách chỉ với một phần tử: [5]"Tôi là một người mới làm quen với Python, và tôi sẽ không mong đợi điều này, bởi vì rõ ràng foo([1]) sẽ trở lại [1, 5], không phải [5]. Những gì bạn có nghĩa là để nói là một người mới sẽ mong đợi các chức năng được gọi không có tham số sẽ luôn quay trở lại [5]. - symplectomorphic
Dành cho ví dụ trong hướng dẫn Python, tại sao lại là if L is None: cần thiết? Tôi đã xóa thử nghiệm này và nó không tạo ra sự khác biệt - sdaffa23fdsf


Các câu trả lời:


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à vì internals, hoặc hiệu suất.
Nó chỉ đơn giản là 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 sau khi bạn suy nghĩ theo cách này, thì hoàn toàn có ý nghĩa: một hàm là một đối tượng được đánh giá dựa trên định nghĩa của nó; các 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ừ một cuộc gọi sang một 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ề những 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ó để hiểu rõ hơn về cách các đối tượng chức năng hoạt động.


1353
2017-07-17 21:29



Để bất cứ ai đọc câu trả lời ở trên, tôi khuyên bạn nên dành thời gian để đọc qua bài viết liên quan đến Effbot. 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 cho kết quả bộ nhớ đệm / memoisation là rất tiện dụng để biết! - Cam Jackson
Ngay cả khi đó là đối tượng hạng nhất, người ta vẫn có thể hình dung ra một thiết kế nơi mã 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 lần hàm được gọi. Tôi không nói điều đó sẽ tốt hơn, chỉ những chức năng là đối tượng hạng nhất không loại trừ hoàn toàn nó. - gerrit
Xin lỗi, nhưng bất cứ điều gì được coi là "WTF lớn nhất trong Python" là chắc chắn nhất 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 - 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 - Danny Pflughoeft
Cho dù đó có phải là 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 bằng cách nào đó cần thiết, tự nhiên và rõ ràng cho rằng các hàm là các đối tượng hạng nhất và đơn giản là không đúng. 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 của mỗi cuộc gọi (có khả năng sử dụng tên được khai báo trong phạm vi bao quanh). Không có lý do nào ở tất cả là không thể hoặc hợp lý để có các đối số mặc định được đánh giá mỗi lần hàm được gọi theo cùng một cách. - Mark Amery
Thiết kế không trực tiếp theo dõi từ 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 như các thuộc tính chứ không phải là các thuộc tính. - bukzor


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

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

def eat(food=fruits):
    ...

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

Tuy nhiên, giả sử 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 tại thực thi hàm thay vì khai báo hàm thì tôi sẽ ngạc nhiên (theo cách rất xấu) để phát hiện ra rằng trái cây đã bị thay đổi. Đây sẽ là IMO đáng kinh ngạc hơn là khám phá ra rằng foochức năng trên đã 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 StringBuffer khi nó được đặt vào bản đồ, hay nó lưu khóa bằng cách tham chiếu? Dù bằng cách nào, ai đó cũng kinh ngạc; hoặc là người cố gắng đưa vật thể ra khỏi Map bằng cách sử dụng một giá trị giống với giá trị mà họ đặt vào hoặc người không thể truy xuất đối tượng của họ ngay cả khi khóa họ đang sử dụng nghĩa là cùng một đối tượng được sử dụng để đưa nó vào bản đồ (đây là thực sự tại sao Python không cho phép các kiểu dữ liệu có sẵn có thể thay đổi được của nó được sử dụng như các khóa từ điển).

Ví dụ của bạn là một ví dụ điển hình 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 cho rằng nếu chúng ta "cố định" điều này, thì điều đó sẽ chỉ tạo ra một tình huống khác mà chúng bị cắn thay vào đó, và điều đó thậm chí còn kém 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 được; bạn luôn luôn chạy vào trường hợp một người nào đó 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ể có 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ự kinh ngạc hơn nữa, chưa kể là không tương thích ngược.


231
2017-07-15 18:11



Tôi nghĩ đó là vấn đề tranh luận. Bạn đang hành động trên một biến toàn cục. Bất kỳ đánh giá nào được thực hiện ở bất kỳ nơi nào trong mã của bạn liên quan đến biến toàn cầu của bạn bây giờ sẽ chính xác ("quả việt quất", "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
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 tôi thích ý tưởng sửa đổi một bộ khởi tạo như vậy ngay từ đầu, nhưng nếu tôi đã làm, tôi mong đợi 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 Blank
Tham số mặc định Là giống như bất kỳ trường hợp nào khác. Điều bất ngờ là tham số là một biến toàn cầu, và không phải là một biến cục bộ. Mà lần lượt là vì mã được thực thi ở định nghĩa hàm, không gọi. Một khi bạn nhận được điều đó, và điều đó cũng tương tự với các lớp học, nó hoàn toàn rõ ràng. - Lennart Regebro
Tôi thấy ví dụ sai lệch hơn là rực rỡ. Nếu some_random_function() nối thêm vào fruits thay vì gán cho nó, hành vi của eat()  sẽ thay đổi. Rất nhiều cho 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 đang yêu cầu sự cố. WTF thực sự là khi mọi người xác định một đối số mặc định mới (một danh sách theo nghĩa đen hoặc một cuộc gọi đến một hàm tạo), và vẫn Lấy chút. - alexis
Bạn chỉ tuyên bố rõ ràng global và phân công lại bộ tuple - hoàn toàn không có gì đáng ngạc nhiên nếu eat hoạt động khác sau đó. - user3467349


AFAICS chưa có ai đăng phần liên quan tài liệu:

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


195
2017-07-10 14:50



Các cụm từ "đây không phải là nói chung những gì đã được dự định" và "một cách xung quanh này là" mùi giống như họ đang tài liệu một lỗ hổng thiết kế. - bukzor
@Matthew: Tôi nhận thức rõ, nhưng nó không đáng để xem. Thông thường, bạn sẽ thấy các hướng dẫn và kiểu dáng theo phong cách vô điều kiện gắn cờ 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ồi một thuộc tính vào hàm (function.data = []) hoặc tốt hơn, tạo một đối tượng. - bukzor
@bukzor: Cạm bẫy cần phải được ghi nhận và ghi lại, đó là lý do tại sao câu hỏi này là tốt và đã nhận được rất nhiều upvotes. Đồng thời, những cạm bẫy không nhất thiết cần 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? Tuy nhiên, 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ỉ nhấm nháp ý kiến ​​về sự đổ vỡ đặc biệt này. - Matthew
Cụm từ "đây không phải là nói chung những gì đã được dự định" có nghĩa là "không phải những gì các lập trình viên thực sự muốn xảy ra," không "không phải là những gì Python là nghĩa vụ phải làm." - holdenweb
@oriadam Có lẽ bạn có thể muốn đăng câu hỏi về nó sau đó. Có lẽ bạn làm điều gì đó khác với những gì được dự định ... - glglgl


Tôi không biết gì về thông dịch viên bên trong Python (và tôi cũng không phải là một chuyên gia về trình biên dịch và thông dịch viên) 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ể.

Miễn là các đối tượng python có thể thay đổi Tôi nghĩ rằng điều này nên được đưa vào tài khoản khi thiết kế các công cụ đối số mặc định. Khi bạn tạo danh sách:

a = []

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

Tại sao a = [] trong

def x(a=[]):

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

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

người dùng, bạn có muốn một để mặc định cho ngày giờ tương ứng với thời điểm bạn xác định hoặc thực thi x? Trong trường hợp này, như trong trường hợp trước, tôi sẽ giữ nguyên hành vi giống như đối số mặc định "gán" là lệnh đầu tiên của hàm (datetime.now () được gọi trên hàm gọi). 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):

97
2017-07-15 23:21



Bạn có thể làm: def x (a = None): Và sau đó, nếu a là None, hãy đặt = datetime.datetime.now () - Anon
Tôi biết, đó chỉ là một ví dụ để giải thích tại sao tôi lại thích ràng buộc thời gian thực thi hơn. - Utaal
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 thất vọng. Bạn đã làm nó đẹp với tối thiểu là mờ và nhầm lẫn. Khi một người nào đó đến từ các hệ thống lập trình bằng C ++ và đôi khi ngây thơ "dịch" các tính năng ngôn ngữ, người bạn giả này đã đá tôi vào trong phần mềm của phần lớn thời gian, giống như các thuộc tính của lớp. Tôi hiểu tại sao mọi thứ theo cách này, nhưng tôi không thể không ghét nó, bất kể tích cực có thể đến từ nó là gì. Ít nhất nó trái ngược với kinh nghiệm của tôi, rằng có lẽ tôi sẽ (hy vọng) không bao giờ quên nó ... - AndreasT
@Andreas một khi bạn sử dụng Python đủ lâu, bạn bắt đầu thấy cách logic cho Python để diễn giải mọi thứ như thuộc tính lớp theo cách của nó - nó chỉ là do các quirks và giới hạn cụ thể của các ngôn ngữ như C ++ (và Java, và C # ...) rằng nó có ý nghĩa gì đối với nội dung của class {} khối được hiểu là thuộc về các phiên bản:) Nhưng khi các lớp là đối tượng hạng nhất, rõ ràng là điều tự nhiên là cho nội dung của chúng (trong bộ nhớ) để phản ánh nội dung của chúng (trong mã). - Karl Knechtel
Cấu trúc tiêu chuẩn không có giới hạn 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ữ năng động có vẻ giống như vô chính phủ với tôi: Tất cả mọi người đều miễn phí, nhưng bạn cần cấu trúc để có được một người nào đó dọn sạch thùng rác và mở đường. Đoán tôi già ... :) - AndreasT


Vâng, lý do là khá đơn giản rằng các ràng buộc được thực hiện khi mã được thực hiện, và định nghĩa chức năng được thực hiện, cũng ... khi các chức năng được xác định.

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 cùng một sự kiện bất ngờ. chuối là một thuộc tính lớp, và do đó, khi bạn thêm các thứ vào nó, nó sẽ được thêm vào tất cả các cá thể của lớp đó. Lý do chính xác là như nhau.

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

Có, đó là bất ngờ. Nhưng một khi đồng xu giảm xuống, nó hoàn toàn phù hợp với cách Python hoạt động nói chung. Trong 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ẽ grok python tốt hơn nhiều.

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


72
2017-07-15 18:54



Nếu nó khác nhau cho mỗi trường hợp, nó không phải là một thuộc tính lớp. Thuộc tính lớp là các thuộc tính trên CLASS. Do đó tên. Do đó chúng giống nhau cho tất cả các trường hợp. - Lennart Regebro
Ông đã không yêu cầu một mô tả về hành vi của Python, ông đã yêu cầu lý do. Không có gì trong Python chỉ là "Cách thức hoạt động"; tất cả đều làm những gì nó làm cho một lý do. - Glenn Maynard
Và tôi đã đưa ra lý do. - Lennart Regebro
Tôi sẽ không nói rằng "đây là một trợ giúp giảng dạy tốt", bởi vì nó không phải. - Geo
@Geo: Ngoại trừ điều đó. Nó giúp bạn hiểu rất nhiều điều trong Python. - Lennart Regebro


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

1. Hiệu suất

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

Nếu đánh giá thời gian 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 trên mỗi cuộc gọi, hoặc cần phải tự lưu trữ giá trị bên ngoài, gây ô nhiễm không gian tên của bạn và thêm độ dài.

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

Một mẹo hữu ích là liên kết các tham số của một lambda với hiện hành ràng buộc của một biến khi lambda được tạo ra. 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 đó họ sẽ ràng buộc i đến thời gian gọi giá trị của i, vì vậy bạn sẽ nhận được một danh sách các hàm mà tất cả đượ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 ra một đóng cửa tiếp theo với i ràng buộc, tức là:

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

3. Introspection

Xem xét mã:

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

Chúng tôi có thể lấy thông tin về các đối số và mặc định bằng cách sử dụng inspect mô-đun, mà

>>> 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 meta, trang trí, v.v.

Bây giờ, giả sử hành vi của các giá trị mặc định có thể được thay đổi để nó 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 quan sát và xem những đố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 bao giờ có thể 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ả về mã đó dưới dạng chuỗi.


50
2017-07-16 10:05



bạn có thể đạt được nội tâm cũng nếu cho mỗi có một hàm để tạo đối số mặc định thay vì một giá trị. mô-đun kiểm tra sẽ chỉ gọi hàm đó. - yairchu
@SilentGhost: Tôi đang nói về nếu hành vi đã được thay đổi để tạo lại nó - tạo ra nó một lần là hành vi hiện tại, và tại sao các vấn đề mặc định có thể thay đổi tồn tại. - Brian
@yairchu: Điều đó giả định rằng việc xây dựng là an toàn để làm như vậy (tức là không có tác dụng phụ). Nhìn vào các args không nên làm bất cứ điều gì, nhưng đánh giá mã tùy ý cũng có thể kết thúc có hiệu lực. - Brian
Một thiết kế ngôn ngữ khác 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 như sau: _expensive = expensive (); def foo (arg = _expensive), 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 đến với "bộ nhớ cache biến bên ngoài" - đó là một chút tiết hơn, và bạn kết thúc với các biến phụ trong không gian tên của bạn mặc dù. - Brian


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

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

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

  3. Tính hữu ích: Như Frederik Lundh đã chỉ ra trong lời giải thích của ông của "Giá trị tham số mặc định bằng Python", các 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 công bố to như một "Cảnh báo quan trọng" bên trong Đầu tiên tiểu mục của phần "Thêm về xác định chức năng". Cảnh báo thậm chí còn sử dụng chữ 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. Siêu học: Rơi vào cái bẫy thực sự là một thời điểm hữu ích (ít nhất là nếu bạn là người học phản chiếu), bởi vì sau đó bạn sẽ hiểu rõ hơn về điểm "Nhất quán" ở trên và điều đó sẽ dạy cho bạn rất nhiều về Python.


47
2018-03-30 11:18



Tôi mất một năm để tìm thấy hành vi này đang làm rối tung mã của tôi về sản xuất, cuối cùng đã xóa bỏ một tính năng hoàn chỉnh cho đến khi tôi gặp phải tình trạng thiếu sót thiết kế này một cách tình cờ. 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 lê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 đè thông số của nhau! Làm lỗ hổng bảo mật, lỗi và những gì không. - oriadam
@oriadam, không có hành vi phạm tội, nhưng tôi tự hỏi làm thế nào bạn học Python mà không cần chạy vào điều này trước đây. Tôi chỉ học Python ngay bây giờ và điều đáng tiếc này là được đề cập trong hướng dẫn Python chính thức ngay bên cạnh đề cập đầu tiên về các đối số mặc định. (Như đã đề cập ở điểm 4 của câu trả lời này.) Tôi cho rằng đạo đức là — thay vì không thông cảm - để đọc tài liệu chính thức của ngôn ngữ bạn sử dụng để tạo phần mềm sản xuất. - Wildcard
Ngoài ra, nó sẽ là đáng ngạc nhiên (với tôi) nếu một chức năng phức tạp chưa biết được gọi là ngoài các cuộc gọi chức năng tôi đang làm. - Vatine


Tại sao bạn không quan tâm?

Tôi là có thật không ngạc nhiên không ai thực hiện cái nhìn sâu sắc sâu sắc được cung cấp bởi Python (2 và 3 áp dụng) trên các cuộc gọi.

Cho một hàm nhỏ đơn giản func đị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 ra 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 đó cửa hàng đối số mặc định (danh sách trống [] ở đây) trong chính đối tượng hàm. Như câu trả lời hàng đầu được đề cập: danh sách a bây giờ có thể được coi là một hội viên của hàm func.

Vì vậy, chúng ta hãy làm một số nội tâm, một trước và sau để kiểm tra cách danh sách được mở rộng phía trong đối tượng hàm. Tôi đang sử dụng Python 3.x cho điều này, cho Python 2 cùng áp dụng (sử dụng __defaults__ hoặc là func_defaults trong Python 2; có, hai tên cho cùng một điều).

Chức năng Trước khi thực thi:

>>> 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 liên quan: Cuộc gọi):

>>> func.__defaults__
([],)

O.k, do đó, một danh sách trống là mục nhập duy nhất trong __defaults__, đúng như mong đợi.

Chức năng sau khi thực thi:

Bây giờ hãy thực thi hàm này:

>>> func()

Bây giờ, hãy xem những __defaults__ lần nữa:

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

Ngạc nhiên? Giá trị bên trong đối tượng thay đổi! Các cuộc gọi liên tiếp tới hàm này sẽ chỉ đơn giản là nối thêm vào hàm đó list vật:

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

Vì vậy, có bạn có nó, lý do tại sao điều này 'lỗ hổng' xảy ra, là vì đối số mặc định là một phần của đối tượng hàm. Không có gì kỳ 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à bình thường None làm mặc định và sau đó khởi tạo trong phần 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 hiện lại lần nữa, bạn luôn nhận được một danh sách trống mới trống nếu không có đối số nào được truyền cho a.


Để xác minh thêm rằng danh sách trong __defaults__ giống như được sử dụng trong hàm func bạn chỉ có thể thay đổi hàm của mình để trả về id của danh 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__ (Chức vụ [0] trong __defaults__) và bạn sẽ thấy cách chúng thực sự đề cập đến cùng một cá thể danh sách:

>>> 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 khi biên dịch hàm, hãy thử thực hiện như 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à liên kết nó với tên bar được thực hiện.


43
2017-12-09 07:13



Là id(...) cần thiết cho lần xác minh cuối cùng đó, hoặc là is toán tử trả lời cùng một câu hỏi? - das-g
@ das-g is sẽ làm tốt, tôi vừa mới sử dụng id(val) bởi vì tôi nghĩ nó có thể trực quan hơn. - Jim Fasarakis Hilliard
@JimFasarakisHilliard Tôi sẽ thêm lời nhắc vào input. như input('Did you just see me without calling me?'). Nó làm cho nó rõ ràng hơn. - Ev. Kounis
@ Ev.Kounis Tôi thích nó! Cảm ơn bạn đã chỉ ra điều đó. - Jim Fasarakis Hilliard


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

  1. khai báo hàm (lớp vv) 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ứ được chuyển qua tham chiếu

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 cuộc gọi gán 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 tạo từ giá trị mặc định và được in
  3. c thay đổi - hoạt động được thực hiện trên cùng một đối tượng - và nó được in

40
2017-07-15 19:15



Số 4 của bạn có thể gây nhầm lẫn cho mọi người, vì các số nguyên là không thay đổi và do đó "nếu" không đúng. Ví dụ, với d được đặt thành 0, d .__ thêm __ (1) sẽ trả về 1, nhưng d vẫn là 0. - Anon
(Thực ra, thêm vào là một ví dụ tồi, nhưng các số nguyên không thay đổi vẫn là điểm chính của tôi.) - Anon
vâng, đó không phải là ví dụ hay - ymv
Thực hiện nó để tôi thất vọng sau khi kiểm tra để thấy rằng, với b thiết lập để [], b .__ thêm __ ([1]) trả về [1] nhưng cũng để lại b vẫn [] mặc dù danh sách có thể thay đổi. Lỗi của tôi. - Anon
@ Men: có __iadd__, nhưng nó không hoạt động với int. Tất nhiên. :-) - Veky