Hướng dẫn dùng amazing def python

Note: see the link below for the English version of this article.

Nội dung chính Show

  • Quản lý tài nguyên một cách thủ công
  • Biến lớp Connection thành context manager
  • 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
  • Context manager mà không cần tạo lớp mới
  • Format tag HTML với context manager
  • Viết lớp HtmlTag
  • Thử chạy formatter vừa tạo
  • Bài tập về nhà cho bạn đọc
  • Kết thúc

https://duongnt.com/context-manager

Hướng dẫn dùng amazing def python

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,…

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('h2') 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'')
    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/h2/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?