Hướng dẫn lập trình
Cơ sở lập trình
Chuyên ngành Thương mại điện tử - Ngành Kinh doanh Thương mại

Cập nhật ngày 26 tháng 02 năm 2018

Số nguyên và số thực

Hãy cẩn thận khi làm việc với các số nguyên (int) và số thực (float), hãy chắc rằng bạn đã lường hết các khả năng có thể xảy ra khi thực hiện các tính toán. Để hiểu chi tiết cách Python thể hiện các số thực, hãy đọc mục 3.4 (trang 29-31) tài liệu Introduction to Computation and Programming Using Python: With Application to Understanding Data (John Guttag).

Nói chung, các số thực được thể hiện như các hệ số của luỹ thừa hai, và nhiều số hệ thập phân không thể được thể hiện chính xác theo cách này. Vấn đề phổ biến nhất bạn sẽ gặp là khi cố gắng kiểm tra bằng nhau giữa các số thực. Ví dụ, bạn có một vòng lặp while, trong đó bạn tăng một biến thực lên 0.1, và bạn muốn vòng lặp sẽ tiếp tục cho đến khi nó đạt giá trị 0.3. Bạn sẽ thấy rằng việc thiết lập vòng lặp như vậy sẽ tạo ra việc lặp vô hạn:

variable = 0.0
while variable != 0.3:
    variable += 0.1

Điều này xảy ra là bởi vì 0.1 + 0.1 + 0.1 = 0.30000000000000004 là không bằng 0.3. Lý do là vì 0.1 không được thể hiện chính xác ở số thực. Do đó, nếu bạn đơn giản chỉ muốn thực hiện phép toán đếm, hãy luôn sử dụng số nguyên để tránh vấn đề này.

Điều ngay sau đây là không còn đúng ở bản Python 3.6. Ngoài ra, hãy đảm bảo rằng bạn thực hiện chuyển đổi kiểu giữa số nguyên và số thực đúng thời điểm. Ví dụ, nếu bạn muốn thực hiện một phép toán chia 3 cho 5, việc viết float(3/5) sẽ không cho ra kết quả mong muốn. Hãy nhớ rằng Python sẽ tính biểu thức bên trong ngoặc trước, nên nó sẽ thực hiện chia nguyên 3/5 trước, sau đó nó mới ép kiểu kết quả ra một số thực. Do đó, bạn nên ép kiểu một trong hai số nguyên sang số thực trước khi thực hiện phép toán chia - ví dụ, float(3)/5 hoặc 3/float(5). Bạn sẽ luôn gặp trường hợp này khi thực hiện tính trung bình hoặc phần trăm.

Vòng lặp forwhile

Lựa chọn câu lệnh vòng lặp chính xác sẽ làm cho mã nguồn của bạn sáng sủa và có thể giảm lỗi. Những thứ có thể viết với câu lệnh for thì đều có thể được viết với câu lệnh while, nhưng câu lệnh while có thể giải quyết một số vấn đề mà câu lệnh for khó có thể viết dễ dàng. Bạn nên sử dụng câu lệnh for khi có thể.

Nói chung, nên sử dụng câu lệnh for khi bạn biết số lần lặp cần thực hiện, ví dụ: 500 phép thử, một thao tác trên mỗi ký tự của một chuỗi, hoặc một hành động trên mỗi thành phần của một danh sách. Nếu trong mô tả bằng lời vấn đề bạn đang giải quyết mà có các từ như "mỗi" hoặc "mọi" hoặc "tất cả" các thành phần trong một đối tượng cho phép lặp, bạn hãy dùng vòng lặp for. Sử dụng vòng lặp for khi có thể sẽ giảm nguy cơ viết một vòng lặp vô hạn và sẽ giúp bạn tránh các lỗi khi làm việc với các biến đếm.

Ví dụ (in chuỗi "hello" 500 lần):
for i in range(500):
    print 'hello'

Ví dụ (tăng 1 đơn vị cho .tất cả các (mọi) thành phần của một danh sách):
my_list = [5, 2, 7, -4, 0]
for i in xrange(len(my_list)):
    my_list[i] += 1

Ngược lại, nếu bạn muốn thực hiện việc lặp khi một điều kiện nào đó là thoả mãn, bạn nên dùng vòng lặp while. Vòng lặp while là hữu dụng khi bạn có thể định nghĩa số lần lặp theo kiểu một biến luận lý. Nếu bạn đang mong chờ người dùng nhập vào một dữ liệu chính xác hoặc một giá trị được tạo ra ngẫu nhiên để đạt một ngưỡng nào đó, bạn nên sử dụng một vòng lặp while. Các vấn đề mà nó có thể được mô tả sử dụng từ "cho đến khi" thì cũng nên được sử dụng vòng lặp while.

Ví dụ (lặp cho đến khi người dùng nhập vào một số dương):
num = float(input('Enter a positive number:')
while num <= 0.0:
    num = float(input('Enter a positive number:')

Ví dụ (lặp cho đến khi tạo ra ngẫu nhiên một số lớn hơn 0.5):
import random
num = random.random()
while num <= 0.5:
    num = random.random()

Để cải tiến hiệu năng trung bình của mã nguồn, bạn có thể đôi khi muốn thoát khỏi các vòng lặp ngay khi bạn đã tìm ra được kết quả, bạn sẽ nhân thấy rằng nhiều vòng lặp được dùng để tìm các kết quả True/False sẽ theo khuôn dạng này. Ví dụ, bạn muốn kiểm tra có bất kỳ giá trị nào trong một danh sách là lớn hơn 5.

my_list = [1, 2, 3, 4, 5, 6, 7, 8]
greater_than_file = False
for elem in my_list:
    if elem > 5:
        greater_than_file = True
        break

Kiểm tra biểu thức luận lý với câu lệnh if/else

Thông thường, mọi người có xu hướng dài dòng quá mức. Hãy xem xét ví dụ sau:

if my_function() == True: # my_function return True or False
    return True
else:
    return False

Khi Python thực hiện my_function(), mã nguồn sẽ được dẫn xuất thành như sau (giả sử như hàm trả ra giá trị True):

if True == True: # my_function return True or False
    return True
else:
    return False

Như vậy, ở đây có sự dư thừa. Bạn biết rằng True bằng True, và False thì không. Do đó, thay vì phải giữ lại phần == True, bạn có thể chỉ cần để lại mỗi lời gọi hàm bên trong câu lệnh if như sau:

if my_function(): # my_function return True or False
    return True
else:
    return False

Hơn nữa, ở đây còn một điểm quan trọng có thể rút gọn. Bởi vì hàm my_function() sẽ trả ra giá trị True/False, và bạn cũng sẽ trả ra chính giá trị đó (câu lệnh return), do đó không có lý do gì mà không viết như sau:

return my_function() # my_function return True or False

Câu lệnh này rất ngắn gọn và chính xác như những gì chúng ta muốn.

Nhưng nếu chúng ta muốn trả ra True khi my_funcion trả ra False và ngược lại thì sao? Ở đây chúng ta sẽ sử dụng từ khoá not để giải quyết vấn đề này. Ví dụ, đoạn chương trình sẽ như sau:

if my_function() == True: # my_function return True or False
    return False
else:
    return True

Chúng ta có thể sử dụng từ khoá not để viết lại thành:

return not my_function() # my_function return True or False

Docstrings

Khi viết một lớp (class) hoặc một hàm (function) mới, bạn nên sử dụng docstring để viết tài liệu hướng dẫn (document) giải thích mục đích của bạn. Ví dụ, trong Bài tập 5, vì có nhiều lớp sẽ được cài đặt, nên thêm docstring để giải thích mục đích của mỗi lớp là cần thiết.

Đôi khi một số sẽ rất đơn giản như:
class TitleTrigger(WordTrigger):
    """
    Lớp con của WordTrigger, thể hiện một Trigger để kiểm tra xem
    tiêu đề có trùng với chuỗi đầu vào hay không.
    """

Khi thêm docstring có nghĩa là đặc tả bạn viết có thể được đọc bởi những ai đang muốn tạo một thể hiện của lớp của bạn. Ví dụ, bạn định nghĩa lớp TitleTrigger như trên, chạy file, sau đó gõ câu lệnh sau vào trình diễn dịch:
>>> TitleTrigger(
Bạn sẽ thấy docstring ở trên hiển thị ra.

Sửa đổi các tập hợp khi đang duyệt qua chúng

Rất không tốt nếu chúng ta sửa đổi một tập hợp (collection) trong khi đang duyệt qua nó. Lý do là vì hành vi được tạo ra từ việc sửa đổi này là bị nhập nhằng. Câu lệnh for duy trì một giá trị chỉ mục (index), nó được tăng lên sau mỗi vòng lặp. Nếu bạn sửa đổi danh sách bạn đang duyệt qua, các chỉ mục sẽ không được đồng bộ, và bạn có thể bỏ qua các thành phần hay xử lý cùng thành phần nhiều lần.

Ví dụ:
elems = ['a', 'b', 'c']
for e in elems:
    print(e)
    elems.remove(e)
Đoạn lệnh này sẽ in ra:
a
c

Trong khi đó, nếu bạn kiểm tra lại danh sách trên còn lại gì, thì kết quả sẽ như sau:
>>> elems
['b']

Vì sao điều này lại xảy ra? Hãy xem đoạn mã sau cũng cho ra cùng kết quả, nhưng sử dụng vòng lặp while.
elems = ['a', 'b', 'c']
i = 0
while i < len(elems):
    e = elems[i]
    print(e)
    elems.remove(e)
    i += 1
Bây giờ, chúng ta có thể thấy rõ ràng điều gì xảy ra: khi bạn xoá thành phần 'a', danh sách sẽ được giảm đi một và thành ['b', 'c'] với chỉ số của 'b' là 0, của 'c' là 1. Bởi vì vòng lặp tiếp theo sẽ có chỉ số 1 (tương ứng với 'c'), cho nên 'b' sẽ bị bỏ qua. Đây không phải là điều chúng ta mong đợi.

Hãy xem một ví dụ khác, thay cho việc xoá, chúng ta sẽ thêm các thành phần vào:
for e in elems:
    print(e)
    elems.append(e)
Điều chúng ta muốn là danh sách sẽ trở thành ['a', 'b', 'c', 'a', 'b', 'c']. Tuy nhiên, vòng lặp trên sẽ lặp vô hạn.

Để xử lý vấn đề này, bạn có thể duyệt qua một bản sao của danh sách này. Ví dụ, trong đoạn mã sau, chúng ta mong muốn lưu lại những thành phần thoả mãn điều kiện nào đó:
elems_copy = elems[:]
for item in elems_copy:
    if not condition:
        elems.remove(item)
elems sẽ chứa những thành phần mà chúng ta mong muốn.

Thay vào đó, bạn có thể tạo một danh sách mới, và thêm chúng vào:
elems_copy = []
for item in elems:
    if condition:
        elems_copy.append(item)
elems_copy sẽ chứa những thành phần mà chúng ta mong muốn

Chú ý là quy luật này cũng áp dụng cho setdictionary. Tuy nhiên, sửa đổi một set hoặc dictionary khi đang duyệt qua nó sẽ phát sinh lỗi RuntimeError bởi vì Python không cho phép điều này.

Truy xuất trực tiếp các biến thành phần (instance variable)

Điều này sẽ phá vỡ giao diện của lớp bạn cung cấp. Ví dụ, một dòng mã bạn có thể viết trong Bài tập 5:

title = story.title # evil

Điều này là không tốt bởi vì nếu muốn hoạt động đúng yêu cầu của người lập trình thì biến thành phần cho tiêu đề (title) của NewsStory phải là self.title. Tuy nhiên, người viết lớp NewsStory đôi khi có thể không dùng self.title, mà có thể dùng self.t, hoặc self.my_story_title, hoặc có thể là thành phần đầu tiên của self.story_attributes, hoặc bất kỳ thứ gì khác. Trong thực tế, điều hứa hẹn duy nhất cho lớp NewsStory là nó có một cấu tử (constructor) lấy một số thông tin của một câu truyện (mã số, tiêu đề,...), và có các hàm get cho mỗi thông tin đó.

Nói chung, cách an toàn nhất cho vấn đề này là hãy sử dụng các hàm get.

title = story.get_title() # much better!

Gọi hàm cấu tử của lớp cơ sở từ lớp dẫn xuất

Đối với một lớp dẫn xuất mở rộng chức năng của lớp cơ sở (Ví dụ lớp ResistantVirusSimpleVirus trong Bài tập 7), hãy luôn tái sử dụng các chức năng đã có của lớp cơ sở thay vì phải viết lại chúng.

Ví dụ, lớp SimpleVirus có hàm cấu tử như sau:
class SimpleVirus(object):

    def __init__(self, maxBirthProb, clearProb):
        self.maxBirthProb = maxBirthProb
        self.clearProb = clearProb

Khi chúng ta định nghĩa lớp ResistantVirus, chúng ta cũng sẽ lặp lại hai dòng mã như trên và bổ sung thêm các dòng mã mới:
# Phương pháp 1
class ResistantVirus(SimpleVirus):
    def __init__(self, maxBirthProb, clearProb, resistances, mutProb):
        self.maxBirthProb = maxBirthProb
        self.clearProb = clearProb
        self.resistances = resistances
        self.mutProb = mutProb

Thay vào đó, chúng ta có thể sử dụng tính năng của thừa kế và gọi hàm cấu tử của lớp cơ sở vì điều này cũng sẽ cho kết quả như nhau.
# Phương pháp 2
class ResistantVirus(SimpleVirus):
    def __init__(self, maxBirthProb, clearProb, resistances, mutProb):
        # Luôn gọi hàm cấu tử của lớp cơ sở ngay dòng đầu tiên trong
        # hàm cấu tử của lớp dẫn xuất
        SimpleVirus.__init__(self, maxBirthProb, clearProb)
        self.resistances = resistances
        self.mutProb = mutProb
Phương pháp 2 là tốt hơn vì nó không bị lặp mã nguồn.

Vì sao điều này lại là quan trọng? Một trường hợp có thể xảy ra như sau: khi cài đặt theo phương pháp 1, thay vì viết chính xác self.maxBirthProb, chúng ta lại viết self.maximumBirthProb. Thành phần này không tồn tại. Do đó, chương trình sẽ bị lỗi.

Những cạm bẫy của việc lưu trữ các đối tượng tự tạo trong các cấu trúc dữ liệu

Ví dụ hàm sau sử dụng các đối tượng Position để kiểm tra các ô có "sạch" hay không trong một cấu trúc dữ liệu:
def isTileCleaned(self, m, n):
    newPosition = Position(m, n)
    return newPosition in self.cleanTileList

Đoạn mã này sẽ luôn trả ra False, thậm chí khi ô tại vị trí (m, n) đó là "sạch". Lý do là nằm ở cách so sánh bằng các object trong Python.

Vậy Python so sánh bằng giữa các object như thế nào? Đối với các kiểu dữ liệu cơ bản như số nguyên (int), điều này rất đơn giản là chỉ cần kiểm tra 2 số có bằng nhau hay không. Tương tự, với chuỗi, kiểm tra xem từng ký tự một có giống nhau hay không.

Tuy nhiên, với lớp, Python sẽ kiểm tra vị trí của object đó trong bộ nhớ. Trong đoạn code trên, chúng ta tạo một đối tượng mới của Position (newPosition), điều này có nghĩa là đối tượng mới được tạo ra này sẽ không nằm cùng vị trí với bất kỳ đối tượng Position nào trong danh sách. Do đó, chúng ta sẽ không thể tìm thấy thành phần nào trong danh sách là phù hợp cả.

Có hai cách để tránh vấn đề này. Trong môn này, phương pháp đề xuất là thay đổi cách chúng ta lưu trữ trong cấu trúc dữ liệu, ví dụ có thể sử dụng tuple để thể hiện một cặp toạ độ x và y.

Nên sử dụng cấu trúc dữ liệu nào ?

Trong Bài tập 6, bạn phải lưu trữ tình trạng sạch sẽ của một căn phòng vuông, kích thước w * h ô. Chúng ta có nhiều giải pháp cho vấn đề này, ở đây chúng ta sẽ bàn luận về các ưu và nhược điểm của các giải pháp này.

List

Hầu hết mọi người sẽ chọn list (danh sách) các tuple (x, y) cho mỗi ô. Cách này có một vài điều dở.

Đầu tiên, trước khi bạn muốn thêm một toạ độ của ô nào đó vào list các ô sạch, bạn phải duyệt qua hầu như hết list đó để kiểm tra xem ô đó là đã "sạch" chưa. Điều này có thể dẫn đến lỗi, cũng như giảm hiệu năng của chương trình.

Vấn đề thứ hai của cách làm này là nó không đủ mềm dẻo khi chúng ta cần lưu trữ nhiều thông tin hơn tương ứng với các ô. Ví dụ, nếu chúng ta muốn lưu trữ các tình trạng "sạch", "dơ", "hơi bẩn", thì cách lưu trữ trên không thể giải quyết được.

Set

Một giải pháp khác là sử dụng cấu trúc dữ liệu tập hợp (set) để lưu các tuple. Ví dụ: set((x,y),...)
Giải pháp này tốt hơn so với dùng các list, bởi vì việc thêm một tuple vào một set sẽ không phát sinh thao tác sao chép, điều này sẽ giảm khả năng phát sinh lỗi. Ngoài ra, các thao tác trên set như tìm kiếm, xoá tiêu tốn ít thời gian hơn, nên nó hiệu quả hơn. Tuy nhiên, set vẫn gặp vấn đề giống như list là không mềm dẻo khi lưu trữ nhiều thông tin hơn.

List các list

Một số người sẽ dùng list các list để cài đặt một ma trận, trong đó tại mỗi vị trí trong ma trận sẽ tương ứng với một ô của căn phòng, nó sẽ nhận giá trị True hoặc False để thể hiện ô đó là sạch hay không. Ví dụ:
[[True, True, True, True, True, True],
[True, True, True, True, True, True],
...
True, True, True, True, True, True]]

Tuy nhiên, giải pháp này đòi hỏi phải có hai chỉ số để truy cập vào list. Ngoài ra, đầu là chỉ số dòng, đâu là chỉ số cột cũng gây ra một chút lúng túng.

Dictionary

Cách tự nhiên hơn để giải quyết vấn đề này là sử dụng dictionary (cấu trúc từ điển), trong đó, thành phần khoá (key) là một tuple (x, y) thể hiện vị trí của ô, thành phần giá trị (value) là một giá trị luận lý, với True thể hiện ô sạch.

Cách này mềm dẻo hơn khi chúng ta muốn lưu trữ nhiều dạng thông tin tình trạng hơn như "sạch", "dơ", "hơi bẩn". Chúng ta có thể dùng một số nguyên trong tập {1, 2, 3} cho thành phần giá trị (value), hoặc thậm chí có thể sử dụng chính các chuỗi trên.

Kết luận

Trong môn này, bạn học cách sử dụng cấu trúc dữ liệu một cách thích hợp nhất với các vấn đề đang đề cập. Do đó, trong tương lai, hãy nghĩ kỹ về cách lưu trữ tốt nhất cho dữ liệu của bạn trước khi lựa chọn một cấu trúc dữ liệu.