Python cơ bản
Python hướng đối tượng
Python nâng cao
Quảng cáo

Phương thức Magic hoặc Dunder trong Python

Trong hướng dẫn này chúng ta sẽ đi tìm hiểu về phương thức Magic và Nạp chồng toán tử (operator overloading) trong lập trình Python

Như bạn đã biết, Python định nghĩa sẵn một số phép toán trên các kiểu dữ liệu của mình. Ví dụ, phép cộng (+) đã được định nghĩa sẵn cho các kiểu số, kiểu xâu ký tự, kiểu danh sách.

Giả sử bạn xây dựng kiểu dữ liệu riêng (class) Matrix (ma trận). Do ma trận cũng có các phép toán như phép cộng, phép nhân, bạn cũng muốn trực tiếp áp dụng các phép toán này cho object của lớp Matrix.

Khi này bạn sẽ cần sử dụng đến kỹ thuật nạp chồng toán tử.

Nạp chồng toán tử (operator overloading) là kỹ thuật định nghĩa các phép toán sẵn có trên kiểu dữ liệu tự tạo.

Trong Python nạp chồng toán tử hoạt động tương đối khác với ngôn ngữ như C++/Java/C#. Các toán tử trong Python hoạt động dựa trên một số magic method.

Magic method trong lập trình Python

Các phương thức ma thuật (magic method) trong Python là các phương thức đặc biệt bắt đầu và kết thúc bằng dấu gạch dưới kép. Chúng còn được gọi là các phương pháp dunder. Các phương thức ma thuật không có nghĩa là được bạn gọi trực tiếp, nhưng việc gọi này xảy ra trong nội bộ lớp trên một hành động nhất định.

Nạp chồng toán tử hoạt động dựa trên magic method. Vì vậy trước khi học cách nạp chồng toán tử, chúng ta cần hiểu thế nào là magic method trong Python.

Khái niệm magic method được nhắc đến lần đầu khi bạn học về hàm tạo trong Python.

Magic method là một số phương thức đặc biệt được Python tự động gọi. Lấy ví dụ, khi bạn thực hiện phép cộng, Python sẽ tự động gọi tới phương thức __add__(). Khi bạn muốn khởi tạo một object, phương thức __new__() được Python tự động gọi để tạo object trước.

Các kiểu dữ liệu xây dựng sẵn của Python có một số magic method. Bạn có thể xem danh sách các phương thức này bằng lệnh dir() như sau:

Ví dụ

>>> dir(int) # danh sách magic method của kiểu int
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']

Một ví dụ nhỏ khác:

Ví dụ

>>> a = 10
>>> a.__add__(10) # tương đương với gọi a + 10
20
>>> a + 10
20

Qua ví dụ này bạn có thể thấy sự tương đương giữa phép toán + và lời gọi hàm __add__(). Khi bạn sử dụng phép toán +, Python tự động thay bạn gọi hàm __add__(). Bạn cũng có thể tự mình gọi tới magic method đứng sau phép +.

Tóm lại, magic method là những phương thức đứng sau các phép toán thường gặp. Bản thân phép toán chỉ là một dạng cú pháp tiện lợi hơn để gọi tới các phương thức này.

Khi hiểu điều này bạn hẳn có thể nhận ra ngay, thực hiện nạp chồng toán tử trong Python thực chất là định nghĩa lại magic method của phép toán trên class mới. Như vậy trong Python, nạp chồng toán tử = ghi đè magic method tương ứng.

Ghi đè phương thức __new__()

Ghi đè phương thức __new__() cho phép bạn nạp chồng phép toán tạo object. Đây chỉ là một ví dụ để bạn hiểu kỹ hơn về class, object và magic method trong Python. Trên thực tế bạn ít khi phải nạp chồng phép toán này.

Hãy xem ví dụ sau đây:

Ví dụ

class Employee:
    def __new__(cls, *args, **kwargs):
        print('__new__ is being called')
        instance = object.__new__(cls)
        return instance

    def __init__(self, name: str):
        print('__init__ is being called')
        self.name = name

    def print(self):
        print(self.name)

if __name__ == '__main__':
    trump = Employee('Donald Trump')
    trump.print()
    putin = Employee('Vladimir Putin')    
    putin.print()

Chạy script trên bạn thu được kết quả như sau:

Ví dụ

__new__ is being called
__init__ is being called
Donald Trump
__new__ is being called
__init__ is being called
Vladimir Putin

Hãy để ý đến cách ghi đè phương thức __new__():

Ví dụ

def __new__(cls, *args, **kwargs):
        print('__new__ is being called')
        instance = object.__new__(cls)
        return instance

Để ghi đè __new__ bạn phải cung cấp một tham số bắt buộc cls. Tham số này có vai trò tương tự như tham số trong class method: nó chính là class mà bạn đang cần tạo object. Python sẽ tự động truyền giá trị cho tham số này.

Hai tham số còn lại *args và **kwargs giúp bạn viết __init__() với tham số tùy ý. Sở dĩ phải có *args và **kwargs ở đây là vì __new__ đòi hỏi cùng tham số với __init__. Do __new__ được gọi tự động và __init__ xây dựng sau, chúng ta sử dụng *args và **kwargs cho __new__() để bao phủ hết các khả năng truyền tham số có thể gặp ở __init__().

Trong thân hàm __new__() bạn bắt buộc phải gọi tới phương thức __new__() của lớp cha object. Nhắc lại: object là lớp cha của mọi class trong Python. Lời gọi __new__() của object sẽ sinh ra object mới và bạn cần trả lại object này.

Kế tiếp, Python sẽ gọi __init__ và tự động truyền object mới sang phương thức __init__().

Khi chạy chương trình bạn có thể thấy rõ, khi gặp lệnh tạo object, __new__() được gọi tự động trước, sau đó mới đến __init__().

Ghi đè phương thức __str__() và __repr__()

Phương thức __str__() được gọi tự động khi bạn phát lệnh in object print(). Phương thức này sẽ chuyển object về một chuỗi ký tự phù hợp cho việc in.

Phương thức __repr__() được gọi tự động khi bạn muốn xuất biến ở chế độ tương tác.

Khi xây dựng một class, nếu muốn tiện lợi cho việc in object của class, bạn nên định nghĩa phương thức __str__() và __repr__().

Hãy xem ví dụ sau:

Ví dụ

class Vector:
    """A class for vector"""
    def __init__(self, x:float, y:float):
        self.x = x
        self.y = y
    def __str__(self):
        return f'({self.x}, {self.y})'
    def __repr__(self):
        return f'({self.x}, {self.y})'

Đây là ví dụ về một class Vector hai chiều đơn giản.

Thông thường, vector trong toán học hay được in ra ở dạng (x, y, z, ..). Chúng ta cũng muốn rằng khi dùng hàm print() trên object của Vector, kết quả in ra cũng có dạng (x, y).

Để làm việc này, chúng ta cần định nghĩa phương thức magic __str__() trong class Vector:

Ví dụ

def __str__(self):
    return f'({self.x}, {self.y})'

Phương thức này cần trả về một chuỗi ký tự.

__str__() rất giống với phương thức ToString() trong C#.

Với class Vector như trên, bạn có thể sử dụng như sau:

Ví dụ

>>> v1 = Vector(0, 1); v2 = Vector(2, 3)
>>> print(v1)
(0, 1)
>>> print(v2)
(2, 3)

Tuy nhiên, nếu chỉ có __str__(), khi bạn nhập lệnh:

Ví dụ

>>> v1 = Vector(1, 2)
>>> v1
<__main__.Vector object at 0x00000183425A2DC0>

Để giao diện tương tác cũng in ra kết quả như khi dùng hàm print(), bạn cần định nghĩa lại phương thức __repr__():

Ví dụ

def __repr__(self):
    return f'({self.x}, {self.y})'

Phần thân __repr__() và __str__() cơ bản là giống nhau.

Nạp chồng các phép toán số học

Các phép toán số học +, -, *, / tương ứng với các magic method __add__(), __sub__(), __mul__(), __div__().

Phép toán + – (âm dương) tương ứng với __pos__() và __neg__().

Để nạp chồng các phép toán này, bạn cần định nghĩa/ghi đè magic method tương ứng.

Hãy xem ví dụ sau đây:

Ví dụ

class Vector:
    """A class for vector"""

    def __init__(self, x:float, y:float):
        self.x = x
        self.y = y

    def __str__(self):
        return f'({self.x}, {self.y})'

    def __repr__(self):
        return f'({self.x}, {self.y})'

    def __add__(self, v):
        x = self.x + v.x
        y = self.y + v.y
        return Vector(x, y)

    def __sub__(self, v):
        x = self.x - v.x
        y = self.y - v.y
        return Vector(x, y)

    def __mul__(self, n):
        x = self.x * n
        y = self.y * n
        return Vector(x, y)

    def __neg__(self):
        return Vector(self.x * -1, self.y * -1)

Với class Vector như trên bạn có thể thực hiện các phép toán cơ bản như sau:

Ví dụ

>>> v1 = Vector(1, 2)
>>> v2 = Vector(3, 4)
>>> v1 + v2 # phép cộng
(4, 6)
>>> v1 - v2 # phép trừ
(-2, -2)
>>> v1 * 2 # phép nhân vô hướng
(2, 4)
>>> -v1 # nghịch đảo
(-1, -2)

Dưới đây là danh sách các hàm toán học cơ bản và magic method tương ứng của chúng

__add__(self, other)phép cộng +__sub__(self, other)phép trừ –__mul__(self, other)phép nhân *__floordiv__(self, other)phép chia lấy phần nguyên //__div__(self, other)phép chia /__mod__(self, other)phép chia lấy phần dư %__pow__(self, other[, modulo])phép tính lũy thừa **__lt__(self, other)phép so sánh nhỏ hơn <__le__(self, other)phép so sánh nhỏ hơn hoặc bằng <=__eq__(self, other)phép so sánh bằng ==__ne__(self, other)phép so sánh khác !=__ge__(self, other)phép so sánh lớn hoặc bằng >=Kết luận

Trong bài học này chúng ta đã làm quen với magic method và ứng dụng của chúng trong nạp chồng toán tử:

  • Các toán tử trong Python đều tương ứng với một magic method.
  • Python tự động gọi magic method khi gặp phép toán tương ứng.
  • Phép toán là cú pháp khác để sử dụng magic method. Có thể trực tiếp gọi magic method nếu muốn.
  • Để nạp chồng phép toán cho class mới cần ghi đè hoặc định nghĩa magic method tương ứng trong class.

Bài viết này đã giúp ích cho bạn?

Advertisements