Chương 8: Chuỗi kí tự

Trở về Mục lục cuốn sách

Chuỗi là một danh sách có thứ tự

Chuỗi là một danh sách có thứ tự hợp thành từ những kí tự riêng rẽ. Bạn có thể truy cập đến từng kí tự một bằng cách dùng toán tử là cặp ngoặc vuông:

>>> fruit = 'banana'
>>> letter = fruit[1]

Câu lệnh thứ hai nhằm chọn ra chữ cái có thứ tự 1 của fruit và gán nó cho letter.

Biểu thức nằm ở trong cặp ngoặc vuông được gọi là chỉ số. Chỉ số biểu thị cho kí tự mà bạn đang cần trong chuỗi (vì thế mà nó có tên là “chỉ số”).

Nhưng đối với bạn kết quả thu được có thể không như mong đợi:

>>> print letter
a

Với hầu hết chúng ta thì chữ cái thứ nhất của 'banana'b chứ không phải a. Nhưng với nhà khoa học máy tính, chỉ số là độ dời đi so với vị trí đầu của chuỗi; và chữ cái đầu tiên thì có độ dời bằng 0.

>>> letter = fruit[0]
>>> print letter
b

Như vậy b là chữ cái thứ 0 của 'banana', a là chữ cái thứ 1, và n là chữ cái thứ 2.

Bạn có thể dùng bất kì biểu thức nào, bao gồm các biến và toán tử, cho một chỉ số, nhưng cuối cùng giá trị của chỉ số phải là số nguyên. Nếu không thì bạn sẽ gặp lỗi:

>>> letter = fruit[1.5]
TypeError: string indices must be integers

len

len là một hàm có sẵn để trả lại số kí tự trong một chuỗi:

>>> fruit = 'banana'
>>> len(fruit)
6

Để lấy chữ cái cuối cùng của một chuỗi, có thể là bạn đã từng thử đoạn lệnh như sau:

>>> length = len(fruit)
>>> last = fruit[length]
IndexError: string index out of range

Lí do xuất hiện lỗi IndexError là vì không có chữ cái nào ở vị trí chỉ số 6 trong chuỗi 'banana'. Vì chúng ta tính chỉ số từ 0, nên sáu chữ cái sẽ được đánh số từ 0 đến 5. Để có được chữ cái cuối cùng, bạn phải trừ bớt length đi 1:

>>> last = fruit[length-1]

>>> print last
a

Một cách khác là bạn dùng chỉ số âm, nghĩa là đếm ngược từ điểm cuối của chuỗi. Biểu thức fruit[-1] cho ra chữ cái cuối cùng, fruit[-2] cho chữ cái áp chót, và cứ như vậy.

Duyệt bằng một vòng lặp for

Có nhiều công việc tính toán liên quan đến xử lý một chuỗi theo từ kí tự một. Bắt đầu từ điểm đầu của chuỗi, từng chữ cái được lựa chọn, một số thao tác được thực hiện đối với chữ cái đó, và công việc được tiếp diễn cho các chữ cái còn lại đến hết chuỗi. Kiểu xử lý như thế này được gọi là duyệt. Để viết mã lệnh thực hiện việc duyệt, ta có thể dùng vòng lặp while:

index = 0
while index < len(fruit):
    letter = fruit[index]
    print letter
    index = index + 1

Vòng lặp này để duyệt chuỗi và hiển thị từng chữ cái trên một dòng riêng. Điều kiện lặp là index < len(fruit), như vậy khi index bằng với chiều dài của chuỗi, điều kiện bị vi phạm, và phần thân của vòng lặp không được thực hiện. Chữ cái cuối cùng được truy cập đến sẽ tương ứng với chỉ số len(fruit)-1, cũng là chữ cái cuối cùng của chuỗi.

Hãy viết một hàm nhận vào đối số là một chuỗi và hiển thị lần lượt các chữ cái theo thứ tự ngược lại, mỗi chữ cái trên một dòng riêng.

Một cách khác để thực hiện việc duyệt là dùng một vòng lặp for:

for char in fruit:
    print char

Mỗi lần qua vòng lặp, kí tự tiếp theo của chuỗi được gán cho biến char. Vòng lặp tiếp diễn đến khi không còn kí tự nào nữa.

Ví dụ tiếp sau đây minh hoạc cho cách dùng phép ghép nối (phép “cộng” chuỗi) và một vòng lặp for để tạo ra một danh sách abc (tức là theo đúng thứ tự của bảng chữ cái). Trong cuốn sách của McCloskey, Make Way for Ducklings (Dành đường cho vịt con), tên của các chú vịt con là Jack, Kack, Lack, Mack, Nack, Ouack, Pack, and Quack. Vòng lặp sau đây sẽ cho ra những cái tên theo đúng thứ tự:

prefixes = 'JKLMNOPQ'
suffix = 'ack'

for letter in prefixes:
    print letter + suffix

Kết quả là

Jack
Kack
Lack
Mack
Nack
Oack
Pack
Qack

Dĩ nhiên là kết quả trên không hoàn toàn đúng vì “Ouack” và “Quack” đã bị viết chệch đi.

Hãy sửa lại chương trình để có kết quả đúng.

Lát cắt trong chuỗi

Một đoạn trong chuỗi được gọi là lát cắt. Việc chọn một lát cắt cũng giống như chọn một kí tự:

>>> s = 'Monty Python'
>>> print s[0:5]
Monty
>>> print s[6:12]
Python

Toán tử [n:m] trả lại phần của chuỗi tính từ kí tự thứ n cho đến kí tự thứ m, trong đó bao gồm kí tự thứ n và không kể kí tự thứ m. Điều này nghe được hợp lý lắm, nhưng nó sẽ giúp bạn hình dung được vị trí của chỉ số là ở giữa các kí tự, như trong sơ đồ sau:

banana

Nếu bạn bỏ qua chỉ số thứ nhất (trước dấu hai chấm) thì lát cắt sẽ bắt đầu ở ngay điểm đầu của chuỗi. Nếu bạn bỏ qua chỉ số thứ hai thì lát cắt sẽ kết thúc ở điểm cuối của chuỗi:

>>> fruit = 'banana'

>>> fruit[:3]
'ban'
>>> fruit[3:]
'ana'

Nếu chỉ số thứ nhất lớn hơn hoặc bằng chỉ số thứ hai thì kết quả thu được sẽ là một chuỗi trống, được biểu thị bằng hai dấu nháy:

>>> fruit = 'banana'
>>> fruit[3:3]
''

Một chuỗi trống không chứa kí tự nào và có độ dài bằng 0, nhưng các đặc điểm khác của nó thì cũng tương tự như một chuỗi bất kì.

Biết rằng fruit là một chuỗi, thì fruit[:] nghĩa là gì?

Chuỗi không thể bị thay đổi

Bạn có thể muốn dùng toán tử [] bên vế trái của một lệnh gán, với ý định thay đổi một kí tự trong chuỗi. Chẳng hạn:

>>> greeting = 'Hello, world!'

>>> greeting[0] = 'J'
TypeError: object does not support item assignment

Đối tượng (“object”) ở trong trường hợp này chính là chuỗi, còn phần tử (“item”) là kí tự mà bạn muốn gán. Tạm thời bây giờ ta coi đối tượng cũng giống như một giá trị, nhưng sẽ định nghĩa lại sau. Một phần tử là một trong số các giá trị có trong chuỗi.

Lí do gây ra lỗi là ở chỗ các chuỗi đều không thể thay đổi, có nghĩa rằng bạn không thể thay đổi một chuỗi hiện có. Việc tốt nhất mà bạn có thể làm được là tạo ra một chuỗi mới như một biến thể của chuỗi ban đầu:

>>> greeting = 'Hello, world!'
>>> new_greeting = 'J' + greeting[1:]
>>> print new_greeting
Jello, world!

Ví dụ này ghép nối một chữ cái mới ở vị trí thứ nhất với lát cát của greeting. Nó không có ảnh hưởng gì đến chuỗi ban đầu.

Tìm kiếm

Hàm sau đây nhằm mục đích gì?

def find(word, letter):
    index = 0
    while index < len(word):
        if word[index] == letter:
            return index
        index = index + 1
    return -1

Theo một nghĩa nào đó, find chính là hàm ngược của toán tử []. Thay vì việc nhận một chỉ số và tìm ra kí tự tương ứng, hàm này lại nhận một kí tự và tìm ra chỉ số là vị trí xuất hiện của kí tự đó. Nếu không tìm thấy kí tự, hàm sẽ trả lại -1.

Đây là ví dụ đầu tiên mà ta thấy một câu lệnh return bên trong vòng lặp. Nếu word[index] == letter, hàm sẽ thoát khỏi vòng lặp và trở về ngay lập tức.

Nếu kí tự không xuất hiện trong chuỗi, chương trình sẽ kết thúc vòng lặp một cách bình thường và trả lại -1.

Dạng tính toán như thế này—duyệt một danh sách và thoát khi ta tìm được phần tử mà ta cần—được gọi là tìm kiếm.

Chỉnh sửa hàm find để nhận thêm tham biến thứ ba nữa, chỉ số trong word tại đó việc tìm kiếm được bắt đầu.

Lặp và đếm

Chương trình sau đây đếm số lần xuất hiện của chữ cái a trong một chuỗi:

word = 'banana'
count = 0
for letter in word:
    if letter == 'a':
        count = count + 1
print count

Chương trình này giới thiệu một dạng tính toán nữa gọi là đếm. Biến count được khởi tạo với giá trị 0 và sau đó tăng thêm 1 mỗi lần chữ cái a được tìm thấy. Khi vòng lặp kết thúc, count sẽ chứa kết quả—tổng số chữ cái a.

Hãy gói đoạn mã này vào trong một hàm có tên là count, và sau đó khái quát hóa sao cho nó nhận chuỗi và chữ cái làm các tham biến.

Hãy viết lại hàm sao cho, thay vì duyệt qua chuỗi, hàm sử dụng dạng có 3 tham số của find từ mục trước.

Các phương thức của string (chuỗi)

Một phương thức tương tự như một hàm—nó nhận vào các đối số và trả lại một giá trị—nhưng cú pháp lại khác. Chẳng hạn, phương thức upper nhận một chuỗi và trả lại một chuỗi mới trong đó tất cả các chữ cái đều được viết in:

Thay vì dạng cú pháp của hàm upper(word), kiểu cú pháp của phương thức word.upper() được dùng đến.

>>> word = 'banana'

>>> new_word = word.upper()
>>> print new_word
BANANA

Dạng này của kí hiệu dấu chấm có nêu ra tên của phương thức, upper, và tên của chuỗi mà ta áp dụng phương thức, word. Cặp ngoặc tròn bỏ trống chỉ ra rằng phương thức này không nhận tham biến.

Một lời gọi phương thức được gọi là kích hoạt; trong trường hợp này ta nói rằng đã kích hoạt upper đối với word.

Hóa ra rằng, cũng có một phương thức của chuỗi có tên find thực hiện nhiệm vụ giống như hàm mà ta đã viết:

>>> word = 'banana'

>>> index = word.find('a')
>>> print index
1

Trong ví dụ này, ta đã kích hoạt find đối với word và sau đó truyền chữ cái cần tìm vào như một tham biến.

Thật ra, phương thức find tổng quát hơn so với hàm của chúng ta; nó có thể tìm được cả các chuỗi con chứ không riêng gì chữ cái:

>>> word.find('na')
2

Nó có thể nhận một đối số thứ hai là chỉ số mà tại đó việc tìm kiếm bắt đầu:

>>> word.find('na', 3)
4

Và tham số thứ ba là chỉ số mà tại đó dừng việc tìm kiếm:

>>> name = 'bob'
>>> name.find('b', 1, 2)
-1

Việc tìm kiếm trên bị thất bại vì b không xuất hiện trong vùng chỉ số từ 1 đến 2 (không bao gồm 2).

Có một phương thức của chuỗi có tên count tương tự như hàm ở trong bài tập trước đây. Hãy đọc tài liệu giải thích hàm này và sau đó viết một lệnh kích hoạt để đếm số chữ cái a có trong 'banana'.

Toán tử in

Từ tiếng Anh in cũng chính là tên một toán tử nhận vào hai chuỗi và trả lại True nếu chuỗi thứ nhật là một chuỗi con của chuỗi thứ hai:

>>> 'a' in 'banana'
True

>>> 'seed' in 'banana'
False

Chẳng hạn, hàm sau iin ra tất cả các chữ có trong word1 đồng thời cũng xuất hiện trong word2:

def in_both(word1, word2):
    for letter in word1:
        if letter in word2:
            print letter

Với các tên biến được chọn hợp lý thì mã lệnh Python đôi khi đọc lên cũng giống tiếng Anh. Nếu dịch nôm na bằng tiếng Việt thì vòng lặp trên sẽ thành: “với (mỗi) chữ cái trong từ (thứ nhất), nếu chữ cái (này) (cũng có) trong từ (thứ hai) thì in ra chữ cái (này)”.

Và sau đây là kết quả thu được khi bạn so sánh apples (táo) với oranges (cam):

>>> in_both('apples', 'oranges')
a
e
s

So sánh chuỗi

Các toán tử quan hệ cũng có tác dụng đối với chuỗi. Để xem hai chuỗi có bằng nhau hay không:

if word == 'banana':
    print  'Được rồi, bananas.'

Các toán tử quan hệ khác sẽ giúp ích khi cần xếp các từ theo thứ tự bảng chữ cái:

if word < 'banana':
    print 'Từ của bạn,' + word + ', xếp trước banana.'
elif word > 'banana':
    print 'Từ của bạn,' + word + ', xếp sau banana.'
else:
    print 'Được rồi, bananas.'

Python không xử lý được chữ in và chữ thường theo cách chúng ta thường làm. Tất cả chữ in đều được xếp trước chữ thường, vì vậy:

Từ của bạn, Pineapple, xếp trước banana.

Một cách làm thông thường để giải quyết điều này là chuyển đổi chuỗi về một dạng chung, như dạng chữ thường, trước khi thực hiện so sánh. Hãy nhớ điều này khi bạn cần phải tự bảo vệ mình trước một gã vừa vớ lấy Pineapple (quả dứa) làm vũ khí.

Gỡ lỗi

Khi bạn dùng chỉ số để duyệt các giá trị trong một danh s, việc lấy đúng các điểm đầu và cuối danh sách cũng là một mẹo cần lưu ý. Sau đây là một hàm dự định để so sánh hai từ và trả lại True nếu từ này là dạng đảo ngược của từ kia; nhưng nó đã có hai lỗi sai:

def is_reverse(word1, word2):
    if len(word1) != len(word2):
        return False

    i = 0
    j = len(word2)

    while j > 0:
        if word1[i] != word2[j]:
            return False
        i = i+1
        j = j-1

    return True

Lệnh if thứ nhất kiểm tra xem các từ có cùng độ dài hay không. Nếu không, ta lập tức trả lại False; sau đó trong phần còn lại của hàm, ta coi như các từ đã có cùng độ dài. Đây là một ví dụ cho cách dùng kiểu mẫu chốt chặn trong Mục guardian.

ij là các chỉ số: i duyệt trên word1 theo hướng tiến còn j duyệt trên word2 theo hướng lùi. Nếu phát hiện được hai chữ cái không khớp nhau, ta sẽ lập tức trả lại False. Nếu như có thể đi qua được toàn bộ vòng lặp và các cặp chữ cái đều khớp nhau thì ta sẽ trả lại True.

Nếu kiểm tra hàm này với các từ “pots” và “stop”, có lẽ chúng ta trông đợi kết quả trả về là True, nhưng thật ra sẽ nhận được thông báo lỗi IndexError:

>>> is_reverse('pots', 'stop')
...
  File "reverse.py", line 15, in is_reverse
    if word1[i] != word2[j]:
IndexError: string index out of range

Để gỡ lỗi kiểu này, hành động trước tiên của tôi là in các giá trị chỉ số ngay trước dòng lệnh có lỗi xảy ra.

    while j > 0:
        print i, j        # đặt lệnh print ở đây

        if word1[i] != word2[j]:
            return False
        i = i+1
        j = j-1

Bây giờ khi chạy lại chương trình, tôi sẽ thu được thêm thông tin:

>>> is_reverse('pots', 'stop')
0 4
...
IndexError: string index out of range

Lần đầu tiên chạy vòng lặp, giá trị của j là 4, nghĩa là vượt ra ngoài phạm vi của chuỗi 'pots'. Chỉ số của kí tự cuối cùng bằng 3, vì vậy giá trị ban đầu của j phải là len(word2)-1.

Nếu sửa lại lỗi đó và chạy lại chương trình, tôi sẽ thu được:

>>> is_reverse('pots', 'stop')
0 3
1 2
2 1
True

Lần này thì chúng ta có kết quả đúng, nhưng dường như vòng lặp chỉ được thực hiện ba lần, điều này thật đáng ngờ. Để thấy rõ hơn điều gì đã xảy ra, ta cần vẽ một biểu đồ trạng thái. Trong suốt lần lặp thứ nhất, khung chứa is_reverse sẽ trông như sau:

sơ đồ trạng thái

Tôi đã sắp xếp các biến trong khung và vẽ thêm các đường đứt nét để cho thấy các giá trị của ij dùng để chỉ đến các chữ cái trong word1word2.

Bắt đầu từ biểu đồ này, hãy thực hiện nhẩm chương trình trên giấy, thay đổi các giá trị của ij ở mỗi lần lặp. Hãy tìm và sửa lại lỗi sai thứ hai trong hàm này.

Thuật ngữ

đối tượng:
Thứ mà một biến có thể chỉ định đến. Tạm thời bây giờ bạn có thể coi “đối tượng” và “giá trị” là tương đương nhau.

danh sách:
Một tập hợp có thứ tự; nghĩa là một tập hợp các giá trị mà mỗi giá trị có thể được chỉ định bởi một chỉ số nguyên.

phần tử:
Một trong số các giá trị trong một danh s.

số thứ tự:
Một giá trị số nguyên dùng để chọn một phần tử trong danh s chẳng hạn như một kí tự trong một chuỗi.

lát cắt:
Một phần của danh sách giới hạn giữa hai số thứ tự.

chuỗi trống:
Một chuỗi mà không có kí tự nào; nó có độ dài bằng 0 và được biểu thị bằng hai dấu nháy.

(tính) không thể thay đổi:
Thuộc tính của một danh sách trong đó các phần tử của nó không thể gán giá trị được.

duyệt:
Việc lặp lại qua các phần tử trong một danh sách, trong khi thực hiện cùng một thao tác với mọi phần tử.

tìm kiếm:
Một kiểu mẫu duyệt trong đó khi tìm được phần tử mong muốn thì dừng lại.

biến đếm:
Một biến dùng để đếm thứ gì đó, thường được khởi tạo giá trị không và sau đó tăng dần lên.

phương thức:
Một hàm được gắn liền với một đối tượng và được gọi theo kiểu cú pháp dấu chấm.

kích hoạt:
Một câu lệnh làm nhiệm vụ gọi một phương thức.

Bài tập

Một lát cắt của chuỗi có thể nhận một chỉ số thứ ba để chỉ định “kích cỡ của bước”; nghĩa là số khoảng cách giữa các kí tự kế tiếp. Một bước bằng 2 nghĩa là cách một kí tự lấy một; bước 3 nghĩa là cách hai kí tự mới lấy một, v.v…

>>> fruit = 'banana'

>>> fruit[0:5:2]
'bnn'

Một bước bằng -1 sẽ duyệt toàn bộ của từ theo hướng ngược lại, vì vậy lát cắt [::-1] cho ta một chuỗi lộn ngược lại.

Dùng cách kí hiệu trên để viết một câu lệnh cho hàm is_palindrome ở Bài tập palindrome.

Hãy đọc tài liệu hướng dẫn về những phương thức liên quan đến chuỗi tại docs.python.org/lib/string-methods.html. Bạn có thể muốn thử một số phương thức để nắm vững hoạt động của chúng. stripreplace là hai phương thức đặc biệt có ích.

Tài liệu sử dụng một hình thức cú pháp có thể làm bạn hiểu lầm. Chẳng hạn, trong find(sub[, start[, end]]), các cặp ngoặc vuông là để chỉ những đối số có hay không đưa vào đều được. Như vậy sub là đối số bắt buộc phải có mặt, còn start thì tùy, và nếu bạn có đưa start vào, thì end vẫn có thể có mặt hoặc không.

Các hàm sau đây đều dự kiến nhằm mục đích kiểm tra xem liệu một chuỗi có bao gồm chữ cái viết thường không, nhưng ít nhất một vài trong số đó bị sai. Với mỗi hàm, hãy mô tả xem tác dụng thực sự của nó là gì (với giả thiết rằng tham biến truyền vào là một chuỗi).

def any_lowercase1(s):
    for c in s:
        if c.islower():
            return True
        else:
            return False

def any_lowercase2(s):
    for c in s:
        if 'c'.islower():
            return 'True'
        else:
            return 'False'

def any_lowercase3(s):
    for c in s:
        flag = c.islower()
    return flag

def any_lowercase4(s):
    flag = False
    for c in s:
        flag = flag or c.islower()
    return flag

def any_lowercase5(s):
    for c in s:
        if not c.islower():
            return False
    return True

ROT13 là một dạng mã hóa yếu trong đó mỗi chữ cái được “xoay” đi 13 vị trí.1 Việc xoay một chữ cái có nghĩa là dịch chuyển vị trí trong bảng chữ cái và đến cuối bảng thì quay lộn về đầu. Chẳng hạn chữ ’A’ dịch đi 3 thì cho chữ ’D’ và ’Z’ dịch đi 1 thì cho chữ ’A’.

Hãy viết một hàm có tên là rotate_word trong đó nhận các tham biến gồm một chuỗi và một số nguyên, rồi trả lại một chuỗi mới trong đó có chứa các chữ trong chuỗi đầu sau khi đã “xoay” đi số vị trí cho trước.

Chẳng hạn, “cheer” xoay đi 7 là “jolly” và “melon” xoay đi -10 là “cubed”.

Bạn có thể cần dùng đến các hàm có sẵn ord, which converts a character to a numeric code, and chr, which converts numeric codes to characters.

Trên Internet đã có một số câu nói đùa khiêu khích được tạo ra bằng ROT13. Nếu bạn không dễ bị chọc giận, hãy tìm kiếm một số câu nói như vậy và thử giải mã xem.

1 bình luận

Filed under Think Python

1 responses to “Chương 8: Chuỗi kí tự

  1. Pingback: Think Python: Cách tư duy như nhà khoa học máy tính | Blog của Chiến

Bình luận về bài viết này