0
0
Lập trình
NM

Cách Khắc Phục Vấn Đề Với Tham Số Mặc Định Có Thể Thay Đổi Trong Python

Đăng vào 7 tháng trước

• 6 phút đọc

Giới Thiệu

Khi viết một hàm đơn giản trong Python, bạn có thể gặp phải một tình huống kỳ lạ liên quan đến tham số mặc định có thể thay đổi. Điều này có thể dẫn đến những kết quả không mong muốn và gây khó khăn cho việc gỡ lỗi. Bài viết này sẽ giúp bạn hiểu rõ hơn về vấn đề này và cung cấp cách xử lý hiệu quả.

Vấn Đề Thường Gặp

Hãy xem ví dụ sau:

python Copy
def add_to_list(item, target=[]):  # 🚨 Đây là lỗi nổi tiếng!
    target.append(item)
    return target

list_1 = add_to_list('a')  # Trả về ['a']
list_2 = add_to_list('b')  # Trả về ['a', 'b']... chờ đã, sao lại vậy?

print(list_1 is list_2)    # True - Chúng là cùng một đối tượng!

Tại sao list_2 lại nhớ được phần tử từ lần gọi đầu tiên? Nếu bạn đã gặp phải vấn đề này, chúc mừng bạn! Đây không phải là lỗi trong logic của bạn; đây là hành vi tinh tế trong Python đã khiến nhiều lập trình viên phải khó chịu. Hãy cùng tìm hiểu.

Nguyên Nhân Gốc Rễ: Thời Điểm Định Nghĩa So Với Thời Điểm Thực Thi

Sự nhầm lẫn bắt nguồn từ một điểm quan trọng: Python chỉ đánh giá tham số mặc định một lần duy nhất - khi hàm được định nghĩa, không phải mỗi khi nó được gọi.

Khi trình thông dịch đọc lệnh def, nó tạo ra đối tượng hàm và đánh giá bất kỳ giá trị mặc định nào ngay lúc đó. Danh sách rỗng [] chỉ được tạo ra một lần và gắn liền với đối tượng hàm.

Hãy nghĩ về nó như thế này:

python Copy
# Điều này xảy ra tại thời điểm ĐỊNH NGHĨA HÀM
_default_value = []  # Danh sách được sinh ra ở đây

def add_to_list(item, target=_default_value):
    target.append(item)
    return target

Mỗi khi bạn gọi add_to_list() mà không có tham số target, nó sẽ sử dụng danh sách gốc đó. Không có danh sách mới nào được tạo ra mỗi lần gọi.

Hình Dung Vấn Đề

Hãy phân tích quy trình từng bước. Chìa khóa là hiểu rằng danh sách mặc định là một đối tượng duy nhất được chia sẻ giữa tất cả các cuộc gọi hàm.

  1. Định Nghĩa Hàm

    • Khi Python định nghĩa hàm, nó tạo ra danh sách mặc định [] trong bộ nhớ.
    • Giả sử danh sách này được gán ID duy nhất (ID) 12345.
  2. Cuộc Gọi Đầu Tiên (list_1 = add_to_list('a'))

    • Hàm sử dụng danh sách tại ID 12345.
    • Nó thêm 'a' vào đó.
    • Trả về danh sách đã được sửa đổi.
    • Biến list_1 bây giờ là một tham chiếu tới danh sách này.
    • Trạng Thái Danh Sách (ID: 12345): ['a']
  3. Cuộc Gọi Thứ Hai (list_2 = add_to_list('b'))

    • Hàm lại sử dụng cùng một danh sách gốc tại ID 12345.
    • Nó thêm 'b' vào đó, làm cho danh sách thành ['a', 'b'].
    • Trả về danh sách này.
    • Biến list_2 bây giờ cũng là một tham chiếu tới cùng một danh sách.
    • Trạng Thái Danh Sách (ID: 12345): ['a', 'b']

Bây giờ, cả list_1list_2 đều là tham chiếu tới cùng một đối tượng danh sách tại ID 12345. Đây là lý do tại sao kiểm tra danh tính list_1 is list_2 trả về True. Chúng không chỉ là các danh sách tương tự; chúng là các tên khác cho cùng một đối tượng trong bộ nhớ.

Bạn có thể tự kiểm tra điều này bằng cách sử dụng hàm id():

python Copy
print(id(list_1)) # ví dụ: 140241231415040
print(id(list_2)) # ví dụ: 140241231415040 (Cùng một số!)

Tại Sao Đây Là Hành Vi Như Vậy?

Bạn có thể tự hỏi tại sao Python lại được thiết kế như thế này. Câu trả lời từ người sáng lập Python, Guido van Rossum, chủ yếu là do hiệu suất. Việc đánh giá tham số mặc định một lần tại thời điểm định nghĩa hàm hiệu quả hơn so với việc đánh giá lại chúng trong mỗi cuộc gọi, đặc biệt là đối với các hàm đơn giản, thường xuyên được gọi. Sự đánh đổi này là hiệu suất so với một cạm bẫy tiềm năng.

Hành vi này chỉ gây ra vấn đề với các đối tượng có thể thay đổi (danh sách, từ điển, tập hợp). Đối với các đối tượng không thể thay đổi (số nguyên, chuỗi, tuple), điều này không gây hại vì chúng không thể bị thay đổi tại chỗ. Bạn chỉ có thể gán lại biến, điều này không ảnh hưởng đến đối tượng mặc định gốc.

python Copy
# Không vấn đề gì với các giá trị mặc định không thể thay đổi
def increment(count=0):
    count += 1  # Điều này gán lại 'count'; nó không sửa đổi giá trị mặc định '0'
    return count

print(increment())  # 1
print(increment())  # 1 (một '0' mới không được tạo ra, nhưng giá trị gốc không thay đổi)

Giải Pháp Pythonic: Sử Dụng None Làm Sentinel

Cách chính thức để tránh cạm bẫy này rất đơn giản và thanh lịch: sử dụng None làm giá trị mặc định và tạo đối tượng có thể thay đổi bên trong hàm.

python Copy
def add_to_list_fixed(item, target=None):
    if target is None:  # ✅ Kiểm tra None với `is`
        target = []     # Một danh sách mới được tạo ra ở đây, tại thời điểm thực thi
    target.append(item)
    return target

list_1 = add_to_list_fixed('a') # Tạo một danh sách mới, trả về ['a']
list_2 = add_to_list_fixed('b') # Tạo một danh sách mới, trả về ['b']

print(list_1, list_2)  # ['a'] ['b']  ← Hành vi mong đợi!
print(list_1 is list_2) # False - Chúng là các đối tượng khác nhau.

Tại Sao Điều Này Hoạt Động: Thân hàm được thực thi trong mỗi cuộc gọi. Vì vậy, mỗi lần bạn gọi hàm mà không có target, mã target = [] được chạy, tạo ra một danh sách hoàn toàn mới.

Khi Nào Hành Vi Này Có Thể Hữu Ích?

Mặc dù thường là một cạm bẫy, hành vi này có thể được sử dụng có chủ đích cho các mẫu nâng cao như caching hoặc memoization, nơi bạn muốn hàm duy trì trạng thái giữa các cuộc gọi trong một biến cục bộ.

python Copy
def generate_id(prefix, cache=[]):
    """Tạo ID duy nhất với một tiền tố. Ví dụ: 'a_1', 'a_2', 'b_3'"""
    cache.append(prefix)
    return f"{prefix}_{len(cache)}"

print(generate_id('a')) # 'a_1'
print(generate_id('a')) # 'a_2' - Danh sách cache đã tồn tại

Tuy nhiên, đây là một kỹ thuật nâng cao và nên được tài liệu hóa rõ ràng để tránh gây nhầm lẫn cho các lập trình viên khác.

Bước Đầu Trong Hành Trình Python

Gặp gỡ và hiểu hành vi này là một cột mốc. Nó có nghĩa là bạn đang vượt qua cú pháp của Python và bắt đầu hiểu mô hình thực thi của nó - một dấu hiệu của một lập trình viên đang phát triển.

Vì vậy, lần tới khi bạn thấy một linter cảnh báo về một "tham số mặc định có thể thay đổi", bạn sẽ biết chính xác điều đó có nghĩa là gì và cách khắc phục nó. Bạn không chỉ sửa một lỗi; bạn đã làm chủ một trong những sắc thái nổi tiếng nhất của Python.


Aaron Rose là một kỹ sư phần mềm và nhà văn công nghệ tại tech-reader.blog và là tác giả của "Suy Nghĩ Như Một Thiên Tài".

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào