Python 101 – The great but nearly forgotten Decorators

Introduction

Python 101 sẽ là một series blog đụng chạm tới những điều hay ho của Python với tư cách là một ngôn ngữ. Rất mong nhận được sự ủng hộ của mọi người! Bài đầu tiên trong series này sẽ nói về decorator trong Python, một trợ thủ đắc lực nhưng không thường xuyên xuất hiện.

Vậy Decorator là cái gì, ăn được không?

Đừng nhầm lẫn, Decorator của ngôn ngữ Python và Decorator design pattern là 2 khái niệm riêng biệt nha.

Decorator trong Python chỉ đơn giản là một cách gọi hàm liên tiếp nhau với cú pháp @decorator_name. Ví dụ khi mình cần phải gọi 3 hàm khác nhau, mỗi hàm được gọi sau đều cần biết kết quả của tính toán trước, để trả về một kết quả thú vị nào đó:

final_result = do_step_3(do_step_2(do_step_1()))

Sử dụng decorator, định nghĩa hàm do_step_1 sẽ được rút xuống còn như sau:

@do_step_3
@do_step_2
def do_step_1(...): ...

Và toàn bộ quy trình sẽ được gọi ngắn gọn như sau:

do_step_1()

Ta hoàn toàn có thể dùng lại @do_step_3, @do_step_2 ở một định nghĩa hàm khác, ví dụ:

@do_step_3
@do_step_2
def do_step_1_alternative(...): ...

Dùng Decorator để làm gì?

Theo mình thấy, decorator giải quyết vấn đề phân tách cross-cutting concerns khá gọn gàng. Những concern này có thể là error handling, logging, caching, permission verification và hơn thế nữa. Ví dụ, ở đây mình có một đoạn code dùng error handling và đoạn error handling này được lặp đi lặp lại nhiều lần:

def do_():
  try:
    return do_error_prone_action()
  except MyKnownException as e:
    print("my exception, continue...")

Hoàn toàn có thể gói phần gọi try-except vào một decorator (implement thế nào phần sau chúng ta sẽ bàn tới). Kết quả là một định nghĩa hàm ngắn gọn siêu cấp vũ trụ:

@handle_my_exception
def do_():
  return do_error_prone_action()

Hay đấy, vậy implement decorator thế nào giờ?

Có thể tạo ra decorator không nhận tham số, hoặc decorator nhận một vài tham số. Có 2 cách để tạo ra từng loại decorator đó. Một là dùng nested function, hai là dùng class có method __call__ (bản chất là tạo ra một function). Hai cách này sẽ phù hợp hơn cho những trường hợp khác nhau, tuy nhiên về cơ bản là có thể dùng cách nào cũng được :D. Tutorial trên mạng thường dùng nested function, nên hãy cùng đi một lượt qua những định nghĩa decorator này theo cách này nhé!

Decorator không tham số

Chỉ cần tạo 1 function nhận vào 1 function khác và trả về một hàm mới:

def handle_exception(func: typing.Callable[..., Any]):
  @functools.wraps(func)
  def handle(*args, **kwargs):
    try:
      return func(*args, **kwargs)
    except MyException as e:
      return "poor you!"
  return handle

Để giữ lại tên hàm và stack trace chuẩn, cần phải decorate hàm được trả về với decorator @wraps từ module functools.

Khi dùng decorator này chỉ cần dùng @handle_exception mà không cần ngoặc ở sau. Nếu có ngoặc Python sẽ báo lỗi đó, nên bạn chú ý nha!

Decorator có tham số

Đây là nơi mọi thứ trở nên thú vị hơn. Nếu dùng nested function chúng ta phải đi 3 tầng. Tầng ngoài cùng định nghĩa tên decorator và tham số decorator cần. 2 tầng bên trong về cơ bản giống như decorator không tham số. Ví dụ như sau:

def handle_exception(
  ExceptionType: typing.Type[Exception],
  handler: typing.Callable[..., Any]
):
  def inner_one(func: typing.Callable[..., Any]):
    @functools.wraps(func)
    def handle(*args, **kwargs):
      try:
        return func(*args, **kwargs)
      except ExceptionType as e:
        return f"poor you! You encoutered {e}"
    return handle
  return inner_one

Để gọi decorator này bạn nhớ gọi với ngoặc và danh sách tham số nhé! Ngay cả khi tham số đều dùng mặc định thì đừng quên ngoặc đơn nha, Python cũng sẽ phàn nàn nếu thiếu ngoặc đó.

Chốt hạ

Mặc dù chỉ là một syntactic sugar tuy nhiên không thể phủ nhận đóng góp của decorator vào việc khiến code Python ngắn gọn và dễ hiểu hơn. Hẹn gặp mọi người trong các blog tiếp theo về Python nha!

Add a Comment

Scroll Up