Sử dụng tốt cho các giá trị mặc định của đối số hàm có thể thay đổi?


82

Một lỗi phổ biến trong Python là đặt một đối tượng có thể thay đổi làm giá trị mặc định của một đối số trong một hàm. Đây là một ví dụ được lấy từ bài viết xuất sắc này của David Goodger :

>>> def bad_append(new_item, a_list=[]):
        a_list.append(new_item)
        return a_list
>>> print bad_append('one')
['one']
>>> print bad_append('two')
['one', 'two']

Lời giải thích tại sao điều này xảy ra là ở đây .

Và bây giờ cho câu hỏi của tôi: Có một trường hợp sử dụng tốt cho cú pháp này không?

Ý tôi là, nếu tất cả mọi người gặp phải lỗi giống nhau, gỡ lỗi nó, hiểu vấn đề và từ đó cố gắng tránh nó, thì cú pháp như vậy có ích lợi gì?


1
Lời giải thích tốt nhất mà tôi biết cho điều này là trong câu hỏi được liên kết: các hàm là các đối tượng hạng nhất, giống như các lớp. Các lớp có dữ liệu thuộc tính có thể thay đổi; các hàm có giá trị mặc định có thể thay đổi.
Katriel

10
Hành vi này nó không phải là "lựa chọn thiết kế" - nó là kết quả từ cách thức hoạt động của ngôn ngữ - bắt đầu từ các nguyên tắc làm việc đơn giản, với càng ít ngoại lệ càng tốt. Tại một số điểm đối với tôi, khi tôi bắt đầu để "nghĩ bằng Python" hành vi này chỉ trở nên tự nhiên - và tôi sẽ ngạc nhiên nếu nó đã không xảy ra
jsbueno

2
Tôi cũng tự hỏi điều này. Ví dụ này ở khắp nơi trên web, nhưng nó không có ý nghĩa gì - hoặc bạn muốn thay đổi danh sách đã chuyển và có một mặc định không có ý nghĩa hoặc bạn muốn trả lại một danh sách mới và bạn nên tạo một bản sao ngay lập tức khi nhập hàm. Tôi không thể tưởng tượng trường hợp mà nó hữu ích để làm cả hai.
Mark Ransom vào


2
Tôi vừa xem qua một ví dụ thực tế hơn không có vấn đề mà tôi phàn nàn ở trên. Giá trị mặc định là đối số của __init__hàm cho một lớp, được đặt thành một biến thể hiện; đây là một điều hoàn toàn hợp lệ để muốn làm, và tất cả đều sai lầm khủng khiếp với một mặc định có thể thay đổi. stackoverflow.com/questions/43768055/…
Đánh dấu tiền chuộc

Câu trả lời:


61

Bạn có thể sử dụng nó để lưu trữ các giá trị giữa các lần gọi hàm:

def get_from_cache(name, cache={}):
    if name in cache: return cache[name]
    cache[name] = result = expensive_calculation()
    return result

nhưng thường thì loại việc đó được thực hiện tốt hơn với một lớp vì sau đó bạn có thể có các thuộc tính bổ sung để xóa bộ nhớ cache, v.v.


12
... hoặc một người trang trí ghi nhớ.
Daniel Roseman

28
@functools.lru_cache(maxsize=None)
Katriel

3
@katrielalex lru_cache là phiên bản mới trong Python 3.2 nên không phải ai cũng có thể sử dụng nó.
Duncan

2
FYI hiện có backports.functools_lru_cache pypi.python.org/pypi/backports.functools_lru_cache
Panda

1
lru_cachekhông khả dụng nếu bạn có các giá trị không thể truy cập.
Synedraacus

13

Câu trả lời chính tắc là trang này: http://effbot.org/zone/default-values.htm

Nó cũng đề cập đến 3 trường hợp sử dụng "tốt" cho đối số mặc định có thể thay đổi:

  • liên kết biến cục bộ với giá trị hiện tại của biến bên ngoài trong một lệnh gọi lại
  • bộ nhớ cache / ghi nhớ
  • liên kết cục bộ của các tên toàn cầu (cho mã được tối ưu hóa cao)

12

Có thể bạn không thay đổi đối số có thể thay đổi, nhưng mong đợi một đối số có thể thay đổi:

def foo(x, y, config={}):
    my_config = {'debug': True, 'verbose': False}
    my_config.update(config)
    return bar(x, my_config) + baz(y, my_config)

(Có, tôi biết bạn có thể sử dụng config=()trong trường hợp cụ thể này, nhưng tôi thấy điều đó kém rõ ràng và ít tổng quát hơn.)


2
Ngoài ra, hãy đảm bảo rằng bạn không đột biếnkhông trả về giá trị mặc định này trực tiếp từ hàm, nếu không một số mã bên ngoài hàm có thể thay đổi nó và nó sẽ ảnh hưởng đến tất cả các lệnh gọi hàm.
Andrey Semakin

10
import random

def ten_random_numbers(rng=random):
    return [rng.random() for i in xrange(10)]

Sử dụng randommô-đun, một cách hiệu quả là một singleton có thể thay đổi, làm bộ tạo số ngẫu nhiên mặc định của nó.


7
Nhưng đây cũng không phải là một trường hợp sử dụng quá quan trọng.
Evgeni Sergeev

3
Tôi nghĩ rằng không có sự khác biệt về hành vi, giữa "lấy tham chiếu một lần" của Python và "tra cứu randommột lần cho mỗi lần gọi hàm " của Python . Cả hai đều sử dụng cùng một đối tượng.
nyanpasu64

4

EDIT (làm rõ): Vấn đề đối số mặc định có thể thay đổi là một dấu hiệu của lựa chọn thiết kế sâu hơn, cụ thể là các giá trị đối số mặc định được lưu trữ dưới dạng thuộc tính trên đối tượng hàm. Bạn có thể hỏi tại sao lựa chọn này lại được thực hiện; như mọi khi, những câu hỏi như vậy rất khó trả lời đúng. Nhưng nó chắc chắn có những công dụng tốt:

Tối ưu hóa hiệu suất:

def foo(sin=math.sin): ...

Lấy giá trị đối tượng trong một bao đóng thay vì biến.

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

7
số nguyên và các hàm nội trang không thể thay đổi!
Phục hồi Monica

2
@Jonathan: Vẫn không có đối số mặc định có thể thay đổi trong ví dụ còn lại, hay tôi không thấy nó?
Phục hồi Monica

2
@Jonathan: quan điểm của tôi không phải là chúng có thể thay đổi được. Đó là hệ thống mà Python sử dụng để lưu trữ các đối số mặc định - trên đối tượng hàm, được xác định tại thời điểm biên dịch - có thể hữu ích. Điều này ngụ ý vấn đề đối số mặc định có thể thay đổi, vì việc đánh giá lại đối số trên mỗi lệnh gọi hàm sẽ khiến thủ thuật trở nên vô dụng.
Katriel

2
@katriealex: OK, nhưng hãy nói như vậy trong câu trả lời của bạn rằng bạn giả định rằng các đối số sẽ phải được đánh giá lại và bạn chỉ ra lý do tại sao điều đó lại tệ. Nit-pick: các giá trị đối số mặc định không được lưu trữ tại thời điểm biên dịch, nhưng khi câu lệnh định nghĩa hàm được thực thi.
Phục hồi Monica

@WolframH: đúng: P! Mặc dù cả hai thường xuyên trùng hợp.
Katriel

-1

Để trả lời câu hỏi về cách sử dụng tốt cho các giá trị đối số mặc định có thể thay đổi, tôi đưa ra ví dụ sau:

Mặc định có thể thay đổi có thể hữu ích cho việc lập trình các lệnh dễ sử dụng, có thể nhập do bạn tạo ra. Phương thức mặc định có thể thay đổi có thể có các biến tĩnh, riêng tư trong một hàm mà bạn có thể khởi tạo trong lần gọi đầu tiên (rất giống với một lớp) nhưng không cần phải sử dụng đến toàn cầu, không cần phải sử dụng trình bao bọc và không cần phải khởi tạo đối tượng lớp đã được nhập. Nó theo cách riêng của nó thanh lịch, như tôi hy vọng bạn sẽ đồng ý.

Hãy xem xét hai ví dụ sau:

def dittle(cache = []):

    from time import sleep # Not needed except as an example.

    # dittle's internal cache list has this format: cache[string, counter]
    # Any argument passed to dittle() that violates this format is invalid.
    # (The string is pure storage, but the counter is used by dittle.)

     # -- Error Trap --
    if type(cache) != list or cache !=[] and (len(cache) == 2 and type(cache[1]) != int):
        print(" User called dittle("+repr(cache)+").\n >> Warning: dittle() takes no arguments, so this call is ignored.\n")
        return

    # -- Initialize Function. (Executes on first call only.) --
    if not cache:
        print("\n cache =",cache)
        print(" Initializing private mutable static cache. Runs only on First Call!")
        cache.append("Hello World!")
        cache.append(0)
        print(" cache =",cache,end="\n\n")
    # -- Normal Operation --
    cache[1]+=1 # Static cycle count.
    outstr = " dittle() called "+str(cache[1])+" times."
    if cache[1] == 1:outstr=outstr.replace("s.",".")
    print(outstr)
    print(" Internal cache held string = '"+cache[0]+"'")
    print()
    if cache[1] == 3:
        print(" Let's rest for a moment.")
        sleep(2.0) # Since we imported it, we might as well use it.
        print(" Wheew! Ready to continue.\n")
        sleep(1.0)
    elif cache[1] == 4:
        cache[0] = "It's Good to be Alive!" # Let's change the private message.

# =================== MAIN ======================        
if __name__ == "__main__":

    for cnt in range(2):dittle() # Calls can be loop-driven, but they need not be.

    print(" Attempting to pass an list to dittle()")
    dittle([" BAD","Data"])
    
    print(" Attempting to pass a non-list to dittle()")
    dittle("hi")
    
    print(" Calling dittle() normally..")
    dittle()
    
    print(" Attempting to set the private mutable value from the outside.")
    # Even an insider's attempt to feed a valid format will be accepted
    # for the one call only, and is then is discarded when it goes out
    # of scope. It fails to interrupt normal operation.
    dittle([" I am a Grieffer!\n (Notice this change will not stick!)",-7]) 
    
    print(" Calling dittle() normally once again.")
    dittle()
    dittle()

Nếu bạn chạy mã này, bạn sẽ thấy rằng hàm dittle () nội bộ hóa trong cuộc gọi đầu tiên chứ không phải trong các cuộc gọi bổ sung, nó sử dụng bộ đệm tĩnh riêng (mặc định có thể thay đổi) để lưu trữ tĩnh nội bộ giữa các cuộc gọi, từ chối các nỗ lực chiếm đoạt lưu trữ tĩnh, có khả năng chống lại đầu vào độc hại và có thể hoạt động dựa trên các điều kiện động (ở đây là số lần hàm đã được gọi.)

Chìa khóa để sử dụng giá trị mặc định có thể thay đổi không phải làm bất cứ điều gì sẽ gán lại biến trong bộ nhớ, mà là luôn thay đổi biến tại chỗ.

Để thực sự thấy sức mạnh tiềm tàng và tính hữu ích của kỹ thuật này, hãy lưu chương trình đầu tiên này vào thư mục hiện tại của bạn với tên "DITTLE.py", sau đó chạy chương trình tiếp theo. Nó nhập và sử dụng lệnh dittle () mới của chúng tôi mà không yêu cầu bất kỳ bước nào để ghi nhớ hoặc lập trình vòng lặp để nhảy qua.

Đây là ví dụ thứ hai của chúng tôi. Biên dịch và chạy chương trình này như một chương trình mới.

from DITTLE import dittle

print("\n We have emulated a new python command with 'dittle()'.\n")
# Nothing to declare, nothing to instantize, nothing to remember.

dittle()
dittle()
dittle()
dittle()
dittle()

Bây giờ không phải là bóng bẩy và sạch sẽ nhất có thể? Các giá trị mặc định có thể thay đổi này thực sự có ích.

========================

Sau khi suy nghĩ về câu trả lời của mình một lúc, tôi không chắc rằng mình đã tạo ra sự khác biệt giữa việc sử dụng phương pháp mặc định có thể thay đổi và cách thông thường để hoàn thành cùng một điều rõ ràng.

Cách thông thường là sử dụng một hàm có thể nhập để bao bọc một đối tượng Lớp (và sử dụng một toàn cục). Vì vậy, để so sánh, đây là một phương thức dựa trên Lớp cố gắng thực hiện những điều tương tự như phương thức mặc định có thể thay đổi.

from time import sleep

class dittle_class():

    def __init__(self):
        
        self.b = 0
        self.a = " Hello World!"
        
        print("\n Initializing Class Object. Executes on First Call only.")
        print(" self.a = '"+str(self.a),"', self.b =",self.b,end="\n\n")
    
    def report(self):
        self.b  = self.b + 1
        
        if self.b == 1:
            print(" Dittle() called",self.b,"time.")
        else:
            print(" Dittle() called",self.b,"times.")
        
        if self.b == 5:
            self.a = " It's Great to be alive!"
        
        print(" Internal String =",self.a,end="\n\n")
            
        if self.b ==3:
            print(" Let's rest for a moment.")
            sleep(2.0) # Since we imported it, we might as well use it.
            print(" Wheew! Ready to continue.\n")
            sleep(1.0)

cl= dittle_class()

def dittle():
    global cl
    
    if type(cl.a) != str and type(cl.b) != int:
        print(" Class exists but does not have valid format.")
        
    cl.report()

# =================== MAIN ====================== 
if __name__ == "__main__":
    print(" We have emulated a python command with our own 'dittle()' command.\n")
    for cnt in range(2):dittle() # Call can be loop-driver, but they need not be.
    
    print(" Attempting to pass arguments to dittle()")
    try: # The user must catch the fatal error. The mutable default user did not. 
        dittle(["BAD","Data"])
    except:
        print(" This caused a fatal error that can't be caught in the function.\n")
    
    print(" Calling dittle() normally..")
    dittle()
    
    print(" Attempting to set the Class variable from the outside.")
    cl.a = " I'm a griefer. My damage sticks."
    cl.b = -7
    
    dittle()
    dittle()

Lưu chương trình dựa trên Lớp này trong thư mục hiện tại của bạn với tên DITTLE.py, sau đó chạy mã sau (mã này giống như trước đó.)

from DITTLE import dittle
# Nothing to declare, nothing to instantize, nothing to remember.

dittle()
dittle()
dittle()
dittle()
dittle()

Bằng cách so sánh hai phương pháp, lợi ích của việc sử dụng mặc định có thể thay đổi trong một hàm sẽ rõ ràng hơn. Phương thức mặc định có thể thay đổi không cần hình cầu, không thể đặt trực tiếp các biến bên trong. Và trong khi phương thức có thể thay đổi chấp nhận một đối số đã được thông qua cho một chu trình đơn lẻ sau đó loại bỏ nó, phương thức Lớp đã bị thay đổi vĩnh viễn vì biến bên trong của nó được tiếp xúc trực tiếp với bên ngoài. Đối với phương pháp nào dễ dàng hơn để lập trình? Tôi nghĩ rằng điều đó phụ thuộc vào mức độ thoải mái của bạn với các phương pháp và mức độ phức tạp của mục tiêu của bạn.

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.