Note: see the link below for the English version of this article.
https://duongnt.com/context-manager
Trong Python, context manager thường được dùng để quản lý tài nguyên. Nó giúp chúng ta không phải lặp lại logic cấp phát hay giải phóng tài nguyên. Nhưng không có lý do gì chúng ta phải bó buộc mình vào chỉ một mục đích duy nhất này. Với một chút sáng tạo, ta có thể dùng context manager để đo thời gian chạy của hàm hay format dữ liệu đầu ra,…
Nhắc lại về context manager
Quản lý tài nguyên một cách thủ công
Giả sử ta có một lớp để quản lý kết nối Internet như dưới đây. Lớp này có các hàm để mở và đóng kết nối.
class Connection:
def __init__(self):
self._connection = None
def open(self):
# code để mở kết nối
def close(self):
# code để đóng kết nối
def send(self, data):
# code để gửi dữ liệu
Phương pháp truyền thống để đảm bảo kết nối luôn được đóng lại sau khi sử dụng là dùng try-finally
.
conn = Connection()
try:
conn.open()
conn.send(data)
finally:
conn.close()
Biến lớp Connection thành context manager
Thay vì tự mình viết đoạn try-finally
, chúng ta có thể chuyển lớp Connection
thành context manager. Để làm điều đó, ta chỉ cần đưa code mở kết nối vào trong hàm __enter__
và đưa code đóng kết nối vào trong hàm __exit__
.
class Connection:
# ... lược bỏ bớt code không cần thiết
def __enter__(self):
self.open()
return self
def __exit__(self, exc_type, exc_value, traceback):
self._close()
Sau đó, ta có thể dùng lớp này với từ khóa with
. Hàm close
sẽ luôn được gọi, kể cả khi có lỗi.
with Connection() as conn:
conn.send(data)
Dùng context manager để đo thời gian chạy của hàm
Lớp để đo thời gian chạy của hàm
Như đã thấy trong phần trước, các lệnh trong hàm __enter__
luôn được thực thi khi ta vào đoạn code sử dụng with
. Và các lệnh trong hàm __exit__
luôn được gọi khi ta ra khỏi đoạn code đó. Vì vậy, ta có thể lợi dùng điều này để đo thời gian chạy của hàm khác. Dưới đây là một context manager với chức năng kể trên.
class Measure:
def __init__(self):
self.start = None
self.end = None
def __enter__(self):
self.start = datetime.datetime.now()
# Vì ta không cần dùng object với kiểu Measure nên ta không cần giá trị trả về
def __exit__(self, exc_type, exc_value, traceback):
self.end = datetime.datetime.now()
diff = (self.end - self.start)
print(f'Thời gian chạy: {diff.total_seconds()}s')
Ta sẽ thử dùng lớp Measure
để đo thời gian chạy của một hàm thử nghiệm.
def test_target():
for i in range(10000000):
j = i + 1
with Measure() as _:
test_target()
Trên máy của tôi, đoạn code trên trả về kết quả như sau.
Thời gian chạy: 1.133019s
Context manager mà không cần tạo lớp mới
Ta có thể viết context manager mà không cần tạo lớp mới. Lúc này, ta sử dụng decorator contextmanager
trong contextlib
. Dưới đây là phiên bản dùng contextmanager
.
@contextmanager
def measure_generator():
try:
start = datetime.datetime.now()
yield
finally:
end = datetime.datetime.now()
diff = (end - start)
print(f'Thời gian chạy: {diff.total_seconds()}s')
Ta có thể thấy là sau khi ghi lại thời điểm bắt đầu chạy hàm, chúng ta dùng từ khóa yield
để trả quyền điều khiển lại cho hàm muốn đo. Sau đó, khi code của ta ra khỏi đoạn sử dụng with
, phần code còn lại trong finally
sẽ được thực thi. Thông thường, ta sẽ để code giải phóng tài nguyên trong finally
.
Cách dùng measure_generator
cũng giống hệt cách dùng lớp Measure
.
with measure_generator() as _:
test_target()
Format tag HTML với context manager
Thoạt nghe, việc format tag HTML nghe không liên quan gì với context manager. Nhưng liệu ta có thể viết đoạn code như dưới đây, với độ lùi vào của từng lệnh with
bằng với độ lùi vào của tag tương ứng hay không?
with HtmlTag('html') as _:
with HtmlTag('head') as _:
with HtmlTag('title') as title:
title.print('Duong Blog')
with HtmlTag('script') as script:
script.print('https://example.com/script.js')
with HtmlTag('body') as _:
with HtmlTag('h1') as header:
header.print('Awesome header')
with HtmlTag('p') as section:
section.print('Lorem ipsum dolor sit amet, consectetur adipiscing elit.')
Viết lớp HtmlTag
Lớp HtmlTag
có một class attribute để lưu lại độ lùi của tag hiện tại. Khi ta tạo một object mới, object này sẽ lưu lại độ lùi đó vào trong instance attribute của riêng mình.
class HtmlTag:
INDENT = 2 # 2 spaces for each indentation level
depth = 0 # độ lùi sâu nhất hiện tại
def __init__(self, tag):
self.tag = tag
self.depth = HtmlTag.depth
Ta chia bài toán hiện tại thành 3 phần: mở tag, ghi nội dung, và đóng tag. Tất nhiên là việc mở tag được thực hiện trong hàm __enter__
. Ta sẽ dùng self.depth
để lùi tag hiện tại vào một khoảng phù hợp. Đồng thời, ta cần tăng giá trị của HtmlTag.depth
(không phải self.depth
) thêm 1 mỗi khi ta vào một đoạn with
mới.
def __enter__(self):
print(' ' * HtmlTag.INDENT * self.depth + f'<{self.tag}>')
HtmlTag.depth += 1
return self
Tương tự thế, khi ta ra khỏi đoạn code with
, hàm __exit__
sẽ đóng tag và giảm giá trị HtmlTag.depth
đi 1.
def __exit__(self, exc_type, exc_value, traceback):
print(' ' * HtmlTag.INDENT * self.depth + f'</{self.tag}>')
HtmlTag.depth -= 1
Ở giữa lúc mở tag và đóng tag, chúng ta dùng self.depth
và hàm print
để ghi nội dung tag. Nhớ là phần nội dung cần lùi vào thêm một cấp so với tag.
def print(self, txt):
print(' ' * HtmlTag.INDENT * (self.depth + 1) + txt)
Các bạn có thể tham khảo code hoàn chỉnh tại đường link này.
Thử chạy formatter vừa tạo
Kết quả khi chạy đoạn code trên là như sau.
Bài tập về nhà cho bạn đọc
Thông thường, tag và nội dung của các tag title/script/h1/p
thường được viết trên cùng một dòng. Phải làm sao để HtmlTag
xuất dữ liệu với format đó? Đáp án là ta cần thêm một tham số vào hàm khởi tạo của HtmlTag
để ghi nhận là tag và nội dung có cần được viết trên cùng một dòng hay không. Xin hãy thử tự mình thực hiện thay đổi trên trước khi tham khảo đáp án tại đường link sau đây).
Sau khi chỉnh sửa, lớp HtmlTag
cần xuất ra được kết quả như dưới đây.
Kết thúc
Mặc dù các lớp Measure
hay HtmlTag
ta vừa viết là chưa phù hợp để sử dụng trong thực tế, chúng vẫn giúp ta hiểu sâu hơn về context manager. Liệu bạn có tìm được ứng dụng thú vị nào khác cho context manager hay không?
One Thought on “Thủ thuật với Context Manager trong Python”