Giải mã bí ẩn phép toán +=

written by HVN on 2016-10-09

list và tuple - hai loài khác biệt

Biết list + list trả về 1 list, tuple + tuple trả về 1 tuple, vậy list + tuple thì trả về gì?

Câu trả lời đúng là: không trả về gì cả bởi có exception xảy ra. Dù cho lấy list + tuple hay tuple + list đi chăng nữa:

>>> [1] + (2,)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate list (not "tuple") to list
>>> (2,) + [1]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate tuple (not "list") to tuple

đây là TypeError exception, với nội dung rất rõ ràng: chỉ có thể cộng list với list, tuple với tuple.

a += b có đúng là a = a + b?

Thông thường, phép tính a += b được hiểu là a = a + b. Điều này đúng trong hầu hết các trường hợp... cho đến khi nó sai:

>>> a = 5
>>> b = 4
>>> a += b
>>> print(a)
9
>>> a = 'pymi'
>>> b = '.vn'
>>> a += b
>>> print(a)
pymi.vn
>>> a = [1]
>>> b = (2,)
>>> a += b
>>> print(a)
[1, 2]
>>> # Ồ ồ ồ ồ ồ 😱 vì sao list lại cộng được với tuple 🙄

Phép toán +=, -=, *=, /= được gọi là augmented assignment operators (phép toán "augmented assignment").

Augmented /ɔːɡˈmɛntɪd/

Vietsub: làm tăng lên. Engsub: Having been made greater in size or value.

Xem đầy đủ danh sách các phép toán augmented assignment tại trang định nghĩa cú pháp của Python.

Bản chất của các phép toán

Trong Python, bản chất các phép toán đều chỉ là syntactic sugar (làm cho cú pháp thêm ngọt - dễ nhìn). Phép cộng a + b, thực chất được thực hiện bằng cách gọi method __add__ của object a với argument là b.

>>> a = 5
>>> b = 4
>>> a.__add__(b)
9

Tại đây, method __add__ trả về một object mới là số 9.

Khi viết a = a + b, ta có

>>> a = 5
>>> id(a)
4309492224
>>> b = 4
>>> a = a.__add__(b)
>>> print(a, id(a))
9 4309492352 # id đã khác

Ở đây sẽ gán cho tên (biến) a kết quả của phép tính a+b - một object mới (số 9).

Phép toán += hoạt động thế nào?

Cách hoạt động của augmented assigment được mô tả tại PEP0203. Đoạn trích từ PEP0203:

 So, given an instance object `x', the expression

        x += y

    tries to call x.__iadd__(y), which is the `in-place' variant of
    __add__. If __iadd__ is not present, x.__add__(y) is attempted,
    and finally y.__radd__(x) if __add__ is missing too.

khi thực hiện phép toán +=, Python sẽ thử method __iadd__ trước. Nếu method này không tồn tại, Python lại thử __add__ rồi sau cùng thử gọi __radd__ . __iadd__ là phiên bản in-place của __add__, Khi mà __add__ sẽ trả về một object mới chứa kết quả của phép cộng, thì __iadd__ lại thay đổi giá trị của chính object gọi nó.

list += tuple

List có method __iadd__, nên khi dùng phép toán +=, python sẽ gọi __iadd__, method này thay đổi chính list object bằng việc gọi method extend. extend chấp nhận đầu vào là iterable (kiểu dữ liệu chứa nhiều giá trị như list, string, tuple ...) nên có thể viết list += tuple. Kết quả thu được là list ban đầu với giá trị mới được extend từ tuple đưa vào.

>>> lst = [1]
>>> id(lst)
4340949768
>>> lst += (2,)
>>> print(lst, id(lst))
[1, 2] 4340949768  # id không đổi

tuple += list

Với kiểu dữ liệu immutable (như tuple, integer, string ... ), chúng không có method __iadd__, nên phép toán += sẽ gọi method __add__. Viết a += b khi a là tuple, b là list sẽ tương đương với viết a = a + b, mà tuple + list thì không thu được gì:

>>> a = (2,)
>>> b = [1]
>>> a += b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate tuple (not "list") to tuple

Đây không phải là bug của Python, nó là một tính năng ™ theo thiết kế.

Bởi nếu cho phép thay đổi một kiểu dữ liệu immutable thì khi viết 4 + 5 bạn sẽ muốn nhận được 1 object mới là số 9, hay bạn muốn sửa số 4 thành số 9? 🤔

Kết luận

Phép toán += khiến cho một list có thể "cộng" được với một tuple đã không còn là điều bí ẩn một khi bức màn bí mật đã được tụt xuống. Hãy nhìn các phép toán theo một con mắt khác, chúng chỉ là ký hiệu và che giấu đi cơ chế hoạt động thực sự ở phía dưới. Đây là lập trình, không phải toán học.