Note: see the link below for the English version of this article.
https://duongnt.com/mixin-python
Khác với những ngôn ngữ như C# và Java, Python cho phép đa kế thừa. Đây là cơ sở cho nhiều chức năng thú vị, mà một trong số đó là mixin. Mixin là những lớp nhỏ, với mục đích duy nhất là bổ sung tính tăng cho những lớp khác. Khi đứng một mình thì chúng không có tác dụng gì, và ta không bao giờ khởi tạo chúng một cách trực tiếp.
Lớp trong các ví dụ
Giả sử ta có lớp Person
dưới đây với 2 attribute là name
và age
.
class Person:
def __init__(self, name, age, country):
self._name = name
self._age = age
Khi ta so sánh 2 object của lớp Person
, về thực chất ta đang so sánh reference của chúng.
p1 = Person('John', 32)
p2 = Person('John', 32)
print(p1 == p2) # False
Nếu ta muốn so sánh các attribute, ta cần viết lại hàm __eq__
.
class Person:
# Lược bỏ phần trên
def __eq__(self, o):
if not isinstance(o, Person):
return False
return self.__dict__ == o.__dict__
p1 = Person('John', 32)
p2 = Person('John', 32)
print(p1 == p2) # True
Bây giờ giả sử ta lại có một lớp khác gọi là Country
, và ta cũng muốn so sánh object của lớp này bằng cách so sánh attribute.
class Country:
def __init__(self, name, area):
self._name = name
self._area = area
def __eq__(self, o):
if not isinstance(o, Country):
return False
return self.__dict__ == o.__dict__
Có lẽ các bạn cũng đã thấy rằng hàm __eq__
bị lặp lại. Nếu ta muốn thay đổi hàm đó, ta sẽ phải thay đổi cùng lúc nhiều lớp.
Giải quyết vấn đề trên bằng mixin
Thay vì định nghĩa hàm __eq__
bên trong từng lớp, ta sẽ chuyển hàm đó vào trong một lớp phụ. Ta đặt tên cho lớp này là CompareByAttributeMixin
.
class CompareByAttributeMixin:
def __eq__(self, o):
if not isinstance(o, type(self)):
return False
return self.__dict__ == o.__dict__
Lúc này, ta có thể cho Person
và Country
kế thừa CompareByAttributeMixin
, nhờ đó ta sẽ thực hiện được so sánh object thông qua attribute.
class Person(CompareByAttributeMixin):
def __init__(self, name, age, country):
self._name = name
self._age = age
class Country(CompareByAttributeMixin):
def __init__(self, name, area):
self._name = name
self._area = area
p1 = Person('John', 32)
p2 = Person('John', 32)
print(p1 == p2) # True
c1 = Country('Vietnam', 331700)
c2 = Country('Vietnam', 331700)
print(c1 == c2) # True
Những lớp như CompareByAttributeMixin
với mục đích duy nhất là bổ sung tính năng cho lớp khác được gọi là mixin.
Cho một lớp kế thừa nhiều mixin
Nếu như ta thử tính hash của đối tượng thuộc lớp Person
hay Country
thì ta sẽ gặp lỗi.
print(hash(c1))
Đoạn code trên gây ra lỗi dưới đây.
Exception has occurred: TypeError
unhashable type: 'Person'
Nguyên nhân là vì lớp nào viết lại hàm __eq__
thì cũng phải viết lại hàm __hash__
, điều này được ghi rõ tại đây. Nếu không, hàm __hash__
sẽ được tự động chuyển thành None
, và ta không thể tính hash cho object thuộc lớp đó.
Để bật lại tính năng hash cho Person
, ta cần viết lại hàm __hash__
và tính hash dựa trên giá trị các attribute. Ta định nghĩa thêm một mixin với tên gọi là HashFromAttributeMixin
.
class HashFromAttributeMixin:
"""
Mặc định là tất cả các attribute đều là hashable
"""
def __hash__(self):
return sum(hash(v) for v in self.__dict__.values())
Rồi ta thêm mixin này vào cả Person
lẫn Country
.
class Person(HashFromAttributeMixin, CompareByAttributeMixin):
def __init__(self, name, age, country):
self._name = name
self._age = age
class Country(HashFromAttributeMixin, CompareByAttributeMixin):
def __init__(self, name, area):
self._name = name
self._area = area
Bây giờ ta có thể tính hash cho object của lớp Person
lẫn Country
.
print(hash(p1) == hash(p2)) # True
print(hash(c1) == hash(c2)) # True
Chú ý: Thông thường ta không nên tính hash dựa trên các mutable attribute. Đó là vì khi giá trị hash của một object bị thay đổi thì nó sẽ không còn nằm trong đúng hash bucket. Ngoải ra, nếu ta dùng object của lớp Person
hay Country
để làm key trong dictionary thì lúc hash thay đổi ta sẽ mất dấu giá trị tương ứng trong dictionary. Đoạn code ở trên chỉ mang tính chất minh họa.
Chú ý thứ tự các mixin trong khai báo lớp
Có lẽ những độc giả tinh ý đã nhận ra rằng ta đặt HashFromAttributeMixin
trước CompareByAttributeMixin
khi khai báo lớp. Nếu ta đảo ngược thứ tự này thì sao?
class PersonUnhashable(CompareByAttributeMixin, HashFromAttributeMixin):
def __init__(self, name, age, country):
self._name = name
self._age = age
p_unhashable = PersonUnhashable('John', 32)
print(hash(p_unhashable))
Đoạn code trên sẽ lại gây ra lỗi TypeError
.
Exception has occurred: TypeError
unhashable type: 'PersonUnhashable'
Và đây là kết quả khi ta kiểm tra hàm __hash__
của PersonUnhashable
.
print(PersonUnhashable.__hash__) # None
Còn hàm __hash__
của Person
thì vẫn kế thừa từ HashFromAttributeMixin
đúng như ta mong đợi.
print(Person.__hash__) # <function HashFromAttributeMixin.__hash__ at 0x000002551968B160>
Để hiểu lý do của hiện tượng trên, ta cần hiểu cách Python quyết định dùng hàm nào trong trường hợp lớp của ta có đa kế thừa.
Đa kế thừa và MRO
Một trong những vấn đề lớn của đa kế thừa là quyết định xem sẽ dùng phiên bản nào khi nhiều lớp cha cùng định nghĩa một hàm. Python giải quyết vấn đề trên bằng các sử dụng MRO của lớp. MRO là danh sách thứ tự các lớp cha của một lớp. Khi ta gọi một hàm của lớp, interpreter sẽ xem lớp đó có chứa hàm đó không. Nếu lớp không chứa hàm thì interpreter sẽ đi ngược MRO của lớp và check từng lớp cha cho đến khi tìm được một lớp có định nghĩa hàm đó.
Ta sẽ kiểm tra MRO của Person
và PersonUnhashable
.
print(Person.__mro__)
# (<class '__main__.Person'>, <class '__main__.HashFromAttributeMixin'>, <class '__main__.CompareByAttributeMixin'>, <class 'object'>)
print(PersonUnhashable.__mro__)
# (<class '__main__.PersonUnhashable'>, <class '__main__.CompareByAttributeMixin'>, <class '__main__.HashFromAttributeMixin'>, <class 'object'>)
Trong ví dụ trên, lớp đầu tiên trong MRO của PersonUnhashable
có chứa hàm __hash__
là CompareByAttributeMixin
. Vì ta đã viết lại hàm CompareByAttributeMixin.__eq__
, hàm CompareByAttributeMixin.__hash__
được tự động chuyển thành None
. Chính vì thế ta không thể tính hash cho object của lớp PersonUnhashable
.
Ngược lại, lớp đầu tiên trong MRO của Person
có chứa hàm __hash__
là HashFromAttributeMixin
. Vì thế ta có thể tính hash của object thuộc lớp Person
dựa trên giá trị của các attribute.
Như đã thấy ở trên, MRO không chỉ phụ thuộc vào tổng thể sơ đồ kế thừa của lớp, mà nó còn phụ thuộc vào thứ tự ta khai báo các lớp cha khi định nghĩa lớp con. Interpreter sử dụng một giải thuật gọi là C3 linearization để xây dựng MRO. Bản thân giải thuật đó không khó hiểu, nhưng có lẽ chúng ta không bao giờ cần phải tự mình tính MRO cho lớp.
Kết thúc
Mixin cho phép ta tận dụng sức mạnh của đa kế thừa. Và nếu ta hiểu cách tạo MRO trong Python, ta có thể đảm bảo rằng kết quả khi kết hợp nhiều mixin là đúng như ta mong đợi.