Chương này mô tả chi tiết hơn một số khái niệm mà bạn đã học, đồng thời bổ sung thêm một số khái niệm mới.
5.1. Tìm hiểu thêm về Danh sách (Lists)
Kiểu dữ liệu danh sách có một số phương thức hữu ích. Dưới đây là các phương thức của đối tượng danh sách:
list.append(x)
: Thêm một phần tử vào cuối danh sách. Tương đương vớia[len(a):] = [x]
.list.extend(iterable)
: Mở rộng danh sách bằng cách thêm tất cả các phần tử từ iterable. Tương đương vớia[len(a):] = iterable
.list.insert(i, x)
: Chèn phần tửx
vào vị tríi
. Ví dụ,a.insert(0, x)
sẽ chèn vào đầu danh sách, vàa.insert(len(a), x)
tương đương vớia.append(x)
.list.remove(x)
: Xóa phần tử đầu tiên có giá trị bằngx
. Nếu không tìm thấy, sẽ báo lỗiValueError
.list.pop([i])
: Xóa phần tử tại vị tríi
trong danh sách và trả về nó. Nếu không chỉ địnhi
, mặc định sẽ xóa phần tử cuối cùng. Nếu danh sách rỗng hoặci
ngoài phạm vi danh sách, sẽ báo lỗiIndexError
.list.clear()
: Xóa toàn bộ các phần tử trong danh sách. Tương đương vớidel a[:]
.list.index(x[, start[, end]])
: Trả về chỉ số của phần tử đầu tiên có giá trị bằngx
. Nếu không tìm thấy, sẽ báo lỗiValueError
. Tham sốstart
vàend
có thể được sử dụng để giới hạn phạm vi tìm kiếm.list.count(x)
: Trả về số lầnx
xuất hiện trong danh sách.list.sort(*, key=None, reverse=False)
: Sắp xếp danh sách tại chỗ.list.reverse()
: Đảo ngược danh sách tại chỗ.list.copy()
: Trả về một bản sao nông (shallow copy) của danh sách. Tương đương vớia[:]
.
Ví dụ sử dụng các phương thức trên:
>>> fruits = ['orange', 'apple', 'pear', 'banana', 'kiwi', 'apple', 'banana']
>>> fruits.count('apple')
2
>>> fruits.count('tangerine')
0
>>> fruits.index('banana')
3
>>> fruits.index('banana', 4) # Tìm "banana" tiếp theo từ vị trí 4
6
>>> fruits.reverse()
>>> fruits
['banana', 'apple', 'kiwi', 'banana', 'pear', 'apple', 'orange']
>>> fruits.append('grape')
>>> fruits
['banana', 'apple', 'kiwi', 'banana', 'pear', 'apple', 'orange', 'grape']
>>> fruits.sort()
>>> fruits
['apple', 'apple', 'banana', 'banana', 'grape', 'kiwi', 'orange', 'pear']
>>> fruits.pop()
'pear'
Các phương thức như insert
, remove
, sort
không trả về giá trị mà chỉ thay đổi danh sách gốc.
5.1.1. Sử dụng Danh sách như Ngăn xếp (Stacks)
Danh sách có thể được sử dụng như ngăn xếp (stack), tuân theo nguyên tắc “vào sau, ra trước” (Last-In, First-Out). Phương thức append()
được sử dụng để thêm phần tử vào đỉnh ngăn xếp, còn pop()
được sử dụng để lấy phần tử từ đỉnh ngăn xếp.
Ví dụ:
>>> stack = [3, 4, 5]
>>> stack.append(6)
>>> stack.append(7)
>>> stack
[3, 4, 5, 6, 7]
>>> stack.pop()
7
>>> stack.pop()
6
>>> stack.pop()
5
>>> stack
[3, 4]
5.1.2. Sử dụng Danh sách như Hàng đợi (Queues)
Danh sách cũng có thể được sử dụng như hàng đợi (queue), tuân theo nguyên tắc “vào trước, ra trước” (First-In, First-Out). Tuy nhiên, sử dụng danh sách để làm hàng đợi không hiệu quả vì khi xóa phần tử đầu tiên, tất cả phần tử còn lại phải dịch chuyển.
Giải pháp hiệu quả hơn là sử dụng collections.deque
, hỗ trợ chèn và xóa nhanh ở cả hai đầu.
Ví dụ:
>>> from collections import deque
>>> queue = deque(["Eric", "John", "Michael"])
>>> queue.append("Terry")
>>> queue.append("Graham")
>>> queue.popleft()
'Eric'
>>> queue.popleft()
'John'
>>> queue
deque(['Michael', 'Terry', 'Graham'])
5.1.3. Hiểu về List Comprehensions
List comprehension cung cấp cách viết ngắn gọn để tạo danh sách mới bằng cách áp dụng một biểu thức lên mỗi phần tử trong một chuỗi hoặc lọc các phần tử thỏa mãn điều kiện.
Ví dụ, tạo danh sách bình phương của các số từ 0 đến 9:
>>> squares = [x**2 for x in range(10)]
>>> squares
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
Cách viết tương đương với vòng lặp:
>>> squares = []
>>> for x in range(10):
... squares.append(x**2)
List comprehension cũng có thể kết hợp với điều kiện if
:
>>> [x for x in range(10) if x % 2 == 0]
[0, 2, 4, 6, 8]
Ngoài ra, có thể lồng nhiều vòng lặp trong một list comprehension:
>>> [(x, y) for x in [1,2,3] for y in [3,1,4] if x != y]
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]
Nếu sử dụng tuple trong list comprehension, cần đặt trong dấu ngoặc đơn:
>>> [(x, x**2) for x in range(6)]
[(0, 0), (1, 1), (2, 4), (3, 9), (4, 16), (5, 25)]
Ngoài ra, có thể sử dụng list comprehension để làm sạch dữ liệu:
>>> freshfruit = [' banana', ' loganberry ', 'passion fruit ']
>>> [fruit.strip() for fruit in freshfruit]
['banana', 'loganberry', 'passion fruit']
5.1.4. Lồng ghép List Comprehensions (Nested List Comprehensions)
Biểu thức ban đầu trong một list comprehension có thể là bất kỳ biểu thức nào, bao gồm cả một list comprehension khác.
Xem xét ví dụ về một ma trận 3×4 được triển khai dưới dạng danh sách gồm 3 danh sách con, mỗi danh sách có độ dài 4:
>>> matrix = [
... [1, 2, 3, 4],
... [5, 6, 7, 8],
... [9, 10, 11, 12],
... ]
List comprehension sau đây sẽ hoán vị hàng và cột:
>>> [[row[i] for row in matrix] for i in range(4)]
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]
Như chúng ta đã thấy ở phần trước, list comprehension bên trong được đánh giá trong ngữ cảnh của vòng lặp for
phía sau nó. Do đó, ví dụ trên tương đương với:
>>> transposed = []
>>> for i in range(4):
... transposed.append([row[i] for row in matrix])
...
>>> transposed
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]
Điều này, đến lượt nó, cũng tương đương với:
>>> transposed = []
>>> for i in range(4):
... transposed_row = []
... for row in matrix:
... transposed_row.append(row[i])
... transposed.append(transposed_row)
...
>>> transposed
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]
Trong thực tế, bạn nên ưu tiên sử dụng các hàm tích hợp sẵn thay vì các câu lệnh vòng lặp phức tạp. Ví dụ, hàm zip()
có thể thực hiện nhiệm vụ này một cách hiệu quả:
>>> list(zip(*matrix))
[(1, 5, 9), (2, 6, 10), (3, 7, 11), (4, 8, 12)]
Xem Giải nén Danh sách Đối số (Unpacking Argument Lists) để biết thêm chi tiết về dấu *
trong dòng này.
5.2. Câu lệnh del
Có một cách để xóa một phần tử khỏi danh sách dựa trên chỉ số của nó thay vì giá trị: câu lệnh del
. Điều này khác với phương thức pop()
vì del
không trả về giá trị. Câu lệnh del
cũng có thể được sử dụng để xóa một phần danh sách hoặc xóa toàn bộ danh sách.
Ví dụ:
>>> a = [-1, 1, 66.25, 333, 333, 1234.5]
>>> del a[0]
>>> a
[1, 66.25, 333, 333, 1234.5]
>>> del a[2:4]
>>> a
[1, 66.25, 1234.5]
>>> del a[:]
>>> a
[]
Câu lệnh del
cũng có thể được sử dụng để xóa hoàn toàn một biến:
>>> del a
Việc tham chiếu đến biến a
sau đó sẽ gây ra lỗi (ít nhất là cho đến khi giá trị mới được gán cho nó). Chúng ta sẽ thấy thêm các ứng dụng của del
sau này.
5.3. Tuple và Dãy (Tuples and Sequences)
Chúng ta đã thấy rằng danh sách và chuỗi có nhiều đặc điểm chung, chẳng hạn như phép lập chỉ mục và cắt lát. Chúng đều là các loại dữ liệu chuỗi tuần tự (sequence data types). Một loại dữ liệu chuỗi chuẩn khác trong Python là tuple.
Tuple bao gồm một tập hợp các giá trị được phân tách bằng dấu phẩy:
>>> t = 12345, 54321, 'hello!'
>>> t[0]
12345
>>> t
(12345, 54321, 'hello!')
Tuple có thể được lồng vào nhau:
>>> u = t, (1, 2, 3, 4, 5)
>>> u
((12345, 54321, 'hello!'), (1, 2, 3, 4, 5))
Tuple là bất biến (immutable):
>>> t[0] = 88888
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
Tuy nhiên, tuple có thể chứa các đối tượng có thể thay đổi như danh sách:
>>> v = ([1, 2, 3], [3, 2, 1])
>>> v
([1, 2, 3], [3, 2, 1])
Trong đầu ra, các tuple luôn được bao quanh bởi dấu ngoặc đơn để đảm bảo rằng chúng được diễn giải chính xác. Khi nhập tuple, bạn có thể sử dụng hoặc không sử dụng dấu ngoặc đơn, nhưng trong nhiều trường hợp, dấu ngoặc đơn là bắt buộc (chẳng hạn nếu tuple là một phần của biểu thức lớn hơn).
Mặc dù tuple có vẻ giống danh sách, nhưng chúng thường được sử dụng trong các tình huống khác nhau:
- Tuple là bất biến, thường chứa một chuỗi không đồng nhất các phần tử, được truy cập thông qua unpacking hoặc lập chỉ mục.
- Danh sách là có thể thay đổi, thường chứa các phần tử đồng nhất và được truy cập bằng cách lặp qua danh sách.
Một vấn đề đặc biệt là cách tạo tuple chứa 0 hoặc 1 phần tử:
- Tuple rỗng được tạo bằng cặp dấu ngoặc đơn trống
()
. - Tuple chứa một phần tử được tạo bằng cách thêm dấu phẩy sau giá trị. Việc chỉ bao quanh giá trị bằng dấu ngoặc đơn không đủ để tạo tuple.
Ví dụ:
>>> empty = ()
>>> singleton = 'hello', # Lưu ý dấu phẩy phía sau
>>> len(empty)
0
>>> len(singleton)
1
>>> singleton
('hello',)
Câu lệnh t = 12345, 54321, 'hello!'
là một ví dụ về đóng gói tuple (tuple packing): các giá trị 12345
, 54321
và 'hello!'
được đóng gói vào một tuple. Ngược lại, chúng ta cũng có thể mở gói tuple (sequence unpacking):
>>> x, y, z = t
Điều này hoạt động với bất kỳ chuỗi tuần tự nào ở phía bên phải dấu =
. Yêu cầu duy nhất là số lượng biến bên trái phải khớp với số phần tử trong chuỗi. Việc gán nhiều biến như thế này thực chất là sự kết hợp của đóng gói tuple và mở gói tuple.
5.4. Tập hợp (Sets)
Python cũng bao gồm một kiểu dữ liệu cho tập hợp (sets). Một tập hợp là một tập hợp không có thứ tự và không chứa phần tử trùng lặp. Các tác vụ cơ bản bao gồm kiểm tra thành viên (membership testing) và loại bỏ các mục trùng lặp. Các đối tượng tập hợp cũng hỗ trợ các phép toán tập hợp như hợp (union), giao (intersection), hiệu (difference), và hiệu đối xứng (symmetric difference).
Bạn có thể tạo tập hợp bằng dấu ngoặc nhọn {}
hoặc bằng cách sử dụng hàm set()
. Lưu ý: để tạo một tập hợp rỗng, bạn phải sử dụng set()
, vì {}
sẽ tạo một từ điển trống (mà chúng ta sẽ thảo luận trong phần tiếp theo).
Ví dụ minh họa:
>>> basket = {'apple', 'orange', 'apple', 'pear', 'orange', 'banana'}
>>> print(basket) # Các phần tử trùng lặp bị loại bỏ
{'orange', 'banana', 'pear', 'apple'}
>>> 'orange' in basket # Kiểm tra thành viên nhanh
True
>>> 'crabgrass' in basket
False
Các phép toán tập hợp:
>>> a = set('abracadabra')
>>> b = set('alacazam')
>>> a # Các chữ cái duy nhất trong a
{'a', 'r', 'b', 'c', 'd'}
>>> a - b # Các chữ cái có trong a nhưng không có trong b
{'r', 'd', 'b'}
>>> a | b # Các chữ cái có trong a hoặc b hoặc cả hai
{'a', 'c', 'r', 'd', 'b', 'm', 'z', 'l'}
>>> a & b # Các chữ cái có trong cả hai tập hợp a và b
{'a', 'c'}
>>> a ^ b # Các chữ cái có trong a hoặc b nhưng không có trong cả hai
{'r', 'd', 'b', 'm', 'z', 'l'}
Tương tự như list comprehension, Python cũng hỗ trợ set comprehension:
>>> a = {x for x in 'abracadabra' if x not in 'abc'}
>>> a
{'r', 'd'}
5.5. Từ điển (Dictionaries)
Một kiểu dữ liệu hữu ích khác trong Python là từ điển (dictionary). Trong các ngôn ngữ khác, từ điển có thể được gọi là mảng kết hợp (associative array) hoặc bộ nhớ liên kết (associative memory). Không giống như chuỗi tuần tự (sequences), được lập chỉ mục bằng số, từ điển được lập chỉ mục bằng khóa (keys), có thể là bất kỳ kiểu dữ liệu bất biến nào (chuỗi, số, hoặc tuple chứa toàn bộ phần tử bất biến).
Từ điển bao gồm các cặp khóa-giá trị (key-value pairs), với yêu cầu khóa phải là duy nhất trong một từ điển.
Tạo từ điển:
>>> tel = {'jack': 4098, 'sape': 4139}
>>> tel['guido'] = 4127
>>> tel
{'jack': 4098, 'sape': 4139, 'guido': 4127}
Truy xuất và sửa đổi:
>>> tel['jack']
4098
>>> del tel['sape']
>>> tel['irv'] = 4127
>>> tel
{'jack': 4098, 'guido': 4127, 'irv': 4127}
>>> list(tel) # Lấy danh sách khóa
['jack', 'guido', 'irv']
>>> sorted(tel) # Sắp xếp khóa theo thứ tự bảng chữ cái
['guido', 'irv', 'jack']
>>> 'guido' in tel # Kiểm tra sự tồn tại của khóa
True
>>> 'jack' not in tel
False
Sử dụng dict()
để tạo từ điển từ danh sách cặp khóa-giá trị:
>>> dict([('sape', 4139), ('guido', 4127), ('jack', 4098)])
{'sape': 4139, 'guido': 4127, 'jack': 4098}
Sử dụng dictionary comprehension để tạo từ điển từ biểu thức:
>>> {x: x**2 for x in (2, 4, 6)}
{2: 4, 4: 16, 6: 36}
Nếu khóa là chuỗi đơn giản, có thể sử dụng tham số từ khóa để tạo từ điển:
>>> dict(sape=4139, guido=4127, jack=4098)
{'sape': 4139, 'guido': 4127, 'jack': 4098}
5.6. Kỹ thuật Lặp (Looping Techniques)
Khi lặp qua một từ điển, có thể lấy cả khóa và giá trị cùng một lúc bằng phương thức items()
:
>>> knights = {'gallahad': 'the pure', 'robin': 'the brave'}
>>> for k, v in knights.items():
... print(k, v)
...
gallahad the pure
robin the brave
Khi lặp qua một chuỗi tuần tự, có thể lấy chỉ số và giá trị tương ứng bằng hàm enumerate()
:
>>> for i, v in enumerate(['tic', 'tac', 'toe']):
... print(i, v)
...
0 tic
1 tac
2 toe
Để lặp qua hai hoặc nhiều chuỗi tuần tự cùng một lúc, có thể sử dụng hàm zip()
để ghép từng phần tử tương ứng:
>>> questions = ['name', 'quest', 'favorite color']
>>> answers = ['lancelot', 'the holy grail', 'blue']
>>> for q, a in zip(questions, answers):
... print('What is your {0}? It is {1}.'.format(q, a))
...
What is your name? It is lancelot.
What is your quest? It is the holy grail.
What is your favorite color? It is blue.
Để lặp qua một chuỗi tuần tự theo thứ tự ngược lại, trước tiên hãy xác định chuỗi theo hướng bình thường rồi gọi hàm reversed()
:
>>> for i in reversed(range(1, 10, 2)):
... print(i)
...
9
7
5
3
1
Để lặp qua một chuỗi tuần tự theo thứ tự đã sắp xếp, sử dụng sorted()
, hàm này trả về một danh sách mới được sắp xếp mà không thay đổi dữ liệu nguồn:
>>> basket = ['apple', 'orange', 'apple', 'pear', 'orange', 'banana']
>>> for i in sorted(basket):
... print(i)
...
apple
apple
banana
orange
orange
pear
Khi sử dụng set()
trên một chuỗi tuần tự, các phần tử trùng lặp sẽ bị loại bỏ. Việc kết hợp sorted()
với set()
giúp lặp qua các phần tử duy nhất theo thứ tự đã sắp xếp:
>>> for f in sorted(set(basket)):
... print(f)
...
apple
banana
orange
pear
Đôi khi có thể muốn thay đổi một danh sách khi đang lặp qua nó. Tuy nhiên, an toàn hơn là tạo một danh sách mới:
>>> import math
>>> raw_data = [56.2, float('NaN'), 51.7, 55.3, 52.5, float('NaN'), 47.8]
>>> filtered_data = []
>>> for value in raw_data:
... if not math.isnan(value):
... filtered_data.append(value)
...
>>> filtered_data
[56.2, 51.7, 55.3, 52.5, 47.8]
5.7. Điều kiện nâng cao (More on Conditions)
Các điều kiện được sử dụng trong câu lệnh while
và if
có thể chứa bất kỳ toán tử nào, không chỉ giới hạn ở các phép so sánh.
- Toán tử
in
vànot in
kiểm tra xem một giá trị có thuộc (hoặc không thuộc) một tập hợp nào đó không. - Toán tử
is
vàis not
so sánh xem hai đối tượng có phải là cùng một thực thể không. - Tất cả các toán tử so sánh có mức ưu tiên ngang nhau và thấp hơn tất cả các toán tử số học.
Các phép so sánh có thể kết chuỗi:
a < b == c # Kiểm tra a nhỏ hơn b và b bằng c
Toán tử logic and
, or
và not
có thể được sử dụng để kết hợp các điều kiện:
A and not B or C # Tương đương với (A and (not B)) or C
Python hỗ trợ đánh giá ngắn mạch (short-circuit evaluation):
- Trong
A and B
, nếuA
làFalse
, Python sẽ không kiểm traB
. - Trong
A or B
, nếuA
làTrue
, Python sẽ không kiểm traB
.
Giá trị cuối cùng được đánh giá là giá trị trả về của biểu thức logic:
>>> string1, string2, string3 = '', 'Trondheim', 'Hammer Dance'
>>> non_null = string1 or string2 or string3
>>> non_null
'Trondeim'
Lưu ý rằng trong Python, không giống như trong C, việc gán giá trị trong một biểu thức phải được thực hiện rõ ràng bằng toán tử cá trích (:=
). Điều này giúp tránh các lỗi phổ biến như vô tình sử dụng =
thay vì ==
trong điều kiện.
5.8. So sánh chuỗi tuần tự và kiểu dữ liệu khác (Comparing Sequences and Other Types)
Các đối tượng chuỗi tuần tự thường có thể so sánh với nhau nếu thuộc cùng một kiểu dữ liệu. Python sử dụng thứ tự từ điển (lexicographical ordering):
- Hai phần tử đầu tiên được so sánh trước, nếu khác nhau thì kết quả so sánh được xác định ngay.
- Nếu chúng bằng nhau, tiếp tục so sánh cặp phần tử tiếp theo, cho đến khi hết phần tử.
- Nếu một chuỗi là phần đầu của chuỗi còn lại, chuỗi ngắn hơn sẽ được coi là nhỏ hơn.
- Với chuỗi ký tự, thứ tự được xác định bằng mã Unicode.
Ví dụ so sánh giữa các chuỗi tuần tự cùng loại:
(1, 2, 3) < (1, 2, 4)
[1, 2, 3] < [1, 2, 4]
'ABC' < 'C' < 'Pascal' < 'Python'
(1, 2, 3, 4) < (1, 2, 4)
(1, 2) < (1, 2, -1)
(1, 2, 3) == (1.0, 2.0, 3.0)
(1, 2, ('aa', 'ab')) < (1, 2, ('abc', 'a'), 4)
Lưu ý rằng Python cho phép so sánh giữa các kiểu dữ liệu khác nhau nếu chúng có phương thức so sánh phù hợp:
- Các kiểu số hỗ trợ so sánh dựa trên giá trị (
0 == 0.0
). - Nếu không thể so sánh hợp lệ, Python sẽ báo lỗi
TypeError
thay vì tạo thứ tự tùy tiện.
Ghi chú: Một số ngôn ngữ lập trình khác có thể trả về đối tượng đã thay đổi sau khi gọi phương thức trên nó, điều này cho phép “xâu chuỗi phương thức” (method chaining), ví dụ:
d->insert("a")->remove("b")->sort();