Nạp Chồng Toán Tử Trong Python: Xử Lý Phép Toán Một Ngôi, Số Học, So Sánh Và Gán Mở Rộng

Python cho phép tùy chỉnh hành vi của các toán tử tích hợp sẵn thông qua các phương thức đặc biệt (magic methods). Bài viết này sẽ đi sâu vào cách triển khai các toán tử một ngôi, phép cộng có xử lý độ dài không đồng nhất, cơ chế fallback của phép so sánh, và phép gán mở rộng.

Toán tử một ngôi

Khi thực hiện các phép toán như -obj, +obj, hay ~obj, Python sẽ lần lượt gọi các phương thức __neg__, __pos__, và __invert__. Dưới đây là cách triển khai chúng trong một lớp tùy chỉnh MathArray:

class MathArray:
    def __init__(self, items):
        self.items = list(items)

    def __neg__(self):
        return MathArray([-v for v in self.items])

    def __pos__(self):
        return MathArray([abs(v) for v in self.items])

    def __invert__(self):
        return MathArray([~v for v in self.items])

    def __repr__(self):
        return f"MathArray({self.items})"

if __name__ == "__main__":
    arr = MathArray([-5, 3, -2])
    print(-arr)    # Gọi __neg__
    print(+arr)    # Gọi __pos__
    print(~arr)    # Gọi __invert__

Kết quả thực thi:

MathArray([5, -3, 2])
MathArray([5, 3, 2])
MathArray([4, -4, 1])

Phép cộng và xử lý mảng không đồng nhất

Để thực hiện phép cộng hai mảng, cách đơn giản nhất là dùng hàm zip. Tuy nhiên, zip sẽ dừng lại ở độ dài của mảng ngắn hơn, dẫn đến việc mất dữ liệu nếu hai mảng có độ dài khác nhau. Để tự động điền giá trị mặc định (như 0) cho các vị trí thiếu, chúng ta sử dụng itertools.zip_longest.

import itertools

class MathArray:
    def __init__(self, items):
        self.items = list(items)

    def __add__(self, other):
        target = other.items if isinstance(other, MathArray) else other
        paired = itertools.zip_longest(self.items, target, fillvalue=0)
        return MathArray([a + b for a, b in paired])

    def __repr__(self):
        return f"MathArray({self.items})"

if __name__ == "__main__":
    m1 = MathArray([1, 2, 3])
    m2 = MathArray([10, 20, 30, 40])
    print(m1 + m2)

Kết quả nhận được sẽ là MathArray([11, 22, 33, 40]), trong đó phần tử 40 được cộng với giá trị điền 0.

Phép cộng đảo ngược (__radd__)

Trong trường hợp biểu thức có dạng [10, 20] + MathArray([1, 2]), đối tượng danh sách (list) ở bên trái không biết cách cộng với MathArray, dẫn đến lỗi TypeError. Khi đó, Python sẽ tìm kiếm phương thức __radd__ ở toán hạng bên phải. Cơ chế này hoạt động như sau: nếu a.__add__(b) trả về NotImplemented, Python sẽ gọi b.__radd__(a).

class MathArray:
    # ... (các phương thức trước đó)

    def __radd__(self, other):
        # Ủy quyền ngược lại cho __add__
        return self + other

if __name__ == "__main__":
    m1 = MathArray([1, 2, 3])
    print([10, 20, 30, 40] + m1)  # Hoạt động bình thường

So sánh bằng và cơ chế NotImplemented

Khi triển khai __eq__, việc cẩn thận với các kiểu dữ liệu khác nhau là rất quan trọng. Nếu so sánh trực tiếp một đối tượng tùy chỉnh với một tuple hoặc list mà không kiểm tra kiểu, kết quả có thể không chính xác so với logic chuẩn của Python (vì [1,2] == (1,2) trả về False). Do đó, nếu kiểu dữ liệu không được hỗ trợ, phương thức nên trả về NotImplemented thay vì False, cho phép Python tiếp tục kiểm tra phương thức __eq__ của toán hạng bên kia.

class MathArray:
    # ... (các phương thức trước đó)

    def __eq__(self, other):
        if not isinstance(other, MathArray):
            return NotImplemented
        paired = itertools.zip_longest(self.items, other.items, fillvalue=None)
        return len(self.items) == len(other.items) and all(a == b for a, b in paired)

Khi MathArray.__eq__ trả về NotImplemented (ví dụ khi so sánh với tuple), Python sẽ gọi tuple.__eq__. Do tuple cũng không biết cách so sánh với MathArray, nó cũng trả về NotImplemented. Khi cả hai bên đều thất bại, Python mới rơi vào phương án cuối cùng là so sánh định danh đối tượng (object ID) và trả về False.

Đồng thời, ta không cần phải định nghĩa __ne__ thủ công. Nếu __eq__ được định nghĩa, Python sẽ tự động lấy phủ định của nó để sử dụng cho toán tử !=.

Phép gán mở rộng (__iadd__)

Mặc định, toán tử += sẽ gọi phương thức __add__, tạo ra một đối tượng mới và gán lại biến cho đối tượng mới đó. Điều này làm thay đổi định danh (ID) của đối tượng. Để thực hiện phép cộng tại chỗ (in-place) và giữ nguyên định danh, ta cần triển khai phương thức __iadd__.

class MathArray:
    # ... (các phương thức trước đó)

    def __iadd__(self, other):
        target = other.items if isinstance(other, MathArray) else other
        paired = itertools.zip_longest(self.items, target, fillvalue=0)
        # Cập nhật trực tiếp dữ liệu nội bộ
        self.items = [a + b for a, b in paired]
        return self

if __name__ == "__main__":
    m1 = MathArray([1.0, 2.0, 3.0])
    print(f"ID ban đầu: {id(m1)}")
    m1 += MathArray([10, 20, 30])
    print(f"Dữ liệu: {m1}")
    print(f"ID sau khi +=: {id(m1)}")

Kết quả cho thấy ID của m1 không thay đổi, chứng tỏ phép toán đã được thực hiện trực tiếp trên chính đối tượng đó thay vì tạo ra một bản sao mới.

Thẻ: python operator-overloading magic-methods itertools

Đăng vào ngày 3 tháng 10 lúc 05:48