Khi nào cần Protocol thay vì ABC hay isinstance()?
Mình dùng Python làm automation tool cho hầu hết task hàng ngày, từ deploy script đến monitoring alert. Một vấn đề mình hay gặp là: code viết xong, test pass, nhưng lúc tích hợp component mới vào hệ thống thì runtime error lại xuất hiện — thường do một class thiếu method mà code mong đợi.
Python vốn là ngôn ngữ duck typing: “nếu nó có thể quạc như vịt, thì nó là vịt”. Python không quan tâm đối tượng thuộc class gì, chỉ cần nó có method cần dùng là được. Vấn đề là kiểm tra này chỉ xảy ra lúc runtime — lúc code chạy thật, không phải lúc viết.
Abstract Base Class (ABC) giải quyết một phần, nhưng yêu cầu kế thừa tường minh — không phù hợp khi làm việc với thư viện bên ngoài hoặc khi muốn giữ code loosely coupled.
Protocol (PEP 544, Python 3.8+) ra đời để giải quyết đúng vấn đề này: static duck typing — kiểm tra duck typing tại thời điểm viết code với type checker như mypy, không phải lúc runtime.
Cài đặt môi trường
Protocol có sẵn trong module typing từ Python 3.8. Nếu đang dùng Python 3.7 trở xuống, cần cài thêm typing_extensions:
pip install typing_extensions mypy
Kiểm tra phiên bản Python và mypy:
python --version
# Python 3.8+ là đủ, Protocol có sẵn trong stdlib
pip install mypy
mypy --version
Mypy là công cụ kiểm tra kiểu tĩnh chính thức cho Python — dùng nó để bắt lỗi Protocol ngay lúc viết code.
Định nghĩa và sử dụng Protocol — Cấu hình chi tiết
1. Protocol cơ bản
Thay vì kế thừa ABC và register(), bạn định nghĩa Protocol mô tả “hình dạng” mong đợi từ đối tượng:
from typing import Protocol
class Serializable(Protocol):
def to_json(self) -> str:
...
def from_json(self, data: str) -> None:
...
Body của method trong Protocol thường là .... Đây là định nghĩa interface, không phải implementation.
2. Dùng Protocol trong function signature
def save_to_file(obj: Serializable, filepath: str) -> None:
data = obj.to_json()
with open(filepath, 'w') as f:
f.write(data)
# Class này KHÔNG kế thừa Serializable,
# nhưng có đủ method — mypy vẫn chấp nhận
class UserConfig:
def __init__(self, username: str, theme: str):
self.username = username
self.theme = theme
def to_json(self) -> str:
import json
return json.dumps({"username": self.username, "theme": self.theme})
def from_json(self, data: str) -> None:
import json
d = json.loads(data)
self.username = d["username"]
self.theme = d["theme"]
config = UserConfig("admin", "dark")
save_to_file(config, "/tmp/config.json") # OK với mypy!
Đây là điểm cốt lõi: UserConfig không cần class UserConfig(Serializable). Chỉ cần có đúng method là mypy chấp nhận.
3. Protocol với @runtime_checkable
Mặc định Protocol chỉ hoạt động với type checker tĩnh. Nếu muốn dùng isinstance() lúc runtime, thêm decorator @runtime_checkable:
from typing import Protocol, runtime_checkable
@runtime_checkable
class Closeable(Protocol):
def close(self) -> None:
...
class DatabaseConnection:
def close(self) -> None:
print("DB connection closed")
db = DatabaseConnection()
print(isinstance(db, Closeable)) # True
print(isinstance("hello", Closeable)) # False
Lưu ý quan trọng: isinstance() với @runtime_checkable chỉ kiểm tra sự tồn tại của method, không kiểm tra signature (kiểu tham số, kiểu trả về). Kiểm tra signature đầy đủ là việc của mypy.
4. Ví dụ thực tế: Plugin system cho alert handler
Mình hay dùng pattern này khi viết tool có thể mở rộng — điển hình là monitoring alert system:
from typing import Protocol
from dataclasses import dataclass
@dataclass
class AlertEvent:
level: str # "info", "warning", "critical"
message: str
source: str
class AlertHandler(Protocol):
def handle(self, event: AlertEvent) -> bool:
"""Returns True nếu xử lý thành công"""
...
def is_available(self) -> bool:
...
# Handler 1: Gửi Telegram
class TelegramHandler:
def __init__(self, bot_token: str, chat_id: str):
self.bot_token = bot_token
self.chat_id = chat_id
def handle(self, event: AlertEvent) -> bool:
print(f"[Telegram] {event.level}: {event.message}")
return True
def is_available(self) -> bool:
return bool(self.bot_token and self.chat_id)
# Handler 2: Ghi log file
class FileLogHandler:
def __init__(self, log_path: str):
self.log_path = log_path
def handle(self, event: AlertEvent) -> bool:
with open(self.log_path, 'a') as f:
f.write(f"[{event.level}] {event.source}: {event.message}\n")
return True
def is_available(self) -> bool:
import os
return os.access(os.path.dirname(self.log_path) or '.', os.W_OK)
# dispatch_alert nhận list[AlertHandler] — không cần kế thừa gì cả
def dispatch_alert(event: AlertEvent, handlers: list[AlertHandler]) -> None:
for handler in handlers:
if handler.is_available():
success = handler.handle(event)
if not success:
print(f"{type(handler).__name__} failed")
# Dùng như thế này
event = AlertEvent(level="critical", message="CPU > 95%", source="server-01")
handlers: list[AlertHandler] = [
TelegramHandler("TOKEN", "CHAT_ID"),
FileLogHandler("/var/log/alerts.log"),
]
dispatch_alert(event, handlers)
Ưu điểm rõ ràng: để thêm handler mới (Slack, PagerDuty…), bạn chỉ cần tạo class có đúng 2 method, không cần sửa code có sẵn — đúng với Open/Closed Principle.
Kiểm tra và Monitoring với mypy
Chạy mypy phát hiện lỗi sớm
# Kiểm tra toàn bộ project
mypy .
# Strict mode — bắt nhiều lỗi hơn
mypy alert_system.py --strict
Ví dụ lỗi mypy sẽ báo khi truyền object không thỏa Protocol:
class BadHandler:
def handle(self, event: AlertEvent) -> None: # Sai! Protocol yêu cầu -> bool
pass
# Thiếu is_available()
# mypy báo:
# error: Argument 2 to "dispatch_alert" has incompatible type
# "list[BadHandler]"; expected "list[AlertHandler]"
Cấu hình mypy với pyproject.toml
[tool.mypy]
python_version = "3.11"
strict = true
ignore_missing_imports = true
Tích hợp vào CI/CD
# Trong GitHub Actions
pip install mypy
mypy . --exit-code # exit code != 0 nếu có lỗi type
# Kết hợp với pytest
pytest tests/ && mypy .
Mình thường thêm mypy vào pre-commit hook để bắt lỗi trước khi commit:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.8.0
hooks:
- id: mypy
additional_dependencies: [types-requests]
Khi nào dùng Protocol, khi nào dùng ABC?
- Protocol: Muốn loosely coupled, không muốn bắt buộc kế thừa, làm việc với code third-party
- ABC: Muốn strongly coupled, cần
@abstractmethodbắt buộc implement, muốn share code quasuper() - TypedDict: Làm việc với dict có structure cố định, không phải object
Với mình, Protocol là lựa chọn mặc định khi thiết kế interface cho plugin system hoặc component cần tách biệt rõ ràng. ABC chỉ dùng khi muốn share implementation qua kế thừa.

