Tư duy Tính toán

Lớp, Phương thức & Lớp con

Trường ĐH Công nghệ – Đại học Quốc gia Hà Nội

Nội dung

  1. Lớp (class) & hàm khởi tạo __init__
  2. Phương thức, tham số self
  3. Biểu diễn đối tượng bằng chuỗi: __repr__, __str__
  4. Kế thừa & phân cấp lớp trong Python
  5. Ghi đè (overriding)
  6. Kiểm tra tương đương (equivalence) với __eq__

Tài liệu đọc: Think Python 3rd ed., 15.{1,2,5,6}, 17.{1,2,5,8,9}

1.
Lớp (class) & hàm khởi tạo

Khái niệm class

  • Kiểu của một đối tượng chính là một lớp.
  • Một đối tượng là một instance của lớp đó.
  • Tên lớp dùng như một hàm khởi tạo (constructor function) để tạo đối tượng.
  • Lớp định nghĩa các method – hàm “đi kèm” đối tượng.

Ví dụ: lớp điểm 3D


class Point3:
    pass

p = Point3()
p.x = 2
p.y = 3
p.z = 5
          
  • Đây là lớp đơn giản nhất: chỉ có tên, chưa có thân.
  • Gọi Point3() để tạo đối tượng.
  • Gán thuộc tính sau khi tạo: p.x, p.y, p.z.

Khởi tạo thuộc tính thủ công – bất tiện


p = Point3()
p.x = 2
p.y = 3
p.z = 5

p2 = Point3()
p2.eks = 2
p2.z = 'five'
          
  • Dễ quên thuộc tính.
  • Dễ gõ sai tên thuộc tính (eks).
  • Dễ lưu giá trị sai kiểu (chuỗi thay vì số).

Hàm khởi tạo __init__


class Point3:
    def __init__(obj, x_val, y_val, z_val):
        obj.x = x_val
        obj.y = y_val
        obj.z = z_val

point1 = Point3(2, 3, 5)
          

Phương thức initializer đảm bảo mọi đối tượng được khởi tạo nhất quán.

Lưu ý: __init__ có dấu gạch dưới kép ở cả hai bên, còn gọi là dunder (double underscore).

Quy ước dùng self


class Point3:
    def __init__(self, x_val, y_val, z_val):
        self.x = x_val
        self.y = y_val
        self.z = z_val

point1 = Point3(2, 3, 5)
          
  • Python không bắt buộc tên self, nhưng đó là chuẩn cộng đồng.
  • Giúp người đọc hiểu ngay tham số đầu là “chính đối tượng”.

Ví dụ: lớp bài đăng mạng xã hội


class Post:
    def __init__(self, text):
        """A post whose text is `text`
        and with no likes so far."""
        self.text = text
        self.n_likes = 0

post1 = Post('good vibes')
          

Thường dùng cùng tên cho tham số & thuộc tính, ví dụ text.

Giao thức tạo đối tượng

Với lời gọi <ClassName>(<args>), Python làm:

  1. Tạo một đối tượng mới trong heap.
  2. Gán một identifier cho đối tượng.
  3. Gọi __init__:
    • Đối tượng mới là tham số đầu (self).
    • Các <args> còn lại truyền cho những tham số sau.
    • Hàm khởi tạo phải trả về None.
  4. Trả về identifier của đối tượng mới.

Tổng kết cú pháp lớp

Thành phần Cú pháp Ghi chú
Định nghĩa lớp class <ClassName>: ... Tên lớp thường viết hoa chữ đầu.
Initializer def __init__(self, ...): ... Tên bắt buộc là __init__, tham số đầu là self.

Tất cả phương thức đều có tên dunder?

Chắc chắn là không! Hãy nhớ lại một số phương thức của list sau:

Phương thức Mô tả
lst.index(item) Trả về chỉ số của lần xuất hiện đầu tiên của item trong lst.
lst.insert(i, item) Chèn item vào lst ngay trước phần tử tại vị trí i, các phần tử bên phải bị dời sang phải.
lst.append(item) Thêm item vào cuối lst.
lst1.extend(lst2) Nối tất cả phần tử của lst2 vào sau lst1.

Các phương thức dunder được dùng cho những nhiệm vụ đặc biệt, gắn với cơ chế riêng của Python.
Phần lớn phương thức ta tự định nghĩa sẽ không phải là dunder.

2.
Phương thức & tham số self

Phương thức là gì?

  • Về mặt cú pháp: hàm được định nghĩa bên trong lớp.
  • Về mặt khái niệm: “thông điệp” gửi đến đối tượng.
  • Đối tượng có thể:
    • Thực hiện hành vi (cập nhật trạng thái).
    • Hoặc trả lại một giá trị.

Ví dụ: bộ đếm Counter


class Counter:
    def __init__(self):
        self.count = 0

    def currCount(self):
        return self.count

    def incr(self):
        self.count = self.count + 1

    def reset(self):
        self.count = 0
          

Dùng Counter


>>> ctr = Counter()
>>> ctr.currCount()
0
>>> ctr.incr()
>>> ctr.currCount()
1
          

Gọi phương thức bằng cú pháp obj.method(...).

Phương thức dùng self


class Counter:
    def __init__(self):
        self.count = 0

    def currCount(self):
        return self.count

    def incr(self):
        self.count = self.count + 1

    def reset(self):
        self.count = 0
          

self luôn trỏ tới đối tượng nhận thông điệp.

Lời gọi phương thức = lời gọi hàm

  • Cú pháp: <obj>.<method>(<args>).
  • Thực chất tương đương:
    • <method>(<obj>, <args>)
  • Đó là lý do vì sao tham số đầu tiên phải là self.

Quên self sẽ ra sao?


class Counter:
    def __init__():
        self.count = 0
          

>>> ctr = Counter()
TypeError: Counter.__init__() takes 0 positional arguments but 1 was given
          

Python tự động truyền self; nếu quên khai báo tham số sẽ báo lỗi.

3.
Biểu diễn đối tượng khi in

In đối tượng ra màn hình


>>> Point3(2, 3, 5)
<__main__.Point3 object at 0x10377e990>

>>> range(0, 10, 2)
range(0, 10, 2)
          
  • Mặc định, đối tượng tự định nghĩa in ra dạng “<Class at address>”.
  • Muốn hiển thị đẹp hơn cho lập trình viên & người dùng?

Biểu diễn chuỗi: __repr__


class Point3:
    def __repr__(self):
        return 'Point3(' \
            + str(self.x) + ', ' \
            + str(self.y) + ', ' \
            + str(self.z) \
            + ')'
          

>>> Point3(2, 3, 5)
Point3(2, 3, 5)
          

__repr__ dành cho lập trình viên, dùng trong tương tác & debug.

Chuỗi cho người dùng: __str__


class Point3:
    def __str__(self):
        return '(' \
            + str(self.x) + ', ' \
            + str(self.y) + ', ' \
            + str(self.z) \
            + ')'
          

>>> str(Point3(2, 3, 5))
'(2, 3, 5)'
>>> print(Point3(2, 3, 5))
(2, 3, 5)
          
  • __repr__: cho lập trình viên.
  • __str__: cho người dùng cuối.

4.
Kế thừa & phân cấp lớp

Kế thừa (subclassing)

  • Subclass được định nghĩa dựa trên một lớp có sẵn (superclass).
  • Subclass chuyên biệt hóa superclass:
    • Thêm hành vi mới.
    • Hoặc tùy biến hành vi cũ.
  • Ví dụ:
    • Account → lớp cơ sở.
    • InterestAccount → lớp con (có lãi suất).

Cú pháp định nghĩa subclass


class Account:
    ...

class InterestAccount(Account):
    ...
          

Ý nghĩa: InterestAccount thừa hưởng toàn bộ phương thức từ Account.

Ví dụ: tài khoản có lãi


class Account:
    def __init__(self, owner, number):
        self._owner = owner
        self._number = number
        self._balance = 0

    def deposit(self, amount):
        self._balance += amount

    def printStatement(self):
        print('Owner: ' + self._owner)
        print('Account Number: ' + str(self._number))
        print('Balance: ' + str(self._balance))


class InterestAccount(Account):
    def addInterest(self, pct_rate):
        interest = self._balance * pct_rate
        self.deposit(interest)
          

Dùng InterestAccount


>>> acct = InterestAccount('Anne', '123')
>>> acct.deposit(1000)
>>> acct.addInterest(0.05)
>>> acct.printStatement()
Owner: Anne
Account Number: 123
Balance: 1050.0
          

Subclass sử dụng lại phương thức của superclass nhờ kế thừa.

Quy tắc bottom-up tìm phương thức

  1. Bắt đầu tìm trong class của đối tượng.
  2. Nếu không có, đi “lên trên” superclass.
  3. Tiếp tục cho đến khi tìm thấy hoặc hết lớp cha.
  4. Nếu không tìm thấy → ném AttributeError.

Gọi initializer của superclass


class Account:
    def __init__(self, owner, number):
        self._owner = owner
        self._number = number
        self._balance = 0.0


class CreditAccount(Account):
    def __init__(self, owner, number, limit):
        super().__init__(owner, number)
        self._limit = limit
          

super().__init__(...) khởi tạo phần trạng thái chung ở lớp cha.

Khi nào không cần viết lại __init__?


class Account:
    def __init__(self, owner, number):
        self._owner = owner
        self._number = number
        self._balance = 0.0


class DepositAccount(Account):
    # Không định nghĩa __init__ ở đây
    pass

acct = DepositAccount('Anne', '2546154123')
          

Quy tắc bottom-up: DepositAccount không có __init__ → dùng của Account.

Kế thừa tạo thành phân cấp lớp

Account __init__(self, owner, number) balance(self) printStatement(self) DepositAccount deposit(self, amount) InterestAccount addInterest(self, pct_rate) CreditAccount __init__(self, owner, number, limit) charge(self, amount)

Phân cấp lớp trong Python

  • Tất cả lớp đều (trực tiếp hoặc gián tiếp) kế thừa từ object.
  • Câu lệnh:
    
    class C:
        ...
                  
    thực chất tương đương:
    
    class C(object):
        ...
                  

Lớp gốc nhất là object

object Account DepositAccount InterestAccount CreditAccount

Lớp gốc object


class object:
    def __str__(self):
        # trả về chuỗi kiểu
        # '<class-name> object at <address>'
        ...
          

class C(object):
    ...
    # không định nghĩa __str__()
          

>>> c = C()
>>> str(c)
<__main__.C object at 0x...>
          

Nếu lớp ta không định nghĩa __str__, Python dùng bản mặc định của object.

Kiểm tra kiểu đối tượng

Hàm Mô tả Ví dụ
isinstance(obj, cls) Trả về True nếu obj là instance của cls hoặc bất kỳ subclass nào. isinstance(a, Account)
type(obj) Trả về lớp trực tiếp của đối tượng; không “leo” lên superclass. type(a) == InterestAccount

4.
Ghi đè phương thức (overriding)

Ghi đè (overriding)

  • Subclass định nghĩa lại một phương thức đã có ở superclass.
  • Theo quy tắc bottom-up, định nghĩa ở subclass ưu tiên.
  • Giúp tùy biến hành vi cho lớp con.

Ví dụ: ghi đè printStatement


class Account:
    def printStatement(self):
        print('Statement:')
        print('Account #: ' + self._number)
        print('Owner: ' + self._owner)
        print('Balance: $' + str(self._balance))


class CreditAccount(Account):
    def printStatement(self):
        print('Statement:')
        print('Account #: ' + self._number)
        print('Owner: ' + self._owner)
        print('Balance: $' + str(self._balance))
        print('Limit: $' + str(self._limit))
          

Dùng super() để tránh lặp code


class Account:
    def printStatement(self):
        print('Statement:')
        print('Account #: ' + self._number)
        print('Owner: ' + self._owner)
        print('Balance: $' + str(self._balance))


class CreditAccount(Account):
    def printStatement(self):
        super().printStatement()
        print('Limit: $' + str(self._limit))
          

Gọi lại phiên bản superclass rồi thêm thông tin riêng của subclass.

Ghi đè object.__str__


class Account:
    def __str__(self):
        return 'Account #' + self._number
          

Giờ khi dùng str(acct), ta nhận chuỗi thân thiện hơn thay vì địa chỉ bộ nhớ.

5.
Equivalence & toán tử ==

Toán tử == nghĩa là gì?


class Point3:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z
          

>>> Point3(0, 0, 0) == Point3(0, 0, 0)  # 2 đối tượng khác nhau
False
>>> 42 == 42
True
          

Hai điểm có cùng tọa độ nhưng lại không “bằng nhau” theo mặc định.

Equality vs Equivalence

Khái niệm Toán tử Câu hỏi
Equality is Hai biến có trỏ tới cùng một đối tượng trong bộ nhớ không?
Equivalence == Hai đối tượng có tương đương về nội dung/ý nghĩa không?

Ví dụ với kiểu built-in


def incr(n):
    return n + 1
          

>>> x = 1000
>>> y = incr(x) - 1
>>> x == y
True
>>> x is y
False
          

Python tùy biến == cho int để so sánh giá trị, không so sánh địa chỉ.

Mặc định __eq__ của object


class object:
    ...
    def __eq__(self, other):
        return (self is other)
          

Với lớp tự định nghĩa, mặc định == giống hệt is.

Tự định nghĩa __eq__


class Point3:
    ...
    def __eq__(self, other):
        """Same coordinates"""
        return type(other) == Point3 \
            and self.x == other.x \
            and self.y == other.y \
            and self.z == other.z
          

>>> Point3(0, 0, 0) == Point3(0, 0, 0)
True
          

Giờ equivalence cho Point3 là “cùng tọa độ”.

Python dịch == sang __eq__

  • Biểu thức <expr1> == <expr2> được dịch thành:
    • <expr1>.__eq__(<expr2>)
  • Nếu lớp của <expr2>subclass của lớp <expr1>, Python có thể ưu tiên dùng <expr2>.__eq__.
  • Nhờ overriding, ta tùy biến được khái niệm “tương đương” cho từng lớp.

Các toán tử khác cũng là override

Toán tử Phương thức dunder
== __eq__
!= __ne__
+ __add__
- __sub__
and __and__
or __or__
< __lt__
<= __le__
> __gt__
>= __ge__
… và nhiều toán tử khác

* Bạn không cần phải nhớ hết tên các phương thức này, chỉ cần nhớ __eq__ là đủ.

Tổng kết

  • Lớp (class):
    • Dùng __init__ để khởi tạo thuộc tính nhất quán.
    • Phương thức nhận tham số đầu là self.
  • Chuỗi biểu diễn đối tượng:
    • __repr__ cho lập trình viên.
    • __str__ cho người dùng.
  • Kế thừa & phân cấp lớp:
    • Subclass kế thừa hành vi từ superclass.
    • super() giúp gọi lại phương thức lớp cha.
  • Ghi đè & equivalence:
    • Overriding cho phép tùy biến hành vi (__str__, __eq__, ...).
    • Phân biệt is (cùng đối tượng) và == (tương đương).