Chương 05 · OOP in Python

Lập trình hướng đối tượng trong Python

Python không "tin" hoàn toàn vào OOP cứng kiểu Java. class trong Python là một namespace khả tạo instance — không có new, không có private thật sự, không có interface tách rời. Thay vào đó, Python có dunder method (cho phép class hòa nhập syntax ngôn ngữ), duck typing (hành vi quan trọng hơn lớp), MRO C3 (multiple inheritance an toàn), @dataclass, @property, abcProtocol. Hiểu trọn gói = code Python "Pythonic" thay vì "Java-in-Python".

Độ dài: ~1100 dòng Bài tập: 5 Quiz: 8 Prerequisites: Chương 1-4
🎯 Mục tiêu chương
  • Khai báo class__init__ — hiểu vai trò của self.
  • Phân biệt instance attributeclass attribute; tránh bẫy mutable class attribute.
  • Sử dụng đúng @classmethod, @staticmethod, instance method.
  • Master inheritancesuper(); hiểu MRO C3 linearization với multiple inheritance.
  • Implement dunder method: __repr__, __eq__, __hash__, __len__, __iter__, __getitem__, __call__.
  • Dùng @property cho computed attribute và validation.
  • Dùng @dataclass để giảm boilerplate; biết khi nào frozen=True, slots=True.
  • Phân biệt abc.ABC (nominal subtype) và typing.Protocol (structural subtype).
  • Tư duy duck typing + EAFP thay vì isinstance check khắp nơi.
Special cases aren't special enough to break the rules.
Although practicality beats purity.

1. class — cú pháp tạo class

Class trong Python được khai báo bằng từ khóa class, theo sau là tên class (convention PascalCase) và dấu hai chấm. Body của class là một namespace — chứa attribute và method. Một class rỗng nhất có thể chỉ cần một dòng với pass.

class_basic.py
class User:
    pass

# Tạo instance — KHÔNG có new keyword
u = User()
print(type(u))   # <class '__main__.User'>
print(isinstance(u, User))  # True

# Có thể gán attribute động vào instance (giống JS)
u.name = "Anh"
print(u.name)   # 'Anh'
🔁 Đối chiếu với JS & Dart

JS: class User {}, tạo instance qua new User(). JS class là syntactic sugar trên prototype. Dart: class User {}, tạo instance User() (Dart 2+ bỏ new bắt buộc); class là first-class type với compile-time check. Python: User() — không có new. Class chính là callable; gọi class trả về instance. Class là một object thực sự (instance của type).

Vì class chỉ là namespace, có thể tham chiếu class như một giá trị bình thường: gán biến, truyền vào hàm, trả về từ hàm. "Everything is an object" — đây là một câu thường được trích từ cộng đồng Python; kể cả class cũng là object (instance của metaclass type).

class_is_object.py
class User:
    pass

print(type(User))         # <class 'type'> — class là instance của type
print(isinstance(User, type))  # True

# Truyền class như giá trị
factories = [User, dict, list]
instances = [f() for f in factories]
print(instances)        # [<User ...>, {}, []]

2. __init__ — constructor và self

__init__initializer được Python gọi ngay sau khi tạo instance. Tham số đầu tiên luôn là self — tham chiếu tới instance vừa tạo. self không phải từ khóa; nó chỉ là convention, nhưng tuyệt đối đừng đổi tên (vi phạm PEP 8 và làm code khó đọc).

init.py
class User:
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

    def greet(self) -> str:
        return f"Xin chào, tôi là {self.name}"

u = User("Anh", 22)
print(u.greet())   # 'Xin chào, tôi là Anh'
🧠 Mental model — self tự pass

Khi viết u.greet(), Python ngầm dịch thành User.greet(u) — instance được pass như tham số đầu tiên. Đây gọi là bound method. Vì vậy hàm trong class luôn cần self ở vị trí đầu. Nếu quên: TypeError: greet() takes 0 positional arguments but 1 was given.

__init__ không phải là người tạo ra object. Việc tạo instance thực ra do __new__ đảm nhiệm (rất hiếm khi cần ghi đè). __init__ chỉ khởi tạo state lên object đã có. Đó là lý do __init__ không có return (thực ra return được nhưng phải là None).

Type hints trong __init__

Từ Python 3.5+, có thể annotate kiểu cho tham số và return. __init__ trả None nên thường bỏ annotation return. Type hints không bị runtime enforce — chỉ tooling (mypy, IDE) kiểm tra. Sẽ học sâu ở Chương 9.

3. Instance attribute vs class attribute

Có hai chỗ để gán attribute:

  • Instance attribute: self.x = ... trong method (thường ở __init__). Mỗi instance có bản sao riêng.
  • Class attribute: khai báo trực tiếp trong body class, ngoài method. Một bản chia sẻ giữa mọi instance.
attr_kinds.py
class User:
    species = "human"      # class attribute — share
    count = 0             # class attribute

    def __init__(self, name):
        self.name = name    # instance attribute — riêng từng instance
        User.count += 1     # cập nhật class attr qua tên class

a = User("Anh")
b = User("Bình")
print(a.species, b.species)   # human human — chia sẻ
print(User.count)              # 2
print(a.name, b.name)         # Anh Bình — riêng
⚠️ Bẫy mutable class attribute

Đừng bao giờ dùng list, dict, set làm class attribute mặc định cho dữ liệu instance. Vì class attribute chia sẻ — mọi instance sẽ mutate cùng một list!

mutable-trap.py
# SAI
class Cart:
    items = []                  # class attr — chia sẻ!

    def add(self, x):
        self.items.append(x)   # mutate list chung

c1, c2 = Cart(), Cart()
c1.add("A")
print(c2.items)             # ['A'] — Cart() khác cũng thấy!

# ĐÚNG
class Cart:
    def __init__(self):
        self.items = []        # instance attr — list riêng

Khi đọc a.x, Python tra cứu theo thứ tự: instance dict → class dict → MRO của các lớp cha. Vì vậy class attribute hoạt động như fallback nếu instance không có. Một khi gán a.x = ..., giá trị mới được tạo trong instance dict và che lấp class attribute (không ảnh hưởng class).

4. Ba loại method — instance / class / static

Python phân biệt 3 loại method qua decorator. Sự khác biệt nằm ở tham số đầu tiên tự pass:

Loại Decorator Tham số tự pass Truy cập gì Khi nào dùng
Instance method (không cần) self (instance) Instance + class attr Hành vi của 1 instance
Class method @classmethod cls (class) Class attr; tạo instance thay thế Alternative constructor, factory
Static method @staticmethod (không có) Không tự pass; chỉ là namespace Utility liên quan logic của class
method_kinds.py
from __future__ import annotations
from datetime import date

class User:
    def __init__(self, name: str, birth_year: int):
        self.name = name
        self.birth_year = birth_year

    # Instance method — dùng self
    def age(self) -> int:
        return date.today().year - self.birth_year

    # Class method — alternative constructor
    @classmethod
    def from_dict(cls, data: dict) -> User:
        return cls(name=data["name"], birth_year=data["birth_year"])

    # Static method — utility
    @staticmethod
    def is_adult_age(age: int) -> bool:
        return age >= 18

u = User.from_dict({"name": "Anh", "birth_year": 2003})
print(u.age())                  # 23
print(User.is_adult_age(17))      # False
💡 @classmethod ưu việt với inheritance

cls trong @classmethod là class thực tế được gọi — không phải class khai báo. Khi Admin kế thừa User và gọi Admin.from_dict(...), cls chính là Admin, nên kết quả là một instance Admin. Nếu hard-code User(...) bên trong, subclass sẽ không được tận dụng.

5. Inheritance — class Admin(User)

Để kế thừa, đặt class cha trong dấu ngoặc: class Admin(User):. Subclass tự động thừa hưởng mọi method và attribute của parent. Override = định nghĩa lại method cùng tên trong subclass. Để gọi implementation của cha (ví dụ trong __init__ của subclass), dùng super().

inheritance.py
class User:
    def __init__(self, name):
        self.name = name

    def greet(self):
        return f"Hi, {self.name}"

class Admin(User):
    def __init__(self, name, level):
        super().__init__(name)   # chain init lên cha
        self.level = level

    def greet(self):                 # override
        base = super().greet()        # gọi version của cha
        return f"{base} [admin lv {self.level}]"

a = Admin("Anh", 2)
print(a.greet())   # 'Hi, Anh [admin lv 2]'
print(isinstance(a, User))  # True

Không cần truyền self hay class hiện tại vào super() — Python tự suy ra từ context. Trong Python 2, phải viết super(Admin, self).__init__(name), nhưng Python 3 đã đơn giản hóa.

🔁 Đối chiếu với JS & Dart

JS: class Admin extends User { constructor(name, level) { super(name); this.level = level; } }. Bắt buộc gọi super() trước khi đụng this. Dart: class Admin extends User { Admin(super.name, this.level); } — Dart 2.17+ có super-parameter shorthand rất gọn. Python: super().__init__(...) không bắt buộc; quên thì instance không có attribute mà parent gán. Tự do hơn nhưng cũng dễ sai hơn.

6. Multiple inheritance và MRO C3

Python cho phép kế thừa nhiều class: class C(A, B):. Khi tra cứu method, Python đi theo MRO (Method Resolution Order) — thứ tự duyệt class. Thuật toán dùng là C3 linearization, đảm bảo: (a) class con trước class cha; (b) thứ tự khai báo bên trái sang phải; (c) tính nhất quán (monotonic).

mro_basic.py
class A:
    def who(self): return "A"

class B:
    def who(self): return "B"

class C(A, B):
    pass

print(C().who())          # 'A' — A đứng trước B trong khai báo
print([k.__name__ for k in C.__mro__])
# ['C', 'A', 'B', 'object']

MRO trở nên thú vị (và đôi khi bối rối) với diamond inheritance — hai class cha chia sẻ một ông nội. C3 đảm bảo class ông nội chỉ xuất hiện một lần ở cuối.

diamond.py
class Animal:
    def describe(self): return "animal"

class Swimmer(Animal):
    def describe(self): return "swimmer-" + super().describe()

class Flyer(Animal):
    def describe(self): return "flyer-" + super().describe()

class Duck(Swimmer, Flyer):
    pass

print(Duck.__mro__)
# (Duck, Swimmer, Flyer, Animal, object)
print(Duck().describe())
# 'swimmer-flyer-animal' — super() đi đúng theo MRO
🧠 Mental model — super() không phải "cha trực tiếp"

Điểm khác biệt then chốt: super() đi tới class kế tiếp trong MRO, không phải "parent của class hiện tại". Trong ví dụ Duck, super() bên trong Swimmer.describe trỏ sang Flyer — vì MRO sắp xếp Flyer ngay sau Swimmer. Đây là điều kỳ diệu của C3: chain super() xếp hàng đẹp với diamond.

Nếu C3 không tìm được thứ tự nhất quán, Python từ chối khai báo class và ném TypeError: Cannot create a consistent method resolution order. Trường hợp đó thường do thứ tự base class mâu thuẫn — cần đổi lại thứ tự khai báo.

7. Dunder method — "double underscore" magic

Dunder method (đọc là "dander", viết tắt "double underscore") là các method tên kiểu __xxx__ mà Python tự gọi khi gặp syntax tương ứng. Override dunder = cho phép class của bạn hòa nhập với syntax ngôn ngữ: +, ==, len(), for ... in, print()...

Dunder Trigger Mục đích
__init__Sau khi tạo instanceKhởi tạo state
__repr__repr(obj), REPL echoDebug-friendly, eval-able nếu có thể
__str__str(obj), print(obj), f-stringUser-facing display
__eq__a == bSo sánh giá trị (mặc định: so sánh id)
__hash__hash(obj), key của dict/setCùng đi với __eq__
__lt__, __le__, __gt__, __ge__< <= > >=So sánh thứ tự (xem functools.total_ordering)
__len__len(obj)Số phần tử; cũng xác định truthiness nếu không có __bool__
__bool__bool(obj), if obj:Truthiness
__iter__for x in objTrả iterator (xem Chương 6)
__next__next(it)Iterator protocol
__contains__x in objMembership test
__getitem__obj[key]Indexing/slicing
__setitem__obj[key] = valueGán index
__delitem__del obj[key]Xóa index
__call__obj(...)Cho phép instance "gọi như hàm"
__add__, __sub__, __mul__...a + b, a - b...Toán tử số học
__enter__, __exit__with obj as x:Context manager (Chương 7)

7.1. __repr__ vs __str__

Convention quan trọng: __repr__ dành cho developer — nên hiển thị chi tiết, tốt nhất là một biểu thức Python hợp lệ có thể eval() lại thành object tương đương. __str__ dành cho người dùng cuối — gọn, đẹp. Nếu chỉ định nghĩa __repr__, Python tự lấy nó làm __str__.

repr_str.py
class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __repr__(self):
        return f"Point(x={self.x}, y={self.y})"

    def __str__(self):
        return f"({self.x}, {self.y})"

p = Point(1, 2)
print(repr(p))   # Point(x=1, y=2)
print(str(p))    # (1, 2)
print(p)         # (1, 2) — print gọi __str__

7.2. __eq____hash__ — luôn đi cùng nhau

Mặc định, hai instance khác nhau không bằng nhau dù state giống hệt — Python so sánh bằng id (địa chỉ object). Để so sánh theo giá trị, override __eq__. Nhưng có một quy tắc bắt buộc:

⚠️ Quy tắc __eq____hash__

Nếu override __eq__ nhưng không định nghĩa __hash__, Python tự đặt __hash__ = None — instance trở thành unhashable, không bỏ vào set hay làm key dict được. Quy tắc bất di bất dịch của hash: a == b ⇒ hash(a) == hash(b). Ngược lại không bắt buộc (collision được phép).

eq_hash.py
class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __eq__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return (self.x, self.y) == (other.x, other.y)

    def __hash__(self):
        return hash((self.x, self.y))   # tuple đã hashable

a, b = Point(1, 2), Point(1, 2)
print(a == b)         # True
print({a, b})         # {Point(x=1, y=2)} — set giữ 1 phần tử

Trả về NotImplemented (không phải NotImplementedError!) trong __eq__ khi không biết cách so sánh với kiểu khác — Python sẽ thử other.__eq__(self), rất quan trọng để hỗ trợ subclass và so sánh hai chiều.

7.3. __iter__, __contains__, __getitem__

iterable.py
class Deck:
    def __init__(self):
        self.cards = ["A♠", "K♠", "Q♠", "J♠"]

    def __len__(self):
        return len(self.cards)

    def __getitem__(self, i):
        return self.cards[i]

    def __contains__(self, card):
        return card in self.cards

d = Deck()
print(len(d))            # 4
print(d[0])             # 'A♠'
print("A♠" in d)        # True
for c in d:                # for-loop tự dùng __getitem__ nếu không có __iter__
    print(c)
💡 Iterable shortcut

Nếu có __getitem__ chấp nhận số nguyên 0, 1, 2... Python tự coi object là iterable (legacy protocol). Tuy nhiên, cách "Pythonic" hiện đại là implement __iter__ — sâu hơn ở Chương 6 (Iterators & Generators).

7.4. __call__ — instance như là hàm

callable.py
class Multiplier:
    def __init__(self, factor):
        self.factor = factor

    def __call__(self, x):
        return x * self.factor

double = Multiplier(2)
print(double(5))       # 10 — instance gọi như hàm
print(callable(double)) # True

__call__ rất hữu ích cho stateful callable — như decorator có config, hàm tính có cache, partial function. Đó cũng là lý do functools.partial trả về một object có __call__.

8. @property — method behave như attribute

@property cho phép truy cập một method không cần dấu ngoặc () — y hệt attribute. Dùng cho computed value (giá trị tính từ field khác) và validation (kiểm tra khi gán).

property.py
class Circle:
    def __init__(self, radius):
        self.radius = radius   # dùng setter nếu có

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("radius phải >= 0")
        self._radius = value

    @property
    def area(self):
        from math import pi
        return pi * self._radius ** 2

c = Circle(3)
print(c.radius)   # 3 — không có ()
print(c.area)     # 28.274... — computed
c.radius = 5      # setter chạy validation
# c.radius = -1  → ValueError

Nếu chỉ có @property không có setter — attribute trở thành read-only. Có thể thêm @xxx.deleter để custom del obj.xxx.

🔁 Đối chiếu với JS & Dart

JS: get area() {} / set radius(value) {} trong class. Dart: double get area => pi * _radius * _radius; — getter cú pháp riêng, gọn. Python: dùng decorator @property + @xxx.setter. Hơi dài dòng nhưng nhất quán với hệ thống decorator chung của ngôn ngữ.

9. @dataclass — giảm boilerplate

Mỗi khi viết class để giữ dữ liệu, ta phải gõ lại __init__, __repr__, __eq__ giống nhau. Python 3.7 đưa ra @dataclass — decorator generate những method này tự động dựa trên type annotation của class.

dataclass.py
from dataclasses import dataclass

@dataclass
class User:
    name: str
    age: int
    email: str = ""         # default value

u1 = User("Anh", 22)
u2 = User("Anh", 22)
print(u1)               # User(name='Anh', age=22, email='')
print(u1 == u2)         # True — __eq__ auto theo field

Mặc định @dataclass tạo: __init__, __repr__, __eq__. Các tham số đáng nhớ:

Tham sốMặc địnhHiệu ứng
init=TrueTrueGenerate __init__
repr=TrueTrueGenerate __repr__
eq=TrueTrueGenerate __eq__
order=TrueFalseGenerate __lt__...__ge__
frozen=TrueFalseInstance bất biến + auto __hash__
slots=TrueFalse (3.10+)Generate __slots__, tiết kiệm RAM, cấm attr mới
kw_only=TrueFalse (3.10+)Mọi tham số là keyword-only
dataclass_frozen.py
from dataclasses import dataclass

@dataclass(frozen=True, slots=True)
class Point:
    x: float
    y: float

p = Point(1.0, 2.0)
print({p})              # {Point(x=1.0, y=2.0)} — hashable vì frozen
# p.x = 9  →  FrozenInstanceError
# p.z = 0  →  AttributeError (slots không cho thêm attr)
⚠️ Default mutable trong dataclass

Không được viết items: list = [] vì sẽ chia sẻ list giữa các instance (giống bẫy mutable class attribute). Dùng field(default_factory=list):

default_factory.py
from dataclasses import dataclass, field

@dataclass
class Cart:
    items: list[str] = field(default_factory=list)
    discount: float = 0.0

10. Abstract Base Class — abc

Khi muốn ép subclass phải implement một method nhất định, dùng abc.ABC + decorator @abstractmethod. Class có @abstractmethod không thể tạo instance trực tiếp.

abc_basic.py
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self) -> float:
        ...

class Square(Shape):
    def __init__(self, side):
        self.side = side
    def area(self):
        return self.side ** 2

# Shape()  →  TypeError: Can't instantiate abstract class Shape
print(Square(3).area())   # 9

ABC tương đương interface ở các ngôn ngữ khác — nhưng vẫn được implement bằng class, nên có thể chứa method có sẵn (concrete method) lẫn @abstractmethod. Đó là lý do thường được gọi là "abstract base class" thay vì "interface": vừa làm khung vừa cung cấp logic chung.

11. typing.Protocol — structural typing

PEP 544 (Python 3.8+) đưa ra typing.Protocol — cho phép khai báo "type theo cấu trúc": "bất kỳ class nào có method X, Y với signature đúng đều coi là SupportsX". Không cần inherit — đây là structural typing, giống interface trong TypeScript / Go.

protocol.py
from typing import Protocol

class SupportsArea(Protocol):
    def area(self) -> float: ...

class Box:                       # không inherit SupportsArea
    def __init__(self, w, h):
        self.w, self.h = w, h
    def area(self) -> float:
        return self.w * self.h

def describe(s: SupportsArea) -> str:
    return f"area = {s.area()}"

print(describe(Box(2, 3)))   # 'area = 6' — mypy chấp nhận
🧠 Mental model — ABC vs Protocol

ABC (nominal): "Bạn phải khai báo bạn là Shape" — phải class Box(Shape). Quan hệ tường minh, kiểm tra được lúc runtime qua isinstance.
Protocol (structural): "Nếu trông giống Shape, làm việc giống Shape, thì bạn là Shape" — không cần khai báo. Chỉ là contract về cấu trúc, chủ yếu phục vụ tooling (mypy). Thường gọn hơn cho API "duck-typed".

Chương 9 (Typing) sẽ đào sâu Protocol + Generic + runtime_checkable.

12. Duck typing — "Nếu kêu quack quack thì là vịt"

Câu thành ngữ nguyên gốc: "If it walks like a duck and it quacks like a duck, then it must be a duck." Áp dụng vào Python: ta thường không quan tâm đối tượng là class gì, chỉ quan tâm nó có method/attribute cần thiết hay không. Đó là duck typing.

duck_typing.py
def play_sound(animal):
    # Không quan tâm animal là class gì, chỉ cần có .speak()
    return animal.speak()

class Duck:
    def speak(self): return "quack"

class Robot:
    def speak(self): return "beep"

print(play_sound(Duck()))    # 'quack'
print(play_sound(Robot()))   # 'beep' — Robot không phải Duck nhưng OK
💡 EAFP > LBYL

Cộng đồng Python đề cao EAFP ("Easier to Ask Forgiveness than Permission") — cứ làm, sai thì bắt exception. Đối lập với LBYL ("Look Before You Leap") — kiểm tra trước rồi mới làm.

eafp_vs_lbyl.py
# LBYL — không "Pythonic"
if hasattr(obj, "speak") and callable(obj.speak):
    obj.speak()

# EAFP — Pythonic
try:
    obj.speak()
except AttributeError:
    handle_no_speak()

Khi nào dùng isinstance? Khi thật sự cần ép contract (vd API public, kiểm tra để hỗ trợ kiểu mới một cách dạng "double dispatch"). Còn lại — tin vào duck typing.

Errors should never pass silently.
Unless explicitly silenced.

Bài tập

Vector 2D với toán tử

Viết class Vector2D với hai thuộc tính x, y. Implement các dunder method: __init__, __repr__, __eq__, __add__, __sub__, __mul__ (scalar bên phải). Yêu cầu:

  • Vector2D(1, 2) + Vector2D(3, 4) == Vector2D(4, 6)
  • Vector2D(3, 4) - Vector2D(1, 2) == Vector2D(2, 2)
  • Vector2D(1, 2) * 3 == Vector2D(3, 6)
  • repr(Vector2D(1, 2)) phải eval lại được
Gợi ý

Trong các toán tử __add__/__sub__, trả về một instance mới — không mutate self. __mul__(self, scalar) nhận một số làm tham số thứ hai. Khi so sánh kiểu khác, trả NotImplemented. Để ổn định repr, dùng f"Vector2D({self.x}, {self.y})".

Hierarchy Shape với ABC

Tạo class trừu tượng Shape (kế thừa abc.ABC) với @abstractmethod area(). Implement Circle(radius), Square(side), Rectangle(width, height). Sau đó:

  • Tạo list shapes = [Circle(2), Square(3), Rectangle(2, 5)]
  • In ra tổng diện tích bằng sum(s.area() for s in shapes)
  • Thử Shape() → phải ra TypeError
Gợi ý

area()Shape chỉ cần ... (Ellipsis literal) làm body. Mỗi subclass override area() với công thức riêng. Đây là ví dụ kinh điển của polymorphism — code dùng list không cần biết từng phần tử là Circle hay Square.

Refactor User sang @dataclass(frozen=True)

Cho class User dạng "manual":

before.py
class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    def __repr__(self):
        return f"User(name={self.name!r}, age={self.age})"
    def __eq__(self, other):
        if not isinstance(other, User): return NotImplemented
        return (self.name, self.age) == (other.name, other.age)
    def __hash__(self):
        return hash((self.name, self.age))

Viết lại dưới dạng @dataclass(frozen=True). So sánh số dòng. Kiểm tra: hash(User("A", 1)) hoạt động, User("A", 1) == User("A", 1) trả True, và user.name = "B" ném FrozenInstanceError.

Gợi ý

frozen=True đồng thời cấm set attribute bật eq=True (mặc định) sẽ kéo theo auto-generate __hash__. Phiên bản sau gọn còn ~4 dòng.

Lớp Temperature đa đơn vị

Tạo class Temperature lưu state trong _celsius (convention private). Implement @property cho ba đơn vị: celsius, fahrenheit, kelvin. Mỗi property có cả getter và setter — setter chuyển ngược về Celsius rồi cập nhật _celsius.

  • Công thức: F = C × 9/5 + 32, K = C + 273.15
  • t = Temperature(); t.celsius = 100; print(t.fahrenheit, t.kelvin)212.0 373.15
  • t.fahrenheit = 32; print(t.celsius)0.0
Gợi ý

Validation hợp lý: setter kelvin kiểm tra value < 0ValueError("dưới 0 K không có nghĩa"). Mỗi setter cập nhật _celsius duy nhất; getter các đơn vị khác đều derive từ _celsius.

Mixin Serializable

Tạo mixin Serializable với method to_json(self) trả về chuỗi JSON đại diện cho instance. Dùng dataclasses.asdict để convert dataclass thành dict, rồi json.dumps. Áp dụng lên dataclass Product:

start.py
from dataclasses import dataclass

@dataclass
class Product(Serializable):
    sku: str
    name: str
    price: float

print(Product("P001", "Book", 19.99).to_json())
# {"sku": "P001", "name": "Book", "price": 19.99}

Vẽ Product.__mro__ và xác nhận thứ tự: Product → Serializable → object.

Gợi ý

Mixin là một class thông thường, không cần inherit ABC — chỉ chứa method công cộng để được mix vào nhiều class khác qua multiple inheritance. Đặt mixin trước object trong khai báo, nhưng theo convention nên đặt sau class chính nếu có nhiều base.

Quiz

Q1

self là gì? Tại sao phải khai báo như tham số đầu?

Xem đáp án
✓ Đáp án

self là tham chiếu tới instance mà method đang được gọi trên. Khi viết obj.method(x), Python ngầm dịch thành Class.method(obj, x) — instance được truyền vào tham số đầu. Vì vậy method instance bắt buộc nhận self. Cái tên "self" chỉ là convention (PEP 8); thay đổi sẽ vẫn chạy được nhưng làm code khó đọc và bị linter cảnh báo.

Q2

@classmethod@staticmethod khác nhau ra sao?

Xem đáp án
✓ Đáp án

@classmethod nhận cls tự động — là class thực tế được gọi (subclass sẽ thấy class mình). Thường dùng làm alternative constructor (from_dict, from_file...). @staticmethod không nhận tham số tự động nào — chỉ là utility được đặt trong namespace class cho gọn. Vì không có cls, static method không hỗ trợ polymorphism qua class. Khi cần override theo subclass, chọn @classmethod.

Q3

__str____repr__ khác gì? Khi nào ta cần cả hai?

Xem đáp án
✓ Đáp án

__str__ dành cho người dùng cuối — gọn, đẹp, dễ đọc (vd "(1, 2)"). __repr__ dành cho developer — chi tiết, mục tiêu lý tưởng là eval(repr(obj)) trả về object tương đương (vd "Point(x=1, y=2)"). Nếu chỉ định nghĩa __repr__, Python tự lấy nó làm __str__ — nên một class tối thiểu chỉ cần __repr__. Override cả hai khi muốn UX hiển thị khác (vd log internal chi tiết hơn cho debug).

Q4

Với class C(A, B), Python quyết định thứ tự lookup method ra sao?

Xem đáp án
✓ Đáp án

Python dùng thuật toán C3 linearization để tính MRO (Method Resolution Order). Quy tắc: (1) class con trước class cha, (2) bên trái trước bên phải trong khai báo, (3) thứ tự nhất quán giữa các MRO. Có thể xem qua C.__mro__ hoặc C.mro(). super() đi theo MRO chứ không phải "cha trực tiếp" — đó là chìa khóa làm cho diamond inheritance hoạt động đúng.

Q5

@dataclass tự generate những method nào? Khi nào có __hash__?

Xem đáp án
✓ Đáp án

Mặc định: __init__, __repr__, __eq__. Option order=True bổ sung __lt__...__ge__. __hash__ được generate khi frozen=True (cùng với eq=True, đây là mặc định). Nếu chỉ có eq=True mà không frozen=True, dataclass đặt __hash__ = None → instance unhashable. Có thể ép unsafe_hash=True nhưng rủi ro (instance mutable mà hashable → hỏng dict/set).

Q6

Setter của @property có bắt buộc không?

Xem đáp án
✓ Đáp án

Không bắt buộc. Nếu chỉ có @property mà không có @xxx.setter, attribute trở thành read-only — gán obj.xxx = ... sẽ ném AttributeError: can't set attribute. Đây là cách phổ biến để khai báo computed property (vd area tính từ radius) hoặc immutable field. Có thể thêm @xxx.deleter để custom del obj.xxx.

Q7

Khi override __eq__, ta còn phải override gì? Tại sao?

Xem đáp án
✓ Đáp án

Phải override __hash__. Lý do: Python tự đặt __hash__ = None khi phát hiện __eq__ custom — instance sẽ unhashable. Quy tắc hash bất di bất dịch: a == b ⇒ hash(a) == hash(b). Nếu chỉ override __eq__ mà không cập nhật hash, hai instance "bằng nhau" lại có hash khác → vi phạm hợp đồng của set/dict, dẫn đến bug khủng khiếp. Quy tắc đơn giản: object mutable (có thể đổi state) → đừng hashable; object immutable → hash từ một tuple các field.

Q8

Duck typing vs isinstance check — cái nào "Pythonic" hơn?

Xem đáp án
✓ Đáp án

Trong đa số trường hợp, duck typing Pythonic hơn — kết hợp với phong cách EAFP (cứ làm, exception thì bắt). Code linh hoạt, dễ test (mock object chỉ cần expose đúng method), không khóa kiểu cụ thể.
isinstance hợp lý khi: (a) thực sự cần phân biệt nhiều kiểu khác nhau (vd hỗ trợ strlist input cho cùng API); (b) viết API public cần guarantee mạnh; (c) dùng typing.Protocol với @runtime_checkable để kết hợp ưu điểm cả hai — có structural type vẫn check được runtime. Tránh isinstance theo thói quen ép kiểu; tin vào hành vi của object.

Tổng kết

Sau chương 5, bạn đã master:

  • Class & __init__: cú pháp, self, instance vs class attribute, bẫy mutable.
  • Ba loại method: instance, @classmethod (alternative constructor), @staticmethod (namespace utility).
  • Inheritance: class B(A), override, super().__init__ chain.
  • Multiple inheritance & MRO: C3 linearization, __mro__, diamond.
  • Dunder methods: __repr__/__str__, __eq__/__hash__ luôn đi đôi, __len__, __iter__, __contains__, __getitem__, __call__.
  • @property: computed value, validation, read-only attribute.
  • @dataclass: frozen, slots, field(default_factory=...).
  • ABC vs Protocol: nominal vs structural typing; duck typing & EAFP làm tinh thần cốt lõi.

Kết nối

  • Chương 6 (Iterators & Generators) — đào sâu __iter__/__next__, iterable protocol và yield để tạo class lazy.
  • Chương 7 (Decorators & Context Managers)@functools.wraps bảo toàn metadata khi decorator class method; __enter__/__exit__ cho with statement.
  • Chương 9 (Typing & Static Analysis)Protocol đi với Generic, TypedDict, type narrowing qua isinstance; PEP 695 Generic syntax (3.12+).
  • JavaScript Chương 4 (Objects, Prototypes & Classes) — đối chiếu mô hình prototype của JS với class-as-namespace của Python.
  • Dart Chương 4 (Class Modifiers) — Dart 3 đưa ra sealed/base/final/interface class, đối lập với phong cách tự do của Python (không có private thật sự, không có sealed).