Chương 15: Lớp và đối tượng

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

Các kiểu dữ liệu do người dùng định nghĩa

Ta đã dùng nhiều kiểu dữ liệu có sẵn trong Python; bây giờ ta sẽ định nghĩa một kiểu mới. Lấy ví dụ, ta sẽ tạo ra một kiểu gọi là Point để biểu diễn một điểm trong không gian hai chiều.

Theo ngôn ngữ toán học, điểm thường được viết trong một cặp ngoặc đơn với dấu phẩy ngăn cách giữa các tọa độ. Chẳng hạn, (0, 0) biểu diễn gốc tọa độ, và (x, y) biểu diễn điểm x đơn vị về bên tay phải và y đơn vị độ dài lên phía trên so với điểm gốc.

Có một số cách để diễn tả một điểm bằng Python:

  • Ta có thể lưu giữ các tọa độ một cách riêng rẽ trong hai biến, xy.
  • Ta có thẻ lưu giữ các tọa độ như những phần tử trong một danh sách hoặc một bộ.
  • Ta có thể tạo ra một kiểu mới để diễn tả điểm như những đối tượng.

Việc tạo ra một kiểu mới sẽ phức tạp hơn (một chút) so với những cách còn lại, nhưng những ưu điểm của nó sẽ sớm trở nên rõ ràng sau này.

Một kiểu dữ liệu do người dùng định nghĩa được gọi là lớp. Một định nghĩa lớp sẽ có dạng như sau:

class Point(object):
    """represents a point in 2-D space"""

Đoạn đầu này cho thấy lớp mới này là một Point, vốn là một kiểu object, và là một kiểu có sẵn.

Phần thân là một chuỗi ghi chú để giải thích mục đích của lớp này. Bạn có thể định nghĩa các biến và hàm bên trong một định nghĩa lớp, nhưng ta sẽ trở lại điều này sau.

Việc định nghĩa một lớp tên là Point sẽ tạo ra một đối tượng lớp.

>>> print Point
<class '__main__.Point'>

Point được định nghĩa ở cấp cao nhất, “tên đầy đủ” của nó là __main__.Point.

Đối tượng lớp giống như một xưởng chế tạo ra các đối tượng. Để tạo ra một Point, bạn gọi Point như thể nó là một hàm.

>>> blank = Point()
>>> print blank
<__main__.Point instance at 0xb7e9d3ac>

Giá trị trả về là một tham chiếu tới một đối tượng Point, mà ta gán bằng blank. Việc tạo ra đối tượng mới được gọi là tạo cá thể, và đối tượng là một cá thể của lớp này.

Khi bạn in ra một cá thể, Python sẽ nói cho bạn biết cá thể này thuộc lớp nào và nó được lưu vào đâu trong bộ nhớ (đoạn đầu 0x nghĩa là số tiếp sau sẽ tính theo hệ thập lục phân).

Thuộc tính

Bạn có thể gán các giá trị cho một cá thể bằng cách dùng kí hiệu dấu chấm:

>>> blank.x = 3.0
>>> blank.y = 4.0

Cú pháp này cũng giống như khi ta viết lệnh chọn một biến từ một module, như math.pi hoặc string.whitespace. Tuy vậy, ở trường hợp này, ta đang gán các giá trị cho các phần tử được đặt tên của một đối tượng. Những phần tử này được gọi là thuộc tính.

Trong tiếng Anh, thuộc tính là danh từ, được phát âm “AT-trib-ute” với trọng âm đặt vào âm tiết đầu, khác với từ “a-TRIB-ute”, là một động từ.

Sơ đồ sau cho thấy kết quả của các phép gán này. Một sơ đồ trạng thái chỉ ra đối tượng và các thuộc tính của nó được gọi là sơ đồ đối tượng:

đối tượng Point

Biến blank chỉ đến một đối tượng Point, vốn chứa hai thuộc tính. Mỗi thuộc tính lại chỉ đến một số có phần thập phân.

Bạn có thể đọc giá trị của thuộc tính bằng cú pháp tương tự:

>>> print blank.y
4.0

>>> x = blank.x
>>> print x
3.0

Biểu thức blank.x nghĩa là, “Đến đối tượng mà blank chỉ tới và lấy giá trị của x”. Trong trường hợp này, ta gán giá trị cho một biến tên là x. Không hề có sự xung đột giữa biến x và thuộc tính x.

Bạn có thể dùng kí hiệu dấu chấm như một phần của biểu thức bất kì. Chẳng hạn:

>>> print '(%g, %g)' % (blank.x, blank.y)
(3.0, 4.0)
>>> distance = math.sqrt(blank.x**2 + blank.y**2)
>>> print distance
5.0

Bạn có thể truyền một cá thể với vai trò của đối số theo cách thông thường. Chẳng hạn:

def print_point(p):
    print '(%g, %g)' % (p.x, p.y)

print_point nhận vào đối số là một điểm và hiển thị nó theo kiểu kí hiệu toán học. Để sử dụng hàm này, bạn có thể truyền blank như một đối số:

>>> print_point(blank)
(3.0, 4.0)

Bên trong hàm, p là một tên khác của blank, vì vậy nếu hàm làm thay đổi p thì blank cũng thay đổi theo.

Hãy viết một hàm tên là distance nhận vào hai điểm làm đối số và và trả lại khoảng cách giữa hai điểm đó.

Hình chữ nhật

Đôi khi có thể dễ thấy rằng các thuộc tính của một đối tượng phải là gì, nhưng có những trường hợp bạn phải quyết định. Chẳng hạn, hãy hình dung rằng bạn cần thiết kế một lớp để biểu diễn hình chữ nhật. Bạn sẽ sử dụng những yếu tố gì để đặc trưng cho vị trí và kích thước của một hình chữ nhật. Để đơn giản, ta không xét đến góc quay của hình chữ nhật và coi rằng nó được đặt thẳng đứng hay nằm ngang.

Có ít nhất là hai khả năng sau đây:

  • Bạn có thể chỉ định một đỉnh (điểm góc) của hình chữ nhật (hoặc tâm điểm), bề rộng, và chiều cao.
  • Bạn có thể chỉ định hai đỉnh đối diện.

Đến đây ta khó nói trước rằng cách nào tốt hơn, vì vậy để làm ví dụ ta sẽ làm theo cách thứ nhất.

Sau đây là định nghĩa lớp:

class Rectangle(object):
    """represent a rectangle. 
       attributes: width, height, corner.
    """

Chuỗi chú thích liệt kê các thuộc tính: widthheight là các số; corner là một đối tượng điểm chỉ định đỉnh góc trái phía dưới.

Để biểu diễn một hình chữ nhật, bạn phải tạo cá thể thuộc đối tượng Rectangle và gán giá trị cho thuộc tính:

box = Rectangle()
box.width = 100.0
box.height = 200.0
box.corner = Point()
box.corner.x = 0.0
box.corner.y = 0.0

Biểu thức box.corner.x có nghĩa là, “Đến đối tượng mà box chỉ đến và chọn lấy thuộc tính có tên là corner; sau đó đến đối tượng đó và chọn lấy thuộc tính có tên là x”.

Hình vẽ sau cho thấy trạng thái của đối tượng này:

đối tượng Rectangle

Một đối tượng được gọi là được nhúng nếu nó là thuộc tính của một đối tượng khác.

Cá thể với vai trò là giá trị trả về

Các hàm có thể trả về các cá thể. Chẳng hạn, find_center nhận vào đối số là một Rectangle và trả lại một Point trong đó chứa tọa độ của tâm điểm Rectangle:

def find_center(box):
    p = Point()
    p.x = box.corner.x + box.width/2.0
    p.y = box.corner.y + box.height/2.0
    return p

Sau đây là một ví dụ trong đó truyền box như một đối số và gán Point thu được cho center:

>>> center = find_center(box)
>>> print_point(center)
(50.0, 100.0)

Bạn có thể thay đổi trạng thái của một đối tượng bằng cách thực hiện phép gán với một thuộc tính của nó. Chẳng hạn, để điều chỉnh kích thước của hình chữ nhật mà không làm dịch chuyển nó, bạn có thể thay đổi các giá trị của widthheight:

box.width = box.width + 50
box.height = box.width + 100

Bạn cũng có thể viết các hàm để thay đổi đối tượng. Chẳng hạn, grow_rectangle nhận vào một đối tượng Rectangle và hai số, dwidthdheight, rồi cộng các số này với bề rộng và chiều cao của hình chữ nhật:

def grow_rectangle(rect, dwidth, dheight) :
    rect.width += dwidth
    rect.height += dheight

Sau đây là một ví dụ minh họa tác dụng của hàm này:

>>> print box.width
100.0

>>> print box.height
200.0
>>> grow_rectangle(box, 50, 100)
>>> print box.width
150.0
>>> print box.height
300.0

Ở trong hàm, rect là một tên khác của box, vì vậy nếu hàm làm thay đổi rect, box cũng thay đổi theo.

Hãy viết một hàm có tên move_rectangle nhận vào một Rectangle và hai số tên là dxdy. Hàm này cần thay đổi vị trí của hình chữ nhật bằng cách cộng dx với tọa độ x của corner và cộng dy với tọa độ y của corner.

Sao chép

Việc dùng tham chiếu bội có thể làm chương trình khó đọc vì sự thay đổi ở một chỗ có thể gây ảnh hưởng không ngờ được ở chỗ khác. Thật khó có thể theo dõi được tất cả các biến có khả năng chỉ đến cùng một đối tượng.

Sao chép một đối tượng thường là cách làm thay thế cho tham chiếu bội. Module copy có một hàm tên là copy có thể sao chép bất kì đối tượng nào:

>>> p1 = Point()
>>> p1.x = 3.0

>>> p1.y = 4.0

>>> import copy
>>> p2 = copy.copy(p1)

p1p2 chứa dữ liệu giống nhau, nhưng chúng không cùng là một điểm (Point).

>>> print_point(p1)
(3.0, 4.0)
>>> print_point(p2)
(3.0, 4.0)

>>> p1 is p2
False
>>> p1 == p2
False

Toán tử is cho thấy rằng p1p2 là các đối tượng khác nhau, như chúng ta mong đợi. Nhưng có lẽ bạn cũng mong đợi rằng == sẽ cho kết quả True vì những điểm này có số liệu như nhau. Nếu vậy, bạn sẽ thất vọng khi biết rằng đối với các thể, toán tử == sẽ hoạt động mặc định như toán tử is; nó kiểm tra sự đồng nhất của hai đối tượng, chứ không phải sự tương đương giữa chúng. Tính năng này vẫn có thể thay đổi được—sau này ta sẽ biết cách thay đổi.

Nếu dùng copy.copy để tạo bản sao cho một Rectangle, bạn sẽ thấy rằng nó sao chép đối tượng Rectangle nhưng không sao chép đối tượng Point được nhúng trong đó.

>>> box2 = copy.copy(box)
>>> box2 is box
False
>>> box2.corner is box.corner
True

Sơ đồ đối tượng sẽ có dạng sau:

đối tượng Rectangle (2)

Thao tác trên được gọi là sao chép nông bởi vì nó chỉ sao chép đối tượng và các tham chiếu trong đó, mà không sao chép các đối tượng nhúng.

Trong phần lớn các trường hợp, đây không phải là điều bạn muốn. Ở ví dụ này, việc gọi grow_rectangle từ một trong hai Rectangle sẽ không làm ảnh hưởng đến đối tượng kia, nhưng move_rectangle từ một đối tượng sẽ làm ảnh hưởng đến cả hai! Hiện tượng này có thể làm ta bối rối và dễ gây lỗi.

Thật may là module copy có một phương thức tên là deepcopy để sao chép không chỉ bản thân đối tượng mà còn cả những đối tượng mà nó chỉ đến, rồi cả những đối tượng mà chúng chỉ đến, và cứ như vậy. Không ngạc nhiên khi người ta gọi thao tác này là sao chép sâu.

>>> box3 = copy.deepcopy(box)
>>> box3 is box
False
>>> box3.corner is box.corner
False

box3box là các đối tượng hoàn toàn riêng biệt.

Hãy viết một dạng của move_rectangle để tạo ra và trả về một Rectangle mới thay vì sửa lại cá thể cũ.

Gỡ lỗi

Khi bắt tay làm việc với các đối tượng, bạn có thể sẽ gặp một số biệt lệ mới. Nếu bạn thử truy cập một thuộc tính không có sẵn, bạn sẽ nhận AttributeError:

>>> p = Point()
>>> print p.z
AttributeError: Point instance has no attribute 'z'

Nếu không biết rõ kiểu của một đối tượng nào đó, bạn có thể hỏi:

>>> type(p)
<type '__main__.Point'>

Nếu không biết rõ liệu một đối tượng có một thuộc tính nào đó, bạn có thể dùng hàm có sẵn hasattr:

>>> hasattr(p, 'x')
True
>>> hasattr(p, 'z')
False

Đối số thứ nhất có thể là một đối tượng bất kì; đối số thứ hai là một chuỗi bao gồm tên của thuộc tính.

Thuật ngữ

lớp:
Kiểu dữ liệu do người dùng định nghĩa. Lời định nghĩa lớp sẽ tạo ra một đối tượng lớp mới.

đối tượng lớp:
Đối tượng trong đó bao gồm thông tin về một kiểu do người dùng định nghĩa. Đối tượng lớp có thể được dùng để tạo ra các cá thể của kiểu này.

cá thể:
Đối tượng thuộc về một lớp.

thuộc tính:
Một trong các giá trị được đặt tên gắn với một đối tượng.

nhúng (đối tượng):
Đối tượng được lưu trữ như một thuộc tính của một đối tượng khác.

sao chép nông:
Việc sao chép nội dung của một đối tượng, bao gồm cả những tham chiếu đến các đối tượng nhúng; được thực hiện bởi hàm copy trong module copy.

sao chép sâu:
Việc sao chép nội dung của một đối tượng cùng tất cả đối tượng nhúng nếu có, và các đối tượng nhúng bên trong chúng, và cứ như vậy; được thực hiện bởi hàm deepcopy trong module copy.

sơ đồ đối tượng:
Sơ đồ biểu diễn các đối tượng, thuộc tính của chúng và các giá trị của thuộc tính.

Bài tập

World.py trong Swampy (see Chapter {turtlechap}) có chứa định nghĩa lớp cho một kiểu người dùng định nghĩa tên là World. Bạn có thể nhập nó như sau:

from World import World

Phiên bản này của lệnh import nhập vào lớp World từ module World. Đoạn mã lệnh sau tạo ra một đối tượng World và gọi phương thức mainloop, để đợi hoạt động từ phía người dùng.

world = World()
world.mainloop()

Một cửa sổ sẽ xuất hiện với thanh tiêu đề và một hình vuông trống không. Ta sẽ dùng cửa sổ này để vẽ các điểm, hình chữ nhật, và các hình khác. Hãy thêm các dòng lệnh sau đây trước khi gọi mainloop và chạy lại chương trình.

canvas = world.ca(width=500, height=500, background='white')
bbox = [[-150,-100], [150, 100]]
canvas.rectangle(bbox, outline='black', width=2, fill='green4')

Bạn sẽ thấy một hình chữ nhật màu xanh lá cây với viền đen. Dòng lệnh thứ nhất tạo ra Canvas, vốn xuất hiện như một hình vuông màu trắng trên cửa sổ. Đối tượng Canvas có các phương thức như rectangle để vẽ nhiều hình dạng khác nhau.

bbox là một danh sách chứa các danh sách biểu diễn “hình bao” của hình chữ nhất. Cặp tọa độ thứ nhất là của đỉnh góc dưới bên trái; cặp thứ hai của đỉnh góc phía trên bên phải.

Bạn có thể vẽ một đường tròn như sau:

canvas.circle([-25,0], 70, outline=None, fill='red')

Tham số thứ nhất là cặp tọa độ tâm của đường tròn; tham số thứ hai là bán kính.

Nếu bạn thêm dòng này vào chương trình, kết quả thu được sẽ giống như quốc kì của Bangladesh (xem wikipedia.org/wiki/Gallery_of_sovereign-state_flags).

  1. Hãy viết một hàm tên là draw_rectangle nhận vào một Canvas và Rectangle làm các đối số và vẽ hình biểu diễn Rectangle trên Canvas.
  2. Hãy bổ sung một thuộc tính tên là color cho các đối tượng Rectangle và sửa đổi draw_rectangle sao cho nó dùng thuộc tính màu này làm màu tô.
  3. Hãy viết một hàm tên là draw_point nhận vào một Canvas và một Point làm các đối số và vẽ hình biểu diễn Point trên Canvas.
  4. Hãy định nghĩa một lớp mới có tên là Circle với các thuộc tính phù hợp rồi tạo ra một số cá thể thuộc đối tượng Circle. Hãy viết một hàm tên là draw_circle để vẽ các Circle trên Canvas.
  5. Hãy viết một chương trình để vẽ hình quốc kì của nước Cộng hòa Séc. Gợi ý: bạn có thể vẽ một hình đa giác như sau:
    points = [[-150,-100], [150, 100], [150, -100]]
    canvas.polygon(points, fill='blue')
    

Tôi đã viết một chương trình ngắn nhằm liệt kê các màu sẵn có; bạn có thể tải nó về từ thinkpython.com/code/color_list.py.

3 bình luận

Filed under Think Python

3 responses to “Chương 15: Lớp và đối tượng

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

  2. Pingback: Danh sách liên kết | Blog của Chiến

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

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