Danh sách bài viết

Bài 11: List và các thao tác cơ bản

Học list trong Python: cách tạo, indexing, slicing, append/insert/extend, remove/pop/del, sort, reverse, copy và các built-in len/max/min/sum. Nền tảng để làm quen với NumPy array sau này.

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

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

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

  • Hiểu list là gì và đặc điểm: ordered, mutable, cho phép trùng lặp.
  • Tạo list bằng literal, list() constructor, từ string.
  • Dùng indexing dương và âm, slicing với start:stop:step.
  • Thêm / xóa / sửa phần tử bằng các method chuẩn.
  • Phân biệt list.sort() (in-place) với sorted() (trả list mới).
  • Copy list đúng cách để tránh chia sẻ tham chiếu.

Bài chạy trên Python 3.8 trở lên. List comprehension sẽ học riêng ở Bài 15.

2

List là gì?

List là kiểu dữ liệu chứa nhiều phần tử trong một biến duy nhất, viết trong cặp ngoặc vuông [ ] và phân tách bằng dấu phẩy.

Ba đặc điểm cần nhớ:

  • Ordered — phần tử có thứ tự cố định. Phần tử thêm vào cuối nằm ở cuối, vị trí của các phần tử khác giữ nguyên.
  • Mutable — sửa được tại chỗ: thêm, xóa, đổi giá trị mà không cần tạo list mới.
  • Allow duplicates — cho phép phần tử trùng nhau, ví dụ [1, 1, 2, 2] là list hợp lệ.

List có thể chứa các kiểu dữ liệu khác nhau trong cùng một list, kể cả list lồng nhau:

# List đồng nhất kiểu
scores = [8.5, 7.0, 9.5, 6.5]

# List hỗn hợp kiểu (hợp lệ trong Python)
mixed = [1, "hello", 3.14, True, None]

# List lồng list (matrix 2x3)
matrix = [
    [1, 2, 3],
    [4, 5, 6],
]

List là kiểu dữ liệu xuất hiện ở khắp nơi trong code AI: lưu danh sách feature, batch sample, lịch sử loss qua các epoch... Hiểu list cũng là bước đệm để hiểu ndarray của NumPy ở Module 4.

3

Cách tạo list

Ba cách phổ biến:

# 1. Literal — cách dùng nhiều nhất
fruits = ["apple", "banana", "cherry"]
empty = []   # list rỗng

# 2. Constructor list() — tạo từ một iterable bất kỳ
nums = list((1, 2, 3))        # từ tuple → [1, 2, 3]
chars = list("abc")           # từ string → ["a", "b", "c"]
zeros = list(range(5))        # từ range → [0, 1, 2, 3, 4]

# 3. Nhân bản nhanh
zeros_10 = [0] * 10           # [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

Lưu ý: list() không nhận dấu phẩy như list(1, 2, 3) — đó là lỗi TypeError. Phải truyền duy nhất một iterable (tuple, string, range, generator...).

list(1, 2, 3)
# TypeError: list expected at most 1 argument, got 3

Mẹo [0] * 10 rất tiện cho list số nhưng không dùng cho list lồng nhau — sẽ tạo ra các tham chiếu cùng object, sửa một là sửa tất. Phần copy ở bước 13 sẽ làm rõ.

4

Indexing: positive và negative

Python đánh số phần tử bắt đầu từ 0. Đồng thời cho phép dùng index âm để đếm từ cuối: -1 là phần tử cuối cùng, -2 là phần tử áp cuối, v.v.

fruits = ["apple", "banana", "cherry", "date"]
#          0         1         2          3
#         -4        -3         -2         -1

print(fruits[0])    # apple   — phần tử đầu
print(fruits[2])    # cherry  — phần tử thứ 3
print(fruits[-1])   # date    — phần tử cuối
print(fruits[-2])   # cherry  — áp cuối

Vì list là mutable, có thể gán giá trị mới qua index:

fruits[1] = "blueberry"
print(fruits)   # ["apple", "blueberry", "cherry", "date"]

Truy cập index vượt phạm vi → IndexError:

print(fruits[10])
# IndexError: list index out of range
5

Slicing

Cú pháp lst[start:stop:step] trả về list mới chứa các phần tử từ start đến trước stop, bước nhảy step.

nums = [10, 20, 30, 40, 50, 60]

print(nums[1:4])      # [20, 30, 40]   — index 1, 2, 3 (4 bị loại)
print(nums[:3])       # [10, 20, 30]   — từ đầu đến index 2
print(nums[3:])       # [40, 50, 60]   — từ index 3 đến cuối
print(nums[:])        # [10, 20, 30, 40, 50, 60] — bản sao toàn list
print(nums[::2])      # [10, 30, 50]   — bước 2
print(nums[::-1])     # [60, 50, 40, 30, 20, 10] — đảo ngược
print(nums[-3:])      # [40, 50, 60]   — 3 phần tử cuối

Hai điểm cần nhớ:

  • stopexclusive — không bao gồm index stop.
  • Slicing luôn tạo list mới, không sửa list gốc. Đây là cách phổ biến để copy list.

Slice cũng dùng được ở vế trái để gán nhiều phần tử cùng lúc:

nums = [10, 20, 30, 40, 50]
nums[1:4] = [99, 99]   # thay 3 phần tử giữa bằng 2 phần tử
print(nums)            # [10, 99, 99, 50]
6

Thêm phần tử: append, insert, extend

Ba method thường dùng để thêm phần tử vào list — đều sửa tại chỗ:

lst = [1, 2, 3]

# append: thêm 1 phần tử vào cuối
lst.append(4)
print(lst)            # [1, 2, 3, 4]

# insert(index, value): chèn tại vị trí index
lst.insert(0, 0)
print(lst)            # [0, 1, 2, 3, 4]

# extend: nối thêm một iterable vào cuối
lst.extend([5, 6])
print(lst)            # [0, 1, 2, 3, 4, 5, 6]

Phân biệt appendextend — đây là lỗi rất hay gặp:

a = [1, 2, 3]
a.append([4, 5])      # thêm cả list [4, 5] như MỘT phần tử
print(a)              # [1, 2, 3, [4, 5]]

b = [1, 2, 3]
b.extend([4, 5])      # thêm TỪNG phần tử của [4, 5]
print(b)              # [1, 2, 3, 4, 5]

Toán tử + giữa hai list tạo list mới, không sửa tại chỗ — khác với extend:

c = [1, 2]
d = c + [3, 4]        # d là list mới, c không đổi
print(c)              # [1, 2]
print(d)              # [1, 2, 3, 4]
7

Xóa phần tử: remove, pop, del, clear

Bốn cách xóa, dùng cho các tình huống khác nhau:

lst = ["a", "b", "c", "b", "d"]

# 1. remove(value): xóa phần tử ĐẦU TIÊN có giá trị == value
lst.remove("b")
print(lst)            # ["a", "c", "b", "d"]

# 2. pop(index): xóa và TRẢ VỀ phần tử tại index
#    Mặc định index = -1 (phần tử cuối)
last = lst.pop()
print(last)           # "d"
print(lst)            # ["a", "c", "b"]

first = lst.pop(0)
print(first)          # "a"

# 3. del lst[index] hoặc del lst[slice]: xóa theo index/slice
nums = [10, 20, 30, 40, 50]
del nums[0]
print(nums)           # [20, 30, 40, 50]
del nums[1:3]
print(nums)           # [20, 50]

# 4. clear(): xóa tất cả phần tử, giữ lại list rỗng
nums.clear()
print(nums)           # []

Sự khác biệt then chốt:

  • remove tìm theo giá trị, chỉ xóa phần tử đầu tiên trùng — không tìm thấy → ValueError.
  • pop xóa theo indextrả về phần tử bị xóa — tiện khi cần dùng giá trị đó.
  • del là statement, không trả gì; có thể dùng cho slice.
  • clear xóa rỗng nhưng không hủy biến — khác với del lst (xóa biến luôn).
8

Tìm và đếm: index, count, in

lst = ["a", "b", "c", "b", "d"]

# in / not in: kiểm tra phần tử có trong list không
print("b" in lst)       # True
print("z" not in lst)   # True

# index(value): trả về index ĐẦU TIÊN của value
print(lst.index("b"))   # 1

# count(value): đếm số lần xuất hiện
print(lst.count("b"))   # 2
print(lst.count("z"))   # 0

index() không tìm thấy sẽ ném ValueError — nên kiểm tra bằng in trước nếu không chắc:

if "z" in lst:
    pos = lst.index("z")
else:
    pos = -1   # quy ước "không tìm thấy"
9

Sort: sort() vs sorted()

Hai cách sort, dùng cho mục đích khác nhau:

nums = [3, 1, 4, 1, 5, 9, 2, 6]

# 1. lst.sort() — sửa TẠI CHỖ, trả về None
nums.sort()
print(nums)           # [1, 1, 2, 3, 4, 5, 6, 9]

# 2. sorted(lst) — trả về LIST MỚI, list gốc không đổi
original = [3, 1, 4, 1, 5]
asc = sorted(original)
print(asc)            # [1, 1, 3, 4, 5]
print(original)       # [3, 1, 4, 1, 5] — không đổi

Cả hai đều nhận hai tham số tùy chọn quan trọng: reversekey.

# reverse=True: sắp xếp giảm dần
desc = sorted([3, 1, 4, 1, 5], reverse=True)
print(desc)           # [5, 4, 3, 1, 1]

# key=func: sort theo kết quả của func áp lên từng phần tử
words = ["banana", "fig", "cherry", "kiwi"]
by_len = sorted(words, key=len)
print(by_len)         # ["fig", "kiwi", "banana", "cherry"]

Lỗi thường gặp: viết nums = nums.sort() rồi tưởng nums là list đã sort. sort() trả về None nên dòng đó gán None vào nums. Đúng phải là một trong hai:

nums.sort()                 # in-place
# hoặc
nums = sorted(nums)         # gán list mới
10

Reverse: reverse() vs reversed()

Tương tự cặp sort/sorted:

lst = [1, 2, 3, 4]

# lst.reverse(): đảo TẠI CHỖ, trả None
lst.reverse()
print(lst)            # [4, 3, 2, 1]

# reversed(lst): trả về iterator, list gốc không đổi
original = [1, 2, 3, 4]
rev = list(reversed(original))
print(rev)            # [4, 3, 2, 1]
print(original)       # [1, 2, 3, 4]

reversed() trả về iterator, không phải list — phải bọc qua list() nếu muốn in ra hoặc index. Cách thứ ba dùng slice lst[::-1] đã thấy ở bước 5, cũng tạo list mới.

11

Built-in: len, max, min, sum

scores = [8.5, 7.0, 9.5, 6.5, 8.0]

print(len(scores))    # 5      — số phần tử
print(max(scores))    # 9.5    — phần tử lớn nhất
print(min(scores))    # 6.5    — phần tử nhỏ nhất
print(sum(scores))    # 39.5   — tổng

# Trung bình cộng
avg = sum(scores) / len(scores)
print(avg)            # 7.9

Lưu ý:

  • sum() chỉ chạy với list số. List string → TypeError.
  • max() / min() cũng so sánh string theo thứ tự từ điển: max(["banana", "apple"])"banana".
  • List rỗng + max/minValueError; sum([]) trả 0.
12

Nested list (preview 2D)

List có thể chứa list, tạo ra cấu trúc 2 chiều (ma trận). Đây là tiền đề của ndarray trong NumPy.

matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
]

print(matrix[0])        # [1, 2, 3]   — hàng đầu
print(matrix[1][2])     # 6           — hàng 1, cột 2
print(len(matrix))      # 3           — số hàng
print(len(matrix[0]))   # 3           — số cột (hàng đầu)

Nested list của Python chạy được nhưng chậm cho phép tính số học so với NumPy: Python phải duyệt từng phần tử trong vòng lặp, còn NumPy gọi xuống C vector hóa. Vì vậy ở Module 4 sẽ chuyển sang dùng numpy.ndarray khi xử lý dữ liệu số nhiều chiều.

13

Copy list — tránh shared reference

Gán b = a không tạo list mới — b chỉ là một tên khác cùng trỏ vào một list trong bộ nhớ. Sửa qua b cũng đồng nghĩa sửa a:

a = [1, 2, 3]
b = a               # b và a cùng trỏ vào MỘT list
b.append(99)
print(a)            # [1, 2, 3, 99]   — a cũng đổi!
print(a is b)       # True            — cùng object

Muốn có bản sao độc lập, dùng một trong các cách sau (shallow copy):

a = [1, 2, 3]

b1 = a.copy()       # method copy
b2 = a[:]           # slice toàn bộ
b3 = list(a)        # constructor

b1.append(99)
print(a)            # [1, 2, 3]   — a không đổi
print(b1)           # [1, 2, 3, 99]

Cảnh báo về shallow copy: ba cách trên chỉ sao chép tầng ngoài. Với nested list, các list bên trong vẫn dùng chung tham chiếu:

a = [[1, 2], [3, 4]]
b = a.copy()
b[0].append(99)
print(a)            # [[1, 2, 99], [3, 4]]   — a vẫn bị ảnh hưởng

Khi cần copy sâu (deep copy) cho cấu trúc lồng nhau, dùng copy.deepcopy() từ standard library:

import copy
a = [[1, 2], [3, 4]]
b = copy.deepcopy(a)
b[0].append(99)
print(a)            # [[1, 2], [3, 4]]   — a được bảo toàn

Quay lại mẹo [[0] * 3] * 4: tạo list lồng kiểu này sai vì 4 hàng đều tham chiếu cùng một list con. Cách đúng để tạo ma trận 4x3 toàn 0 dùng vòng lặp (hoặc numpy.zeros ở Module 4):

# SAI — 4 hàng cùng tham chiếu
m = [[0] * 3] * 4
m[0][0] = 1
print(m)            # [[1, 0, 0], [1, 0, 0], [1, 0, 0], [1, 0, 0]]

# ĐÚNG — mỗi hàng là list mới
m = []
for _ in range(4):
    m.append([0] * 3)
m[0][0] = 1
print(m)            # [[1, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]
14

Bài tập

Bài 1: Lọc số chẵn.

Cho list nums = [3, 8, 1, 14, 7, 22, 5, 16]. Viết đoạn code tạo list mới evens chứa các số chẵn trong nums (giữ nguyên thứ tự). Yêu cầu dùng vòng for + append, chưa dùng list comprehension. Kết quả mong đợi: [8, 14, 22, 16].

Bài 2: Tìm max và min không dùng built-in.

Viết hàm find_min_max(numbers) nhận một list số, trả về tuple (min_val, max_val)không dùng min() / max(). Gợi ý: khởi tạo hai biến bằng phần tử đầu, duyệt từ phần tử thứ hai và cập nhật. Test:

print(find_min_max([3, 8, 1, 14, 7, 22, 5, 16]))
# (1, 22)
print(find_min_max([-5, -2, -10, -1]))
# (-10, -1)

Bài 3 (mở rộng): Loại bỏ phần tử trùng nhưng giữ thứ tự.

Cho items = ["a", "b", "a", "c", "b", "d"]. Viết đoạn code trả về list ["a", "b", "c", "d"] — chỉ giữ lần xuất hiện đầu tiên của mỗi phần tử. Gợi ý: duyệt từng phần tử, dùng toán tử in để kiểm tra trước khi append.

15

Tóm tắt

  • List là kiểu dữ liệu ordered, mutable, allow duplicates; có thể chứa kiểu hỗn hợp và lồng nhau.
  • Tạo bằng literal [...], list(iterable), hoặc nhân bản [0] * n.
  • Indexing từ 0; index âm đếm từ cuối (-1 là phần tử cuối).
  • Slicing lst[start:stop:step] tạo list mới; stop là exclusive.
  • Thêm: append (1 phần tử), insert (theo index), extend (nối iterable).
  • Xóa: remove (theo giá trị), pop (theo index, trả về), del (statement), clear (làm rỗng).
  • Tìm: in, index, count.
  • Sort: lst.sort() in-place vs sorted(lst) trả mới; tham số reverse, key.
  • Reverse: lst.reverse() in-place vs reversed(lst) trả iterator; slice lst[::-1] trả list mới.
  • Built-in cho list số: len, max, min, sum.
  • Gán b = a chỉ chia sẻ tham chiếu — muốn bản sao dùng a.copy(), a[:], hoặc list(a); nested list cần copy.deepcopy.

Bài tiếp theo sẽ học tuple — phiên bản bất biến của list, hữu ích cho dữ liệu cố định và unpacking nhiều biến cùng lúc.