Note: see the link below for the English version of this article.
https://duongnt.com/init_subclass-metaclass
Metaclass là một tính năng thú vị trong Python. Nó giúp ta lấy thông tin và thay đổi hành vi của lớp mà không cần thay đổi mã nguồn. Nhưng với các bài toán đơn giản, sử dụng metaclass cũng giống như dùng dao giết ruồi. Từ phiên bản 3.6, Python bổ sung một hàm mới với tên gọi là __init_subclass__
. Ta có thể dùng hàm này thay cho metaclass trong phần lớn các trường hợp.
Khái quát về metaclass
Metaclass là một lớp đặc biệt, nó kế thừa trực tiếp từ type
và định nghĩa hàm __new__
. Hàm này được gọi ngay tại thời điểm ta import lớp, trước khi bất kỳ đối tượng nào thuộc lớp được khởi tạo.
class DummyMeta(type):
def __new__(cls, name, bases, class_dict):
# Code trong hàm này được chạy khi import lớp
class DummyClass(metaclass=DummyClass):
# Code để định nghĩa lớp
Dưới đây là ý nghĩa của các tham số trong hàm __new__
.
- cls: metaclass hiện tại. Trong ví dụ của ta, giá trị này là
DummyMeta
. - name: tên của lớp sử dụng metaclass. Trong ví dụ của ta, giá trị này là
DummyClass
. - bases: tất cả các lớp cha của
DummyClass
. - class_dict: các thành viên trong
DummyClass
. Chúng bao gồm cả thuộc tính lẫn hàm.
Dùng metaclass để bắt buộc các class khác phải đặt tên sử dụng snake case
Theo quy ước chung, tên của hàm và thuộc tính trong Python thường sử dụng snake case. Ta sẽ viết một metaclass để tạo exception khi một lớp định nghĩa hàm hay tham số với tên gọi sử dụng camel case hoặc pascal case. Bước kiểm tra này được thực hiện ngay khi ta import lớp, trước khi các đối tượng thuộc lớp được khởi tạo.
Đầu tiên, ta định nghĩa một metaclass với tên gọi SnakeCaseMeta
.
class SnakeCaseMeta(type):
def __new__(cls, name, bases, class_dict):
not_camel_case = set()
for ele in class_dict:
if cls._not_snake_case(ele) and ele not in not_camel_case:
not_camel_case.add(ele)
if not_camel_case:
raise ValueError(f'Các thành viên sau đây có tên không dùng snake case: {", ".join(not_camel_case)}')
return type.__new__(cls, name, bases, class_dict)
@classmethod
def _not_snake_case(cls, txt):
return txt.lower() != txt
Trong hàm __new__
, ta duyệt qua tất cả các thuộc tính và hàm của lớp, rồi kiểm tra xem tên của chúng có dùng snake case hay không. Tất cả những tên không dùng snake case sẽ được lưu vào trong một list, rồi ta hiển thị chúng trong thông báo lỗi.
Chú ý là hàm _not_snake_case
chỉ kiểm tra xem text có chứa ký tự viết hoa hay không. Cách kiểm tra này là không đủ để dùng trong các project thực tế.
Cách dùng SnakeCaseMeta
rất đơn giản.
class Animal(metaclass=SnakeCaseMeta):
def EatMethod(self):
print('This animal can eat.')
def sleepMethod(self):
print('This animal can sleep.')
Khi ta import module chứa lớp Animal
, interpreter sẽ gặp lỗi dưới đây.
ValueError: Các thành viên sau đây có tên không dùng snake case: EatMethod, sleepMethod
Bước kiểm tra này không chỉ được áp dụng cho Animal
mà còn được áp dụng cho các lớp con của nó.
Hạn chế của metaclass
Ta phải định nghĩa metaclass riêng
Giả sử ta có cây các lớp con của Animal
như dưới đây.
Vì Animal
sử dụng SnakeCaseMeta
làm metaclass nên bước kiểm tra sẽ được áp dụng cho tất cả các lớp con. Đây là điều ta mong muốn, nhưng sẽ còn tiện hơn nếu ta có thể loại bỏ metaclass và chuyển bước kiểm tra vào trong Animal
.
Một class chỉ có thể có một metaclass
Giả sử ta muốn ghi lại tất cả những thời điểm lớp Animal
được import bằng metaclass dưới đây.
class LogImportMeta(type):
def __new__(cls, name, bases, class_dict):
print(f'Import class {name}')
return type.__new__(cls, name, bases, class_dict)
Nhưng ở đây ta gặp khó khăn: một lớp chỉ có thể có một metaclass. Vì Animal
đã dùng SnakeCaseMeta
làm metaclass nên nó không thể dùng LogImportMeta
được nữa. Ta có thể giải quyết vấn đề này bằng một vài cách dưới đây, nhưng không cách nào là hoàn hảo.
- Kết hợp 2 lớp
LogImportMeta
vàSnakeCaseMeta
lại làm một. Cách này vi phạm quy tắc single-responsibility principle, vì metaclass mới sẽ đảm nhiệm 2 việc hoàn toàn khác nhau. - Tạo cây các metaclass như dưới đây. Cách này làm tăng độ phức tạp của code. Và cây metaclass đó cũng không mấy hợp lý.
Thay thế metaclass bằng init_subclass
Từ bản 3.6, Python bổ sung một hàm mới với tên gọi là __init_subclass__
. Trong nhiều trường hợp, ta có thể thay thế metaclass bằng việc định nghĩa hàm __init__subclass__
. Dưới đây là cách dùng __init__subclass__
để kiểm tra xem tên thành viên trong class có dùng snake case hay không.
class VerifySnakeCase:
def __init_subclass__(cls):
super().__init_subclass__()
not_camel_case = set()
for ele in cls.__dict__:
if cls._not_snake_case(ele) and ele not in not_camel_case:
not_camel_case.add(ele)
if not_camel_case:
raise ValueError(f'Các thành viên sau đây có tên không dùng snake case: {", ".join(not_camel_case)}')
@classmethod
def _not_snake_case(cls, txt):
return txt.lower() != txt
Sau đó ta có thể áp dùng VerifySnakeCase
cho các lớp khác bằng cách dùng kế thừa.
class Animal(VerifySnakeCase):
# lược bỏ phần còn lại
Tương tự trên, ta định nghĩa lớp LogImport
như sau.
class LogImport:
def __init_subclass__(cls):
super().__init_subclass__()
print(f'Import {cls}')
Lúc này, ta có thể dùng đồng thời cả VerifySnakeCase
lẫn LogImport
.
class Animal(VerifySnakeCase, LogImport):
# lược bỏ phần còn lại
Kết thúc
Metaprogramming là một trong những tính năng thú vị nhất của Python. Nó giúp tăng sức mạnh và độ linh hoạt cho Python. Và với sự bổ sung của hàm __init_subclass__
, metaprogramming lại càng trở nên dễ dàng và đơn giản hơn.
One Thought on “Dùng init_subclass để thay thế cho metaclass”