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

https://duongnt.com/mixin-python

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à nameage.

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 PersonCountry 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 PersonPersonUnhashable.

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__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__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.

A software developer from Vietnam and is currently living in Japan.

Leave a Reply