Python đối số mặc định có thể thay đổi: Tại sao?


20

Tôi biết rằng các đối số mặc định được tạo tại thời điểm khởi tạo hàm và không phải mỗi lần hàm được gọi. Xem mã sau đây:

def ook (item, lst=[]):
    lst.append(item)
    print 'ook', lst

def eek (item, lst=None):
    if lst is None: lst = []
    lst.append(item)
    print 'eek', lst

max = 3
for x in xrange(max):
    ook(x)

for x in xrange(max):
    eek(x)

Những gì tôi không nhận được là tại sao điều này được thực hiện theo cách này. Hành vi này mang lại lợi ích gì cho việc khởi tạo tại mỗi thời điểm cuộc gọi?


Điều này đã được thảo luận chi tiết đáng kinh ngạc về Stack Overflow: stackoverflow.com/q/1132941/5419599
Wildcard

Câu trả lời:


14

Tôi nghĩ lý do là đơn giản thực hiện. Hãy để tôi giải thích.

Giá trị mặc định của hàm là một biểu thức mà bạn cần đánh giá. Trong trường hợp của bạn, đó là một biểu thức đơn giản không phụ thuộc vào bao đóng, nhưng nó có thể là một cái gì đó có chứa các biến miễn phí - def ook(item, lst = something.defaultList()). Nếu bạn định thiết kế Python, bạn sẽ có một lựa chọn - bạn có đánh giá nó một lần khi hàm được xác định hoặc mỗi khi hàm được gọi. Python chọn cái đầu tiên (không giống như Ruby, đi với tùy chọn thứ hai).

Có một số lợi ích cho việc này.

Đầu tiên, bạn nhận được một số tăng tốc độ và bộ nhớ. Trong hầu hết các trường hợp, bạn sẽ có các đối số mặc định bất biến và Python có thể xây dựng chúng chỉ một lần, thay vì trên mỗi lệnh gọi hàm. Điều này giúp tiết kiệm (một số) bộ nhớ và thời gian. Tất nhiên, nó không hoạt động khá tốt với các giá trị có thể thay đổi, nhưng bạn biết làm thế nào bạn có thể đi xung quanh.

Một lợi ích khác là sự đơn giản. Rất dễ hiểu cách biểu thức được đánh giá - nó sử dụng phạm vi từ vựng khi hàm được xác định. Nếu họ đi theo một cách khác, phạm vi từ vựng có thể thay đổi giữa định nghĩa và lời gọi và làm cho việc gỡ lỗi khó hơn một chút. Python đi một chặng đường dài để cực kỳ đơn giản trong những trường hợp đó.


3
Điểm thú vị - mặc dù đó thường là nguyên tắc ít gây bất ngờ nhất với Python. Một số điều đơn giản theo nghĩa phức tạp chính thức của mô hình, nhưng không rõ ràng và đáng ngạc nhiên, và tôi nghĩ rằng điều này có giá trị.
Steve314

1
Điều ít gây ngạc nhiên nhất ở đây là: nếu bạn có ngữ nghĩa đánh giá trên mỗi cuộc gọi, bạn có thể gặp lỗi nếu việc đóng thay đổi giữa hai lệnh gọi hàm (điều này hoàn toàn có thể). Điều này có thể đáng ngạc nhiên hơn là trái ngược với việc biết rằng nó được đánh giá một lần. Tất nhiên, người ta có thể lập luận rằng khi bạn đến từ các ngôn ngữ khác, bạn ngoại trừ ngữ nghĩa đánh giá trên mỗi cuộc gọi và đó là điều ngạc nhiên, nhưng bạn có thể thấy cách nó đi cả hai chiều :)
Stefan Kanev

Điểm hay về phạm vi
0xc0de

Tôi nghĩ rằng phạm vi thực sự là bit quan trọng hơn. Vì bạn không bị hạn chế các hằng số cho mặc định, bạn có thể yêu cầu các biến không có trong phạm vi tại trang web cuộc gọi.
Đánh dấu tiền chuộc

5

Một cách để đặt nó là lst.append(item) không biến đổi lsttham số. lstvẫn tham khảo cùng một danh sách. Chỉ là nội dung của danh sách đó đã bị thay đổi.

Về cơ bản, Python không có (mà tôi nhớ lại) bất kỳ biến không đổi hoặc bất biến nào cả - nhưng nó có một số loại không đổi, không thay đổi. Bạn không thể sửa đổi một giá trị số nguyên, bạn chỉ có thể thay thế nó. Nhưng bạn có thể sửa đổi nội dung của một danh sách mà không cần thay thế nó.

Giống như một số nguyên, bạn không thể sửa đổi một tham chiếu, bạn chỉ có thể thay thế nó. Nhưng bạn có thể sửa đổi nội dung của đối tượng được tham chiếu.

Đối với việc tạo đối tượng mặc định một lần, tôi tưởng tượng đó chủ yếu là tối ưu hóa, để tiết kiệm chi phí cho việc tạo đối tượng và thu gom rác.


+1 Chính xác. Điều quan trọng là phải hiểu lớp cảm ứng - rằng một biến không phải là giá trị; thay vào đó nó tham chiếu một giá trị. Để thay đổi một biến, giá trị có thể được hoán đổi hoặc biến đổi (nếu nó có thể thay đổi).
Joonas Pulakka

Khi gặp bất cứ điều gì khó khăn liên quan đến các biến trong python, tôi thấy hữu ích khi xem "=" là "toán tử ràng buộc tên"; tên luôn luôn bật lại, cho dù thứ mà chúng ta ràng buộc nó là mới (đối tượng mới hoặc thể hiện loại bất biến) hay không.
StarWeaver

4

Hành vi này mang lại lợi ích gì cho việc khởi tạo tại mỗi thời điểm cuộc gọi?

Nó cho phép bạn chọn hành vi bạn muốn, như bạn đã thể hiện trong ví dụ của mình. Vì vậy, nếu bạn muốn rằng đối số mặc định là bất biến, bạn sử dụng một giá trị bất biến , chẳng hạn như Nonehoặc 1. Nếu bạn muốn làm cho đối số mặc định có thể thay đổi, bạn sử dụng một cái gì đó có thể thay đổi, chẳng hạn như []. Đó chỉ là sự linh hoạt, mặc dù phải thừa nhận, nó có thể cắn nếu bạn không biết.


2

Tôi nghĩ rằng câu trả lời thực sự là: Python được viết như một ngôn ngữ thủ tục và chỉ áp dụng các khía cạnh chức năng sau khi thực tế. Những gì bạn đang tìm kiếm là để mặc định tham số được thực hiện dưới dạng một bao đóng và các bao đóng trong Python thực sự chỉ được nướng một nửa. Để có bằng chứng về sự cố gắng này:

a = []
for i in range(3):
    a.append(lambda: i)
print [ f() for f in a ]

nơi cung cấp cho [2, 2, 2]nơi bạn mong đợi một đóng cửa thực sự để sản xuất [0, 1, 2].

Có khá nhiều thứ tôi muốn nếu Python có khả năng bọc mặc định tham số trong các bao đóng. Ví dụ:

def foo(a, b=a.b):
    ...

Sẽ cực kỳ tiện dụng, nhưng "a" không nằm trong phạm vi tại thời điểm xác định chức năng, vì vậy bạn không thể làm điều đó và thay vào đó phải thực hiện một cách khó hiểu:

def foo(a, b=None):
    if b is None:
        b = a.b

Điều này gần như giống nhau ... gần như vậy.



1

Một lợi ích rất lớn là ghi nhớ. Đây là một ví dụ tiêu chuẩn:

def fibmem(a, cache={0:1,1:1}):
    if a in cache: return cache[a]
    res = fib(a-1, cache) + fib(a-2, cache)
    cache[a] = res
    return res

và để so sánh:

def fib(a):
    if a == 0 or a == 1: return 1
    return fib(a-1) + fib(a-2)

Đo thời gian trong ipython:

In [43]: %time print(fibmem(33))
5702887
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 200 µs

In [43]: %time print(fib(33))
5702887
CPU times: user 1.44 s, sys: 15.6 ms, total: 1.45 s
Wall time: 1.43 s

0

Nó xảy ra bởi vì quá trình biên dịch trong Python được thực hiện bằng cách thực thi mã mô tả.

Nếu một người nói

def f(x = {}):
    ....

sẽ khá rõ ràng rằng bạn muốn có một mảng mới mỗi lần.

Nhưng nếu tôi nói:

list_of_all = {}
def create(stuff, x = list_of_all):
    ...

Ở đây tôi đoán tôi muốn tạo nội dung vào nhiều danh sách khác nhau và có một danh sách toàn cầu duy nhất khi tôi không chỉ định danh sách.

Nhưng làm thế nào trình biên dịch sẽ đoán điều này? Vậy tại sao phải thử? Chúng ta có thể dựa vào việc cái này có được đặt tên hay không, và đôi khi nó có thể giúp ích, nhưng thực sự nó chỉ là phỏng đoán. Đồng thời, có một lý do chính đáng để không thử - tính nhất quán.

Như nó là, Python chỉ thực thi mã. Biến list_of_all đã được gán một đối tượng, do đó, đối tượng đó được chuyển bằng tham chiếu vào mã mặc định x theo cách mà một lệnh gọi đến bất kỳ hàm nào sẽ nhận được tham chiếu đến một đối tượng cục bộ có tên ở đây.

Nếu chúng ta muốn phân biệt tên chưa được đặt tên với trường hợp được đặt tên, thì điều đó sẽ liên quan đến mã khi biên dịch thực thi gán theo một cách khác đáng kể so với nó được thực thi trong thời gian chạy. Vì vậy, chúng tôi không làm cho trường hợp đặc biệt.


-5

Điều này xảy ra vì các hàm trong Python là các đối tượng hạng nhất :

Các giá trị tham số mặc định được ước tính 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 .

Nó tiếp tục giải thích rằng việc chỉnh sửa giá trị tham số sẽ sửa đổi giá trị mặc định cho các cuộc gọi tiếp theo và rằng một giải pháp đơn giản sử dụng Không làm mặc định, với một thử nghiệm rõ ràng trong thân hàm, là tất cả những gì cần thiết để đảm bảo không có gì bất ngờ.

Điều đó có nghĩa là def foo(l=[])trở thành một thể hiện của chức năng đó khi được gọi và được sử dụng lại cho các cuộc gọi tiếp theo. Hãy nghĩ về các tham số chức năng như là tách rời các thuộc tính của đối tượng.

Pro có thể bao gồm tận dụng điều này để có các lớp có biến tĩnh giống như C. Vì vậy, tốt nhất là khai báo các giá trị mặc định Không và khởi tạo chúng khi cần:

class Foo(object):
    def bar(self, l=None):
        if not l:
            l = []
        l.append(5)
        return l

f = Foo()
print(f.bar())
print(f.bar())

g = Foo()
print(g.bar())
print(g.bar())

sản lượng:

[5] [5] [5] [5]

thay vì bất ngờ:

[5] [5, 5] [5, 5, 5] [5, 5, 5, 5]


5
Không. Bạn có thể định nghĩa các hàm (hạng nhất hoặc không) khác nhau để đánh giá lại biểu thức đối số mặc định cho mỗi cuộc gọi. Và mọi thứ sau đó, tức là khoảng 90% câu trả lời, hoàn toàn bên cạnh câu hỏi. -1

1
Vì vậy, sau đó chia sẻ với chúng tôi kiến ​​thức này về cách xác định các hàm để đánh giá đối số mặc định cho mỗi cuộc gọi, tôi muốn biết một cách đơn giản hơn so với Python Docs khuyến nghị.
đảo ngược

2
Ở cấp độ thiết kế ngôn ngữ, ý tôi là. Định nghĩa ngôn ngữ Python hiện nói rằng các đối số mặc định được xử lý theo cách chúng; nó cũng có thể nói rõ rằng các đối số mặc định được xử lý theo một cách khác. IOW bạn đang trả lời "đó là cách mọi thứ" cho câu hỏi "tại sao mọi thứ lại như vậy".

Python có thể đã triển khai các tham số mặc định tương tự như cách Coffeescript thực hiện nó. Nó sẽ chèn mã byte để kiểm tra các tham số bị thiếu và nếu chúng bị thiếu sẽ đánh giá biểu thức.
Winston Ewert
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.