Đóng gói hàm (Closure)
Closure là một hàm được định nghĩa bên trong một hàm khác, và hàm này sử dụng các biến từ hàm bên ngoài. Khi đó, chúng ta gọi hàm bên trong cùng với các biến được sử dụng là một closure. Ví dụ:
def tao_cong_thuc(a, b):
def tinh_toan(c):
return a + b + c
return tinh_toan
Chúng ta đã định nghĩa một hàm tao_cong_thuc, bên trong nó có một hàm tính_toan sử dụng các biến a, b từ hàm tao_cong_thuc. Hàm tao_cong_thuc trả về hàm tính_toan. Cách sử dụng như sau:
print('------cong_thuc1---------')
cong_thuc1 = tao_cong_thuc(1, 2)
print('cong_thuc1 trỏ đến:', cong_thuc1)
print(cong_thuc1(3))
print(cong_thuc1(4))
print('--------cong_thuc2---------')
cong_thuc2 = tao_cong_thuc(2, 3)
print('cong_thuc2 trỏ đến:', cong_thuc2)
print(cong_thuc2(10))
print(cong_thuc2(20))
Kết quả chạy:
-
Có thể thấy mỗi lần gọi hàm tao_cong_thuc, nó sẽ trả về một hàm tính_toan, và mỗi hàm tính_toan có một không gian nhớ riêng, tức là cong_thuc1 và cong_thuc2 trỏ đến các địa chỉ khác nhau
-
Có thể gọi nhiều lần cong_thuc1 hoặc cong_thuc2, và mỗi lần gọi chung một cong_thuc1 hoặc cong_thuc2 thì các biến a và b được dùng chung
-
Các lời gọi cong_thuc1 và cong_thuc2 độc lập với nhau, không ảnh hưởng lẫn nhau
Tại sao sử dụng closure
Tại sao lại sử dụng closure, nó khác gì với hàm thông thường và lớp? Chúng ta hãy xem một ví dụ để hiểu rõ hơn. Thay ví dụ line ở trên bằng một phương trình bậc nhất: y = kx + b. Trong phương trình này, các giá trị k và b khác nhau tương ứng với các đường thẳng có hệ số góc khác nhau và cắt trục y tại các điểm khác nhau. Nói đơn giản là các đường thẳng có hình dạng khác nhau.
Bây giờ, nhu cầu của chúng ta là tính giá trị y dựa trên giá trị x khác nhau. Ví dụ, khi k=1, b=1 thì y = x + 1, định nghĩa thành hàm sẽ là:
def tinh_y(x):
return x + 1
print(tinh_y(1))
print(tinh_y(2))
print(tinh_y(3))
# Kết quả
2
3
4
Nếu trong hàm trên, chúng ta muốn thay đổi k và b tùy ý, có thể cải tiến thành:
def tinh_y(k, x, b):
return k * x + b
print(tinh_y(1, 2, 1))
print(tinh_y(1, 2, 2))
print(tinh_y(1, 2, 3))
# Kết quả
3
4
5
Hai hàm trên đều có ưu và nhược điểm riêng:
Đối với hàm đầu tiên:
Ưu điểm: Đối với cùng một hình dạng đường thẳng (k và b không đổi), mỗi lần chỉ cần truyền giá trị x là đủ
Nhược điểm: Nếu muốn sử dụng đường thẳng có hình dạng khác, tức là tùy ý thiết lập k và b, mỗi lần đều phải sửa code trong hàm
Đối với hàm thứ hai:
Ưu điểm: Có thể tùy ý thiết lập các giá trị k, b, x
Nhược điểm: Nếu muốn tính toán nhiều lần cho cùng một hình dạng đường thẳng (k và b không đổi), thì cần phải viết lại các tham số k và b nhiều lần, như tinh_y(1, 2, 1), tinh_y(1, 2, 2), tinh_y(1, 2, 3), tức là lặp lại tham số k=1 và b=2
Có thể thấy ưu và nhược điểm của chúng thực chất là bổ sung cho nhau. Vậy có cách nào để đồng thời có cả hai ưu điểm trên không?
Tức là vừa có thể tùy ý thiết lập k và b, và đối với cùng một sự kết hợp của k và b, chỉ cần thiết lập một lần các giá trị k và b để lưu lại, sau đó mỗi lần gọi chỉ cần truyền x vào. Nhu cầu này có thể thực hiện bằng lớp, ví dụ:
class DuongThang(object):
def __init__(self, k, b):
self.k = k
self.b = b
def tinh_y(self, x):
return self.k * x + self.b
print('--------duong_thang1--------')
duong_thang1 = DuongThang(1, 2)
print(duong_thang1.tinh_y(1))
print(duong_thang1.tinh_y(2))
print(duong_thang1.tinh_y(3))
print('--------duong_thang2--------')
duong_thang2 = DuongThang(10, 20)
print(duong_thang2.tinh_y(1))
print(duong_thang2.tinh_y(2))
print(duong_thang2.tinh_y(3))
# Kết quả:
--------duong_thang1--------
3
4
5
--------duong_thang2--------
30
40
50
Bằng cách này, khi chúng ta cần các đường thẳng có hình dạng khác nhau (k và b khác nhau), chúng ta có thể tạo một đối tượng duong_thang khác một lần (không cần thiết lập lại k và b), và khi sử dụng đối tượng duong_thang để lấy giá trị y (gọi tinh_y), chỉ cần truyền x vào là đủ.
Qua lớp chúng ta có thể thực hiện nhu cầu, nhưng thực chất để tạo một phương thức đơn giản lại tạo một lớp, có thể giống như dùng dao mổ để giết gà, vì mỗi khi tạo đối tượng DuongThang đều gọi nhiều phương thức ma thuật không cần thiết và một số thứ kế thừa từ lớp cha, như vậy sẽ chiếm một số không gian hoặc tài nguyên không cần thiết, hiệu quả quá thấp. Trong khi đó, không gian cần thiết để tạo hàm nhỏ hơn nhiều so với tạo lớp. Vì vậy, chúng ta có thể sử dụng closure để thực hiện. Ví dụ:
def duong_thang(k, b):
def tinh_y(x):
return k * x + b
return tinh_y
print('--------duong_thang1--------')
duong_thang1 = duong_thang(1, 2)
print(duong_thang1(1))
print(duong_thang1(2))
print(duong_thang1(3))
print('--------duong_thang2--------')
duong_thang2 = duong_thang(10, 20)
print(duong_thang2(1))
print(duong_thang2(2))
print(duong_thang2(3))
# Kết quả:
--------duong_thang1--------
3
4
5
--------duong_thang2--------
30
40
50
Có thể hiểu rằng, khi gọi duong_thang(1, 2), hệ thống sẽ tạo một không gian nhớ để lưu các tham số truyền vào là k=1, b=2 và code định nghĩa của hàm tinh_y, sau đó biến duong_thang1 trỏ đến địa chỉ không gian nhớ đó. Khi gọi duong_thang1(1), nó sẽ gọi hàm tinh_y bên trong và sử dụng các giá trị k và b đã truyền vào trước đó. Khi gọi duong_thang(10, 20) mới, lại tạo một không gian nhớ mới để lưu k=10, b=20 và code định nghĩa của hàm tinh_y. Vì vậy, mỗi khi gọi hàm bên ngoài của closure, giống như trong lập trình hướng đối tượng đã tạo một thể hiện của lớp và mở ra một không gian nhớ độc lập, bên trong lưu thuộc tính thể hiện (tham số của hàm bên ngoài, như k và b) và phương thức thể hiện (hàm bên trong).
Sử dụng nonlocal để sửa đổi biến cục bộ của hàm bên ngoài
Trong closure, hàm bên trong có thể sử dụng trực tiếp biến và tham số của hàm bên ngoài, nhưng không thể sửa đổi trực tiếp, như ví dụ sau:
def ham_benngoai(a):
num = 0
print('Ban đầu,num:%d, id:%d' % (num, id(num)))
print('Ban đầu,a:%d, id:%d' % (a, id(a)))
def ham_bentro():
# nonlocal num, a
num = 1
a = 200
print()
print('Trong ham_bentro,num:%d, id:%d' % (num, id(num)))
print('Trong ham_bentro,a:%d, id:%d' % (a, id(a)))
ham_bentro()
print()
print('Ngoài,num:%d, id:%d' % (num, id(num)))
print('Ngoài,a:%d, id:%d' % (a, id(a)))
return ham_bentro
ham1 = ham_benngoai(100)
Kết quả chạy:
Ban đầu,num:0, id:140724620236416
Ban đầu,a:100, id:140724620239616
Trong ham_bentro,num:1, id:140724620236448
Trong ham_bentro,a:200, id:140724620242816
Ngoài,num:0, id:140724620236416
Ngoài,a:100, id:140724620239616
-
Ban đầu, biến cục bộ trỏ đến địa chỉ ....6416 và ....9616, trên địa chỉ này lưu hai giá trị 0 và 100
-
Trong hàm ham_bentro, cố gắng sửa đổi giá trị của biến cục bộ ở trên, tức là tạo hai địa chỉ mới ....6448 và ....2816, lưu hai giá trị 1 và 200, muốn biến cục bộ không còn trỏ đến địa chỉ cũ mà trỏ đến địa chỉ mới tạo
-
Nhưng kết quả phát hiện biến cục bộ vẫn trỏ đến địa chỉ cũ, tức là giá trị của biến không thay đổi, chứng tỏ không thay đổi được giá trị ban đầu
Để sửa đổi biến cục bộ bên ngoài, cần phải khai báo bằng nonlocal trước khi sửa đổi, như sau:
def ham_benngoai(a):
num = 0
print('Ban đầu,num:%d, id:%d' % (num, id(num)))
print('Ban đầu,a:%d, id:%d' % (a, id(a)))
def ham_bentro():
nonlocal num, a
num = 1
a = 200
print()
print('Trong ham_bentro,num:%d, id:%d' % (num, id(num)))
print('Trong ham_bentro,a:%d, id:%d' % (a, id(a)))
ham_bentro()
print()
print('Ngoài,num:%d, id:%d' % (num, id(num)))
print('Ngoài,a:%d, id:%d' % (a, id(a)))
return ham_bentro
ham1 = ham_benngoai(100)
Kết quả chạy:
Ban đầu,num:0, id:140724620236416
Ban đầu,a:100, id:140724620239616
Trong ham_bentro,num:1, id:140724620236448
Trong ham_bentro,a:200, id:140724620242816
Ngoài,num:1, id:140724620236448
Ngoài,a:200, id:140724620242816
Phát hiện cuối cùng biến cục bộ bên ngoài đã được sửa đổi thành công, tức là biến cục bộ num và a bên ngoài đã trỏ đến địa chỉ mới ....6448 và ...2816