Danh sách bài viết

Bài 12: Tuple — list bất biến và unpacking

Tuple là phiên bản bất biến (immutable) của list. Bài này học cú pháp khai báo (kể cả singleton với dấu phẩy), indexing / slicing, vì sao immutable lại hữu ích, tuple unpacking và *rest, trả nhiều giá trị từ function, namedtuple cơ bản, và khi nào nên chọn tuple thay vì list.

24/05/2026
10 phút đọc
0 lượt xem
1

Mục tiêu bài học

Sau bài này bạn sẽ:

  • Khai báo tuple đúng cú pháp, kể cả tuple 1 phần tử ((1,)).
  • Đọc tuple bằng indexing và slicing như list.
  • Giải thích được vì sao immutable lại quan trọng (hashable, dict key, signal).
  • Dùng tuple unpacking, swap biến không cần biến tạm.
  • Dùng *rest để bắt phần dư.
  • Trả nhiều giá trị từ function qua tuple.
  • Biết khi nào dùng tuple, khi nào dùng list.

Bài chạy trên Python 3.8 trở lên.

2

Tuple là gì?

Tuple là một collection có thứ tự (ordered) các phần tử, có 3 đặc điểm chính:

  • Ordered — các phần tử có vị trí cố định, truy cập bằng index như list.
  • Immutable — sau khi tạo, không thể thêm / xoá / sửa phần tử nào.
  • Allow duplicates — cho phép phần tử trùng giá trị.
point = (3, 4)              # tuple 2 phần tử
rgb = (255, 128, 0)         # tuple 3 phần tử
mixed = (1, "a", 3.14, True)  # khác kiểu cũng được
dup = (1, 1, 2, 2, 3)       # cho phép trùng

print(type(point))   # <class 'tuple'>
print(len(rgb))      # 3

Khác biệt chính so với list: list dùng [ ]thay đổi được, tuple dùng ( )không thay đổi được:

lst = [1, 2, 3]
lst[0] = 99           # OK — list mutable
print(lst)            # [99, 2, 3]

tup = (1, 2, 3)
# tup[0] = 99         # TypeError: 'tuple' object does not support item assignment
3

Cú pháp khai báo và singleton

Có 3 cách tạo tuple:

# 1) Dấu ngoặc đơn (thường dùng nhất)
a = (1, 2, 3)

# 2) Bỏ luôn ngoặc — vẫn là tuple (gọi là tuple packing)
b = 1, 2, 3
print(type(b))   # <class 'tuple'>

# 3) Từ iterable bằng hàm tuple()
c = tuple([10, 20, 30])
print(c)         # (10, 20, 30)

# Tuple rỗng — chỉ một cách duy nhất
empty = ()
print(len(empty))   # 0

Singleton — bẫy hay gặp: tuple 1 phần tử bắt buộc phải có dấu phẩy cuối. Không có dấu phẩy thì đó chỉ là biểu thức trong ngoặc, không phải tuple:

single = (1,)
print(type(single))   # <class 'tuple'>
print(len(single))    # 1

not_tuple = (1)
print(type(not_tuple))   # <class 'int'> — chỉ là số 1 trong ngoặc

Điều quyết định nó là tuple chính là dấu phẩy, không phải dấu ngoặc. Câu lệnh sau cũng tạo tuple:

x = 1,
print(type(x))   # <class 'tuple'>
print(x)         # (1,)
4

Indexing và slicing

Đọc tuple giống hệt list — chỉ khác là không ghi được:

colors = ("red", "green", "blue", "yellow", "purple")

print(colors[0])     # red          — index dương
print(colors[-1])    # purple       — index âm tính từ cuối
print(colors[1:3])   # ('green', 'blue')   — slicing trả về tuple mới
print(colors[::2])   # ('red', 'blue', 'purple')   — step 2

# Nối / lặp tuple sinh ra tuple mới (không sửa cái cũ)
a = (1, 2) + (3, 4)
print(a)            # (1, 2, 3, 4)

b = (0,) * 5
print(b)            # (0, 0, 0, 0, 0)

# Kiểm tra phần tử
print("red" in colors)   # True

Slicing tuple luôn trả về một tuple mới, không phải view; không có cú pháp gán slice như colors[0:2] = (...) — sẽ báo lỗi.

5

Immutability — vì sao hữu ích

Tại sao Python cần thêm tuple khi đã có list? Vì immutable mang lại 4 lợi ích cụ thể:

1. Hashable — dùng được làm dict key hoặc phần tử của set. List không hash được vì có thể thay đổi.

# Toạ độ làm key — hợp lệ
grid = {(0, 0): "start", (1, 2): "treasure", (3, 3): "exit"}
print(grid[(1, 2)])   # treasure

# Set chứa các điểm
points = {(0, 0), (1, 1), (0, 0)}
print(points)         # {(0, 0), (1, 1)} — set tự loại trùng

# Thử với list → lỗi
# bad = {[0, 0]: "start"}   # TypeError: unhashable type: 'list'

2. Signal "không được sửa". Khi bạn trả về tuple, người đọc code hiểu ngầm là dữ liệu cố định — record, toạ độ, kích thước. Trả list thì người ta có thể vô tình .append() vào.

3. An toàn hơn khi share giữa các phần code (kể cả thread). Không ai sửa được thì không sợ race condition trên chính object đó.

4. Hơi nhanh hơn list khi cùng dữ liệu — interpreter có thể tối ưu một số phép truy cập. Khác biệt nhỏ, không phải lý do chính.

Lưu ý: immutable nghĩa là tuple không sửa được, chứ không có nghĩa các phần tử bên trong không sửa được. Nếu phần tử là list thì list đó vẫn mutable:

t = (1, 2, [3, 4])
# t[2] = [9, 9]     # TypeError — không gán lại slot được
t[2].append(5)      # OK — sửa list bên trong
print(t)            # (1, 2, [3, 4, 5])

# Hệ quả: tuple chứa list KHÔNG còn hashable
# hash(t)           # TypeError: unhashable type: 'list'
6

Tuple unpacking và swap

Có thể gán nhiều biến cùng lúc bằng cách unpack tuple:

point = (3, 4)
x, y = point
print(x, y)   # 3 4

# Khỏi cần biến trung gian
a, b, c = (10, 20, 30)
print(a, b, c)   # 10 20 30

Số biến bên trái phải đúng bằng số phần tử bên phải, nếu không sẽ báo lỗi:

# a, b = (1, 2, 3)
# ValueError: too many values to unpack (expected 2)

Swap 2 biến không cần biến tạm — pattern kinh điển của Python:

a = 1
b = 2
a, b = b, a       # Python tạo tuple (b, a) rồi unpack ngược lại
print(a, b)       # 2 1

Hoạt động được vì bên phải b, a tạo ra tuple (2, 1) trước, rồi unpack vào a, b. So với cách của các ngôn ngữ khác (tmp = a; a = b; b = tmp), bản Python ngắn và rõ ý hơn.

Unpacking dùng được với mọi iterable, không chỉ tuple:

x, y, z = [10, 20, 30]      # unpack từ list
a, b = "ab"                  # unpack từ string
print(a, b)                  # a b
7

*rest unpacking

Khi không biết trước số phần tử, dùng * trước một biến để nó nuốt phần dư. Biến đó luôn nhận một list (không phải tuple):

first, *rest = [1, 2, 3, 4, 5]
print(first)   # 1
print(rest)    # [2, 3, 4, 5]    — list

*head, last = [1, 2, 3, 4, 5]
print(head)    # [1, 2, 3, 4]
print(last)    # 5

* có thể đặt ở giữa:

first, *middle, last = [1, 2, 3, 4, 5]
print(first)    # 1
print(middle)   # [2, 3, 4]
print(last)     # 5

Chỉ được đặt * cho một biến trong mỗi câu unpack. Nếu phần dư rỗng thì biến đó là list rỗng:

first, *rest = [42]
print(first)   # 42
print(rest)    # []

Pattern này hay dùng khi bạn chỉ quan tâm phần đầu hoặc phần cuối của một sequence dài — ví dụ tách header / row trong dữ liệu CSV.

8

Multiple return value

Python không có cú pháp riêng cho "trả nhiều giá trị". Khi bạn viết return a, b, Python ngầm gói lại thành tuple (a, b). Phía gọi unpack ra là xong:

def min_max(numbers):
    return min(numbers), max(numbers)   # ngầm trả tuple (min, max)

lo, hi = min_max([3, 1, 4, 1, 5, 9, 2, 6])
print(lo, hi)    # 1 9

# Hoặc nhận nguyên tuple
result = min_max([3, 1, 4, 1, 5, 9, 2, 6])
print(result)            # (1, 9)
print(type(result))      # <class 'tuple'>

Một ví dụ khác — hàm tính mean và std cùng lúc:

def mean_std(numbers):
    mean = sum(numbers) / len(numbers)
    variance = sum((x - mean) ** 2 for x in numbers) / len(numbers)
    std = variance ** 0.5
    return mean, std

mu, sigma = mean_std([1, 2, 3, 4, 5])
print(mu, sigma)   # 3.0 1.4142135623730951

Pattern này có ở khắp nơi trong ML: X_train, X_test, y_train, y_test = train_test_split(...) trong scikit-learn, hay output, hidden = rnn(input) trong PyTorch — đều là tuple trả về rồi unpack ngay.

Khi không cần một giá trị, gán cho _ để báo cho người đọc:

mu, _ = mean_std([1, 2, 3, 4, 5])   # chỉ lấy mean, bỏ std
print(mu)   # 3.0
9

Tuple methods: count, index

Vì không sửa được, tuple chỉ có 2 method — đều thuộc loại "đọc":

t = (1, 2, 3, 2, 4, 2, 5)

print(t.count(2))   # 3   — số lần 2 xuất hiện
print(t.index(3))   # 2   — index đầu tiên có giá trị 3
print(t.index(2))   # 1   — chỉ trả về vị trí ĐẦU TIÊN

# t.index(99)   # ValueError: tuple.index(x): x not in tuple

Ngoài 2 method này, các thao tác phổ biến khác đều dùng built-in: len(t), min(t), max(t), sum(t), sorted(t) (trả về list), x in t.

Lưu ý sorted(t) trả về list, không phải tuple. Muốn tuple thì bọc lại:

t = (3, 1, 4, 1, 5, 9, 2, 6)
sorted_tup = tuple(sorted(t))
print(sorted_tup)   # (1, 1, 2, 3, 4, 5, 6, 9)
10

namedtuple — tuple có tên field

Khi tuple có nhiều phần tử, việc truy cập bằng index dễ gây nhầm (person[2] là gì?). namedtuple trong module collections cho phép đặt tên từng field — vẫn là tuple (immutable, hashable) nhưng đọc rõ hơn:

from collections import namedtuple

# Tạo "kiểu" namedtuple: tên kiểu là Point, fields là 'x', 'y'
Point = namedtuple("Point", ["x", "y"])

p = Point(3, 4)
print(p.x, p.y)     # 3 4    — truy cập bằng tên
print(p[0], p[1])   # 3 4    — vẫn truy cập bằng index như tuple thường
print(len(p))       # 2

# Vẫn là tuple — vẫn immutable, vẫn unpack được
x, y = p
print(x, y)         # 3 4

# p.x = 99   # AttributeError: can't set attribute

Ứng dụng phổ biến: thay vì tạo class chỉ để chứa data, dùng namedtuple cho ngắn. Ví dụ một record dataset:

Sample = namedtuple("Sample", ["features", "label"])

s = Sample(features=[1.2, 0.7, -0.3], label=1)
print(s.label)        # 1
print(s.features)     # [1.2, 0.7, -0.3]

Từ Python 3.7 còn có typing.NamedTuple (cú pháp class với type hints) và Python 3.7+ có @dataclass — hai lựa chọn mạnh hơn khi cần thêm method. Sẽ học sau. Lúc này chỉ cần biết namedtuple tồn tại và là tuple "có tên".

11

Tuple vs list — chọn cái nào?

Quy tắc đơn giản dựa theo ý nghĩa của dữ liệu:

  • Dùng tuple khi tập hợp là một record cố định — toạ độ (x, y), màu RGB (r, g, b), shape của tensor (batch, channel, height, width), kết quả nhiều giá trị từ function. Số phần tử biết trước, ý nghĩa từng phần tử cố định.
  • Dùng list khi tập hợp là một collection thuần, có thể dài ngắn khác nhau, có thể thêm / xoá — danh sách user, danh sách số cần xử lý, batch của dữ liệu đang gom.

Một mẹo: nếu các phần tử trong collection có vai trò khác nhau (toạ độ x khác toạ độ y), nó nên là tuple. Nếu các phần tử cùng vai trò (đều là user, đều là số đo), nó nên là list.

# Tuple — record có vai trò khác nhau
position = (10, 20)              # x, y
color = (255, 128, 0)            # r, g, b
shape = (32, 3, 224, 224)        # batch, channel, h, w

# List — các phần tử cùng vai trò
scores = [8.5, 7.0, 9.2, 6.5]
users = ["alice", "bob", "charlie"]
batch = [sample1, sample2, sample3]

Cũng có lúc tuple được dùng vì cần hashable (làm key của dict, phần tử của set) như đã thấy ở mục 5.

12

Bài tập

Bài 1: Swap bằng unpacking.

Cho 2 biến a = "left"b = "right". Swap giá trị 2 biến chỉ bằng 1 dòng tuple unpacking, in ra để kiểm chứng. Sau đó thử swap 3 biến cùng lúc: a, b, c = c, a, b với giá trị ban đầu a=1, b=2, c=3 — dự đoán kết quả trước khi chạy.

Bài 2: Function trả nhiều giá trị.

Viết hàm describe(numbers) nhận một list số, trả về tuple gồm 4 giá trị: (min, max, mean, count). Yêu cầu:

  • Có docstring và type hints.
  • Test với [3, 1, 4, 1, 5, 9, 2, 6].
  • Unpack kết quả: lo, hi, avg, n = describe(...) và in từng giá trị.
  • Bonus: in lại kết quả bằng cách bỏ qua count: lo, hi, avg, _ = describe(...).

Bài 3: Toạ độ làm dict key.

Cho một lưới 3x3, một số ô có "treasure", còn lại không có. Hãy dùng dict với key là tuple (row, col):

grid = {
    (0, 0): "treasure",
    (1, 2): "treasure",
    (2, 1): "treasure",
}

Viết hàm has_treasure(grid, pos) trả về True nếu ô pos (là tuple) có treasure, ngược lại False. Test với (0, 0)(1, 1).

Bài 4 (mở rộng): namedtuple.

Tạo namedtuple RGB với 3 field r, g, b. Viết hàm to_hex(color) nhận một RGB, trả về chuỗi hex dạng "#RRGGBB" (ví dụ RGB(255, 128, 0)"#FF8000"). Gợi ý: dùng f-string f"#{color.r:02X}{color.g:02X}{color.b:02X}".

13

Tóm tắt

  • Tuple là collection ordered, immutable, cho phép trùng — viết bằng ( ) hoặc bỏ luôn ngoặc.
  • Singleton phải có dấu phẩy: (1,); (1) chỉ là số 1.
  • Indexing / slicing giống list, nhưng không gán được.
  • Immutable đem lại hashable (dict key, set element), signal "không sửa", an toàn khi share.
  • Tuple unpacking gán nhiều biến cùng lúc; swap viết gọn a, b = b, a.
  • *rest bắt phần dư thành list, đặt được ở đầu / giữa / cuối; mỗi câu chỉ một dấu *.
  • Function trả nhiều giá trị thực ra là trả một tuple — unpack ở phía gọi.
  • Tuple chỉ có 2 method: count, index; còn lại dùng built-in.
  • collections.namedtuple cho tuple có tên field, đọc rõ hơn mà vẫn immutable.
  • Chọn tuple cho record cố định (toạ độ, RGB, shape), list cho collection mutable.

Bài tiếp theo sẽ học dictionary — cấu trúc key-value, nền tảng cho mọi thao tác lookup nhanh.