Mục lục
- Mục tiêu bài học
- List là gì?
- Cách tạo list
- Indexing: positive và negative
- Slicing
- Thêm phần tử: append, insert, extend
- Xóa phần tử: remove, pop, del, clear
- Tìm và đếm: index, count, in
- Sort: sort() vs sorted()
- Reverse: reverse() vs reversed()
- Built-in: len, max, min, sum
- Nested list (preview 2D)
- Copy list — tránh shared reference
- Bài tập
- Tóm tắt
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ớisorted()(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.
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.
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õ.
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
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ớ:
stoplà exclusive — không bao gồm indexstop.- 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]
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 append và extend — đâ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]
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:
removetìm theo giá trị, chỉ xóa phần tử đầu tiên trùng — không tìm thấy →ValueError.popxóa theo index và trả về phần tử bị xóa — tiện khi cần dùng giá trị đó.dellà statement, không trả gì; có thể dùng cho slice.clearxóa rỗng nhưng không hủy biến — khác vớidel lst(xóa biến luôn).
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"
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: reverse và key.
# 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
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.
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/min→ValueError;sum([])trả0.
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.
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]]
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) mà 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.
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 (
-1là phần tử cuối). - Slicing
lst[start:stop:step]tạo list mới;stoplà 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 vssorted(lst)trả mới; tham sốreverse,key. - Reverse:
lst.reverse()in-place vsreversed(lst)trả iterator; slicelst[::-1]trả list mới. - Built-in cho list số:
len,max,min,sum. - Gán
b = achỉ chia sẻ tham chiếu — muốn bản sao dùnga.copy(),a[:], hoặclist(a); nested list cầncopy.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.
