Chương 4: Nghiên cứu cụ thể: thiết kế giao diện

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

TurtleWorld

Kèm theo cuốn sách này, tôi có viết một bộ module có tên là Swampy. Một trong những module này là TurtleWorld; nó cung cấp một nhóm các hàm phục vụ cho việc vẽ các đường nét bằng cách điều khiển những “con rùa” chạy trên màn hình.

Bạn có thể tải về Swampy từ thinkpython.com/swampy; và thực hiện theo những chỉ dẫn cần thiết để cài đặt Swampy vào máy của mình.

Hãy chuyển đến thư mục có chứa TurtleWorld.py, tạo ra một file có tên polygon.py và gõ vào đoạn mã lệnh sau:

from TurtleWorld import *

world = TurtleWorld()
bob = Turtle()
print bob

wait_for_user()

Dòng đầu tiên là một dạng của lệnh import mà ta đã gặp; thay vì tạo ra một đối tượng module, nó trực tiếp nhập vào các hàm có trong module đó, từ đó bạn có thể truy cập chúng mà không cần dùng kí hiệu dấu chấm.

Dòng kế tiếp tạo ra một đối tượng TurtleWorld để gán vào world và một đối tượng Turtle gán vào bob. Việc in ra bob bằng lệnh print sẽ cho ta thông tin kiểu như:

<TurtleWorld.Turtle instance at 0xb7bfbf4c>

Điều này có nghĩa là bob tham chiếu đến một cá thể của Turtle được định nghĩa trong module TurtleWorld. Trong trường hợp này, “cá thể” có nghĩa là thành viên của một tập hợp; cá thể kiểu Turtle này là một trong số các cá thể của tập hợp các Turtle.

wait_for_user lệnh cho TurtleWorld đợi người dùng thực hiện một thao tác, dù trong trường hợp này người dùng không có nhiều lựa chọn khác ngoài việc đóng cửa sổ.

TurtleWorld cung cấp một số hàm phục vụ cho việc “lái” “con rùa”: fdbk để đi tiến và lùi, ltrt để rẽ trái và rẽ phải. Ngoài ra, mỗi con rùa (đối tượng Turtle) đều nắm một cây bút, vốn lại có thể được nhấc hoặc hạ; nếu hạ bút xuống, rùa sẽ để lại nét vẽ khi nó di chuyển. Các hàm pupd tương ứng với nhấc bút hoặc hạ bút.

Để vẽ một góc vuông, hãy thêm các dòng lệnh dưới đây vào chương trình (sau khi tạo ra bob và trước khi gọi wait_for_user):

fd(bob, 100)
lt(bob)
fd(bob, 100)

Dòng đầu tiên nhằm lệnh cho bob đi tiến 100 bước. Dòng lệnh thứ hai bảo nó rẽ trái.

Khi chạy chương trình này, bạn sẽ thấy bob di chuyển trước hết theo hướng đông, và sau đó theo hướng nam, để lại sau nó hai đoạn thẳng.

Bây giờ hãy chỉnh sửa chương trình để vẽ một hình vuông. Xin đừng đọc tiếp trước khi bạn hoàn thành chương trình này!

Cách lặp lại đơn giản

Đôi khi bạn viết mã lệnh kiểu như sau (ở đây không nói đến đoạn lệnh dùng để khởi tạo TurtleWorld và đợi người sử dụng):

fd(bob, 100)
lt(bob)

fd(bob, 100)
lt(bob)

fd(bob, 100)
lt(bob)

fd(bob, 100)

Chúng ta có thể làm việc này bằng cách viết gọn hơn với một lệnh for. Hãy thêm ví dụ sau đây vào polygon.py và chạy lại chương trình:

for i in range(4):
    print 'Hello!'

Bạn sẽ thấy kết quả gióng như:

Hello!
Hello!
Hello!
Hello!

Đây là cách dùng lệnh for đơn giản nhất; sau này chúng ta sẽ tìm hiểu thêm. Nhưng chỉ bằng cách đơn giản này cũng đủ để viết lại chương trình vẽ hình vuông. Xin đừng đọc tiếp trước khi bạn hoàn thành chương trình.

Đây là cách dùng một lệnh for để vẽ hình vuông:

for i in range(4):
    fd(bob, 100)
    lt(bob)

Cú pháp của lệnh for cũng tương tự như một định nghĩa hàm. Nó gồm có một phần đầu kết thúc bởi dấu hai chấm và một phần thân được viết thụt vào so với lề. Phần thân có thể chứa bao nhiêu câu lệnh cũng được.

Một câu lệnh for đôi khi còn đươc gọi là một vòng lặp vì dòng thực hiện sẽ đi xuôi theo phần thân và vòng ngược trở lại đầu. Trong ví dụ trên, phần thân được chạy qua bốn lần.

Thực ra phiên bản mã lệnh này hơi khác với các bản vẽ hình vuông trước đó ở chỗ nó thực hiện một lần rẽ nữa sau khi vẽ cạnh cuối cùng của hình vuông. Lần rẽ dư thừa này làm thời gian chạy lâu hơn một chút, nhưng nó đơn giản hoá mã lệnh nếu chúng ta có thể thực hiện các công việc gióng nhau bằng vòng lặp. Phiên bản này cũng có tác dụng đưa con rùa về trạng thái khởi đầu, đặt nó về hướng xuất phát.

Bài tập

Tiếp theo đây là một loạt các bài tập có sử dụng TurtleWorld. Chúng có mục đích riêng ngoài việc giải trí. Khi làm các bài tập này, bạn hãy nghĩ xem mục đích riêng đó là gì.

Bên cạnh các bài tập còn có lời giải. Bạn hãy cố hoàn thành (hoặc ít nhất là nỗ lực làm) các bài tập trước khi xem qua lời giải này.

  1. Hãy viết một hàm có tên square nhận một tham biến tên t, vốn là một con rùa. Hàm này dùng con rùa để vẽ một hình vuông.

    Viết một hàm nhằm mục đích chuyển bob như một đối số cho square, và sau đó chạy lại chương trình.

  2. Thêm một tham biến nữa có tên length cho square. Sửa đổi phần thân của sao cho độ dài của các cạnh là length, và sau đó sửa đổi gọi hàm để cung cấp cho một đối số thứ hai. Chạy lại chương trình. Thử chương trình với một loạt các giá trị của length.
  3. Các hàm ltrt đều mặc định thực hiện rẽ 90 độ, nhưng bạn có thể cung cấp đối số thứ hai chứa số độ. Chẳng hạn, lt(bob, 45) thực hiện rẽ bob 45 độ về bên trái.

    Tạo một bản sao của square và đổi tên thành polygon. Thêm một tham số có tên là n và sửa đổi phần thân để nó vẽ một hình đa giác đều có n cạnh. Gợi ý: Các góc ngoài của một hình n-giác đều cùng bằng 360. 0 / n độ.

  4. Viết một hàm có tên là circle để điều khiển rùa t, và bán kính, r, như là hai tham số. Hàm này thực hiện vẽ gần chính xác một đường tròn bằng cách gọi hàm polygon với các gía trị phù hợp cho chiều dài cạnh và số cạnh. Thử lại hàm của bạn với một loạt các giá trị của r.

    Gợi ý: hình dung ra độ dài chu vi đường tròn và đảm bảo rằng length * n = circumference (chu vi).

    Một gợi ý khác: nếu bob tỏ ra quá chậm, bạn có thể tăng tốc độ bằng cách thay đổi bob.delay, vốn là thời gian giữa các lần dịch chuyển tính theo giây. bob.delay = 0.01 có thể sẽ đủ nhanh.

  5. Viết một bản tổng quát hơn so với circle gọi là arc, trong đó nhận thêm một tham biến angle, nhằm chỉ định bao nhiêu phần đường tròn cần được vẽ. angle có đơn vị là độ, vì vậy khi angle=360, arc sẽ vẽ một đường tròn.

Bao bọc

Bài tập thứ nhất yêu cầu bạn đặt đoạn mã vẽ hình vuông vào trong một định nghĩa hàm và sau đó gọi hàm này, trong đó chuyển con rùa như một tham biến. Một cách làm là như sau:

def square(t):
    for i in range(4):
        fd(t, 100)
        lt(t)

square(bob)

Các câu lệnh trong cùng, fdlt được thụt lề hai lần, nhằm cho thấy chúng ở bên trong vòng lặp for, vốn bản thân ở trong định nghĩa hàm. Dòng tiếp theo, square(bob), lại bắt đầu từ lề trái, đó chính là chỗ kết thúc của cả vòng lặp for và định nghĩa hàm.

Bên trong hàm, t tham chiếu đến cùng con rùa như bob tham chiếu, vì vậy lt(t) có cùng ý nghĩa như lt(bob). Vậy tại sao không đặt tên tham biến là bob? Đó là vì t có thể là bất cứ con rùa nào chứ không riêng gì bob, và bạn có thể tạo ra một con rùa thứ hai và chuyển nó như một đối số của square:

ray = Turtle()
square(ray)

Việc gói một đoạn mã vào trong một hàm được gọi là bao bọc. Một trong những ưu điểm của việc bao bọc là nó gắn đoạn mã với một tên cụ thể, chính là một kiểu giúp cho việc biên khảo sau này. Một ưu điểm khác là nếu bạn sử dụng lại đoạn mã, việc gọi tên hàm sẽ ngắn gọn hwon nhiều so với việc sao chép và dán toàn bộ phần thân hàm!

Khái quát hoá

Bước tiếp theo là thêm một tham biến length vào square. Sau đây là một giải pháp:

def square(t, length):
    for i in range(4):
        fd(t, length)
        lt(t)

square(bob, 100)

Việc thêm một tham số vào một hàm được gọi là khái quát hoá vì nó làm cho hàm số trở nên khái quát hơn: trong phiên bản trước, kích thước của hình vuông là cố định, ở phiên bản này nó có thể lớn nhỏ bất kì.

Bước tiếp theo cũng là một cách khái quát hoá. Thay vì việc vẽ hình vuông, polygon vẽ một hình đa giác đều với số cạnh bất kì. Sau đây là một lời giải:

def polygon(t, n, length):
    angle = 360.0 / n
    for i in range(n):
        fd(t, length)
        lt(t, angle)

polygon(bob, 7, 70)

Đoạn mã trên thực hiện vẽ thất giác đều với mỗi cạnh dài bằng 70. Nếu bạn có nhiêu tham biến hơn, sẽ rất dễ quên ý nghĩa của từng tham biến cũng như thứ tự của chúng. Việc đưa vào tên của các tham biến trong danh sách đối số là hợp lệ và thậm đôi khi là cần thiết:

polygon(bob, n=7, length=70)

Các tên này được gọi là tham biến từ khoá vì chúng bao gồm cả tên các tham biến đóng vai trò “từ khoá” (không nên nhầm với các từ khoá dành riêng trong Python như whiledef).

Cú pháp này giúp cho chương trình trở nên dễ đọc hơn. Nó cũng giúp bạn nhớ được rằng các đối số và tham biến hoạt động thế nào: khi bạn gọi một hàm, các đối số được gán cho các tham biến.

Thiết kế giao diện

Bước tiếp theo là viết circle, trong đó nhận một tham biến là bán kính r. Sau đây là một lời giải đơn giản có sử dụng polygon để vẽ đa giác đều 50 cạnh:

def circle(t, r):
    circumference = 2 * math.pi * r
    n = 50
    length = circumference / n
    polygon(t, n, length)

Dòng đầu tiên nhằm tính toán chu vi của đường tròn có bán kính r theo công thức r. Vì ta dùng math.pi nên cần phải nhập math. Để cho tiện, các câu lệnh import thường được đặt ở đầu đoạn mã lệnh.

n là số đọan thẳng để vẽ gần đúng đường tròn, sao cho length là chiều dài mỗi đoạn. Vì vậy, polygon sẽ vẽ một đa giác đều có 50 cạnh gần khớp với một đường tròn có bán kính r.

Một hạn chế của lời giải này là n là một hằng số; điều đó có nghĩa là với những đường tròn lớn, các đoạn thẳng sẽ rất dài, và với những đường tròn nhỏ, chúng ta mất thời gian để vẽ quá nhiều đoạn thẳng ngắn. Một giải pháp là khái quát hoá hàm này bằng cách nhận n làm tham số. Điều này giúp cho người dùng (khi gọi circle) có quyền lựa chọn tốt hơn, nhưng giao diện của chương trình vì thế cũng kém phần trong sáng.

Giao diện của một hàm là phần tóm tắt cách dùng hàm đó: các tham biến là gì? Hàm được viết nhằm mục đích gì? Và giá trị được trả lại là gì? Một giao diện “trong sáng” có nghĩa là nó “đơn giản nhất tới mức có thể, nhưng không được đơn giản hơn.1

Ở ví dụ này, r phải thuộc về giao diện vì nó chi phối đường tròn cần được vẽ. Còn n thì ít có lí hơn vì nó liên quan đến những chi tiết gắn với cách vẽ đường tròn đó.

Thay vì việc làm lộn xộn giao diện, tốt hơn là ta chọn một giá trị hợp lí cho n tuỳ thuộc vào chu vi circumference:

def circle(t, r):
    circumference = 2 * math.pi * r
    n = int(circumference / 3) + 1
    length = circumference / n
    polygon(t, n, length)

Bây giờ số cạnh xấp xỉ bằng circumference/3, như vậy mỗi cạnh có độ dài xấp xỉ bằng 3, tức là đủ nhỏ để cho đường tròn được đẹp, nhưng cũng đủ lớn để mã lệnh được hiệu quả, và phù hợp với mọi kích cỡ đường tròn.

Chỉnh đốn

Khi viết circle, tôi có thể dùng polygon vì một đa giác đều nhiều cạnh có thể gần khớp với một đường tròn. Nhưng arc thì không phù hợp; ta không dùng được polygon hoặc circle để vẽ một cung tròn.

Một cách làm khác là bắt đầu bằng một bản sao của polygon và biến đổi nó về thành arc. Kết quả có thể là như sau:

def arc(t, r, angle):
    arc_length = 2 * math.pi * r * angle / 360
    n = int(arc_length / 3) + 1
    step_length = arc_length / n
    step_angle = float(angle) / n

    for i in range(n):
        fd(t, step_length)
        lt(t, step_angle)

Nửa sau của hàm này trông giống như polygon, nhưng ta không thể dùng lại polygon mà không thay đổi giao diện. Ta có thể khái quát hoá polygon để nhận vào tham biến thứ ba là góc, như khi đó polygon lại không còn là một tên gọi phù hợp nữa! Thay vào đó, hãy gọi hàm với tên polyline để khái quát hơn:

def polyline(t, n, length, angle):
    for i in range(n):
        fd(t, length)
        lt(t, angle)

Bây giờ ta có thể viết lại polygonarc có dùng polyline:

def polygon(t, n, length):
    angle = 360.0 / n
    polyline(t, n, length, angle)

def arc(t, r, angle):
    arc_length = 2 * math.pi * r * angle / 360
    n = int(arc_length / 3) + 1
    step_length = arc_length / n
    step_angle = float(angle) / n
    polyline(t, n, step_length, step_angle)

Sau cùng, ta có thể viết lại circle có dùng arc:

def circle(t, r):
    arc(t, r, 360)

Quá trình này—việc sắp xếp lại chương trình để cải thiện giao diện của hàm và giúp cho sử dụng lại mã lệnh— được gọi là chỉnh đốn. Trong trường hợp này, ta đã nhận thấy rằng có sự tương đồng trong mã lệnh của arcpolygon, vì vậy ta đã chỉnh đốn lại bằng cách đưa phần chung này vào trong polyline.

Nếu đã có kế hoạch từ trước, có thể ta đã viết polyline từ đầu và tránh việc chỉnh đố, nhưng thường thì vào thời điểm bắt đầu dự án bạn không biết rõ để thiết kế được toàn bộ giao diện. Một khi đã bắt tay vào viết mã lệnh, bạn hiểu hơn về vấn đề cần giải quyết. Đôi khi việc chỉnh đốn là một tín hiệu cho thấy bạn đã học được một điều gì đó.

Một kế hoạch phát triển

Một kế hoạch phát triển là một quá trình trong việc lập trình. Ở đây ta sẽ dùng kĩ thuât “bao bọc và khái quát hoá”. Các bước trong quá trình này gồm có:

  1. Bắt đầu bằng việc viết chương trình nhỏ mà không định nghĩa hàm.
  2. Một khi chương trình của bạn đã chạy, hãy đóng gói nó vào trong một hàm và đặt tên cho hàm này.
  3. Khái quát hoá hàm bằng cách thêm vào các tham số một cách thích hợp.
  4. Lặp lại các bước 1–3 đến khi bạn có một tập hợp các hàm hoạt động tốt. Hãy sao chép và dán các đoạn mã lệnh tốt đó để khỏi đánh máy lại (và gỡ lỗi lại).
  5. Tìm mọi cơ hội để cải thiện chương trình bằng cách chỉnh đốn. Chẳng hạn, nếu bạn có đoạn mã lệnh tương tự ở một vài chỗ trong chương trình, hãy xét xem có thể chỉnh đốn bằng việc đưa nó vào một hàm chung hay không.

Quá trình này có một số hạn chế—ta sẽ thấy các giải pháp khác trong phần sau quyển sách—nhưng có thể nó sẽ có ích nếu bạn không biết trước được việc chia chương trình thành các hàm như thế nào cho hợp lí. Phương pháp này giúp bạn thiết kế trong lúc bạn viết chương trình.

docstring

docstring (viết tắt của “documentation string”) là một chuỗi được đặt ở đầu một hàm có nhiệm vụ giải thích giao diện. Sau đây là một ví dụ:

def polyline(t, length, n, angle):
    """Vẽ n đoạn thẳng với chiều dài cho trước và góc
    (tính bằng độ) giữa chúng.  t là một Turtle.
    """    
    for i in range(n):
        fd(t, length)
        lt(t, angle)

Docstring này là một chuỗi đặt trong ba dấu nháy, cũng được gọi là chuỗi nhiều dòng vì ba dấu nháy cho phép chuỗi kéo dài qua nhiều dòng liên tiếp.

Tuy rất gọn gàng nhưng docstring này chứa đầy đủ tất cả những thông tin thiết yếu cho người cần dùng đến hàm này. Nó giải thích một cách cô đọng hàm này có nhiệm vụ gì (mà không nói chi tiết rằng hàm thực hiện bằng cách nào). Nó giải thích ảnh hưởng của mỗi tham biến đối với biểu hiện của hàm và mỗi tham biến có kiểu là gì (trong trường hợp không rõ ràng).

Việc ghi chép này là một phần quan trọng trong thiết kế giao diện. Một giao diện được thiết kế tốt phải là giao diện rất dễ diễn giải; nếu bạn gặp khó khăn khi giải thích các hàm mà bạn viết ra thì đó có thể là dấu hiệu cho thấy giao diện của bạn có thể phải được cải thiện.

Gỡ lỗi

Một giao diện cũng tựa như một giao kèo giữa hàm và chương trình gọi. Chương trình đồng ý cung cấp những tham biến nhất định còn hàm thì đồng ý thực hiện một việc nhất định.

Chẳng hạn, polyline đòi hỏi bốn đối số. Thứ nhất phải là một Turtle. Thứ hai phải là một số, và nó hẳn phải là một số dương, dù rằng hàm vẫn hoạt động nếu không phải là số dương. Đối số thứ ba phải là một số nguyên; nếu không thì range sẽ báo lỗi (điều này còn tùy vào phiên bản Python mà bạn đang dùng). Thứ tư phải là một số, ma ta hiểu là nó tính bằng độ.

Các yêu cầu trên được gọi là những điều kiện tiền đề vì chúng cần được đảm bảo là đúng trước khi hàm được thực hiện. Trái lại, các điều kiện ở cuối hàm được gọi là trạng thái cuối. Các trạng thái cuối gồm có những hiệu ứng được mong đợi của hàm (như việc vẽ các đoạn thẳng) và bất kì hiệu ứng phụ nào khác (như di chuyển Turtle hoặc thay đổi gì đó trong khung cảnh).

Các điều kiện tiền đề thuộc về trách nhiệm của chương trình gọi. Nếu chương trình vi phạm một điều kiện tiền đề (đã được viết rõ ở docstring) và hàm không thực hiện được việc, thì lỗi thuộc về chương trình gọi chứ không thuộc về hàm.

Thuật ngữ

cá thể:
Một thành viên của tập hợp. Trong chương này, TurtleWorld là một cá thể của tập hợp các TurtleWorld.

vòng lặp:
Một phần của chương trình được thực hiện lặp đi lặp lại.

bao bọc:
Quá trình đưa một danh sách các câu lệnh vào trong bên một định nghĩa hàm.

khái quát hoá:
Quá trình thay thế một thứ riêng biệt một cách không cần thiết (chẳng hạn một con số) với một thứ khái quát thích hợp (như một biến hoặc tham biến).

đối số từ khoá:
Một đối số bao gồm cả tên của đối số đó dưới hình thức của một “từ khoá”.

giao diện:
Một đoạn mô tả cách dùng một hàm, bao gồm tên và lời mô tả các đối số và giá trị được trả về.

kế hoạch phát triển:
Một quy trình để viết chương trình máy tính.

docstring:
Một chuỗi xuất hiện bên trong định nghĩa hàm nhằm ghi chép lại giao diện của hàm đó.

điều kiện tiền đề:
Một yêu cầu cần được thoả mãn bởi chương trình gọi trước khi hàm được thực hiện.

trạng thái cuối:
Một điều kiện cần được hàm thoả mãn trước khi nó kết thúc.

Bài tập

Tải về mã lệnh trong chương này từ địa chỉ thinkpython.com/code/polygon.py.

  1. Viết các docstring thích hợp cho polygon, arccircle.
  2. Vẽ một sơ đồ ngăn xếp trong đó chỉ ra trạng thái của chương trình khi chạy circle(bob, radius). Bạn có thể tính tay hoặc thêm vào các lệnh print kèm theo mã lệnh.
  3. Phiên bản arc trong Mục {refactor} không chính xác lắm vì cách xấp xỉ đoạn thẳng này luôn nằm ngoài đường tròn đúng. Do đó, con rùa đã dừng lại ở cách đích cuối cùng một vài điểm. Cách làm của tôi đã giảm đi sai lệch này. Hãy đọc mã lệnh và cố gắng hiểu nó. Bạn có thể vẽ biểu đồ và xem cơ chế hoạt động của cách này.

Viết một tập hợp tổng quát gồm các hàm để vẽ những bông hoa như sau:

hoa

Bạn có thể tải về một lời giải từ thinkpython.com/code/flower.py.

Viết một tập hợp tổng quát gồm các hàm để vẽ những hình như sau:

Bạn có thể tải về một lời giải từ thinkpython.com/code/pie.py.

Các chữ cái trong bảng chữ có thể được xây dựng từ một số đủ nhiều các thành phần cơ bản, như những đường thẳng đứng, đường ngang, và đường cong. Hãy thiết kế một bộ phông chữ mà có thể được vẽ với một số tốt thiểu các thành phần cơ bản như vậy; rồi viết các hàm thực hiện việc vẽ chữ cái.

Bạn nên từng hàm riêng cho mỗi chữ cái, với tên hàm như draw_a, draw_b, v.v., và đặt chung các hàm vào một file có tên là letters.py. Bạn có thể tải về một “bộ chữ turtle” từ thinkpython.com/code/typewriter.py để so sánh với mã lệnh của bạn.

Bạn có thể tải về một lời giải từ thinkpython.com/code/letters.py.


  1. Nguyên văn: “as simple as possible, but not simpler.” (Einstein)

7 phản hồi

Filed under Sách, Think Python

7 responses to “Chương 4: Nghiên cứu cụ thể: thiết kế giao diện

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

  2. Pingback: Chương 19: Nghiên cứu cụ thể: Tkinter | Blog của Chiến

  3. trần thọ

    anh ơi..dùng swampy để chạy chương trình polygon.py thế nào vậy…e đã cài swampy bằng easy install rồi mà không dùng được a! a giúp em với

  4. Pingback: Phụ lục: Lumpy | Blog của Chiến

Gửi phản hồi

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Log Out / Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Log Out / Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Log Out / Thay đổi )

Google+ photo

Bạn đang bình luận bằng tài khoản Google+ Log Out / Thay đổi )

Connecting to %s