Giao diện
Protocols & ABCs Nâng cao
Duck Typing + Type Safety = Protocols
Learning Outcomes
Sau khi hoàn thành trang này, bạn sẽ:
- ✅ Hiểu sự khác biệt giữa structural và nominal typing
- ✅ Sử dụng
typing.Protocolcho duck typing với type hints - ✅ So sánh và chọn đúng giữa Protocol và ABC
- ✅ Viết code linh hoạt mà vẫn type-safe
Duck Typing là gì?
"If it walks like a duck and quacks like a duck, then it must be a duck."
Python không quan tâm object thuộc class nào — chỉ quan tâm object có thể làm gì.
python
class Duck:
def quack(self):
print("Quack!")
def walk(self):
print("Walking like a duck")
class Person:
def quack(self):
print("I'm pretending to be a duck!")
def walk(self):
print("Walking like a person")
def make_it_quack(thing):
"""Không quan tâm type, chỉ cần có method quack()."""
thing.quack()
make_it_quack(Duck()) # ✅ Quack!
make_it_quack(Person()) # ✅ I'm pretending to be a duck!Vấn đề: Duck Typing không có Type Hints
python
def make_it_quack(thing): # thing là gì? IDE không biết!
thing.quack() # Có method quack() không? Không chắc!
# Type checker không thể verify
# IDE không thể autocomplete
# Bugs chỉ phát hiện khi runtimeStructural vs Nominal Typing
Nominal Typing (Inheritance-based)
Type được xác định bởi tên class và inheritance chain.
python
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def speak(self) -> str:
pass
class Dog(Animal): # Phải kế thừa Animal
def speak(self) -> str:
return "Woof!"
class Cat(Animal): # Phải kế thừa Animal
def speak(self) -> str:
return "Meow!"
def make_speak(animal: Animal) -> str:
return animal.speak()
# ❌ Robot có speak() nhưng không kế thừa Animal
class Robot:
def speak(self) -> str:
return "Beep boop!"
make_speak(Robot()) # Type error! Robot không phải AnimalStructural Typing (Protocol-based)
Type được xác định bởi structure (methods và attributes).
python
from typing import Protocol
class Speaker(Protocol):
def speak(self) -> str: ...
class Dog: # Không cần kế thừa!
def speak(self) -> str:
return "Woof!"
class Robot: # Không cần kế thừa!
def speak(self) -> str:
return "Beep boop!"
def make_speak(speaker: Speaker) -> str:
return speaker.speak()
make_speak(Dog()) # ✅ OK - Dog có speak()
make_speak(Robot()) # ✅ OK - Robot có speak()typing.Protocol (Python 3.8+)
Định nghĩa Protocol
python
from typing import Protocol
class Drawable(Protocol):
"""Protocol cho objects có thể vẽ."""
def draw(self) -> None:
"""Vẽ object lên canvas."""
... # Ellipsis = abstract method
class Circle:
def __init__(self, radius: float):
self.radius = radius
def draw(self) -> None:
print(f"Drawing circle with radius {self.radius}")
class Square:
def __init__(self, side: float):
self.side = side
def draw(self) -> None:
print(f"Drawing square with side {self.side}")
def render(shape: Drawable) -> None:
"""Render bất kỳ object nào có draw()."""
shape.draw()
render(Circle(5)) # ✅ OK
render(Square(10)) # ✅ OKProtocol với Attributes
python
from typing import Protocol
class Named(Protocol):
"""Protocol cho objects có name attribute."""
name: str # Required attribute
class User:
def __init__(self, name: str, email: str):
self.name = name
self.email = email
class Product:
def __init__(self, name: str, price: float):
self.name = name
self.price = price
def greet(entity: Named) -> str:
return f"Hello, {entity.name}!"
greet(User("Alice", "alice@example.com")) # ✅ OK
greet(Product("Laptop", 999.99)) # ✅ OKProtocol với Properties
python
from typing import Protocol
class HasArea(Protocol):
@property
def area(self) -> float:
"""Computed area property."""
...
class Rectangle:
def __init__(self, width: float, height: float):
self.width = width
self.height = height
@property
def area(self) -> float:
return self.width * self.height
class Circle:
def __init__(self, radius: float):
self.radius = radius
@property
def area(self) -> float:
import math
return math.pi * self.radius ** 2
def total_area(shapes: list[HasArea]) -> float:
return sum(shape.area for shape in shapes)
shapes = [Rectangle(10, 5), Circle(3)]
print(total_area(shapes)) # 50 + 28.27... = 78.27...Generic Protocols
python
from typing import Protocol, TypeVar
T = TypeVar('T')
T_co = TypeVar('T_co', covariant=True)
class Comparable(Protocol):
"""Protocol cho objects có thể so sánh."""
def __lt__(self, other: "Comparable") -> bool: ...
def __le__(self, other: "Comparable") -> bool: ...
def __gt__(self, other: "Comparable") -> bool: ...
def __ge__(self, other: "Comparable") -> bool: ...
class Container(Protocol[T_co]):
"""Generic protocol cho containers."""
def __contains__(self, item: object) -> bool: ...
def __iter__(self) -> "Iterator[T_co]": ...
def find_max(items: list[Comparable]) -> Comparable:
"""Tìm max trong list của Comparable objects."""
return max(items)
# Hoạt động với bất kỳ type nào có comparison operators
find_max([1, 2, 3]) # ✅ int
find_max(["a", "b", "c"]) # ✅ str
find_max([1.5, 2.5, 3.5]) # ✅ floatCallable Protocol
python
from typing import Protocol
class Handler(Protocol):
"""Protocol cho callable handlers."""
def __call__(self, event: dict) -> bool: ...
def log_handler(event: dict) -> bool:
print(f"Logging: {event}")
return True
class AlertHandler:
def __init__(self, threshold: int):
self.threshold = threshold
def __call__(self, event: dict) -> bool:
if event.get("severity", 0) >= self.threshold:
print(f"ALERT: {event}")
return True
return False
def process_event(event: dict, handlers: list[Handler]) -> None:
for handler in handlers:
handler(event)
handlers: list[Handler] = [
log_handler, # Function
AlertHandler(threshold=5), # Callable class instance
]
process_event({"type": "error", "severity": 7}, handlers)abc.ABC - Abstract Base Classes
Định nghĩa ABC
python
from abc import ABC, abstractmethod
class Shape(ABC):
"""Abstract base class cho shapes."""
@abstractmethod
def area(self) -> float:
"""Calculate area. Must be implemented."""
pass
@abstractmethod
def perimeter(self) -> float:
"""Calculate perimeter. Must be implemented."""
pass
def describe(self) -> str:
"""Concrete method - có implementation."""
return f"Shape with area {self.area():.2f}"
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
def perimeter(self) -> float:
return 2 * (self.width + self.height)
# ❌ Không thể instantiate ABC
# shape = Shape() # TypeError!
# ✅ Phải implement tất cả abstract methods
rect = Rectangle(10, 5)
print(rect.area()) # 50.0
print(rect.describe()) # Shape with area 50.00Abstract Properties
python
from abc import ABC, abstractmethod
class Vehicle(ABC):
@property
@abstractmethod
def max_speed(self) -> float:
"""Maximum speed in km/h."""
pass
@property
@abstractmethod
def fuel_type(self) -> str:
"""Type of fuel used."""
pass
class Car(Vehicle):
@property
def max_speed(self) -> float:
return 200.0
@property
def fuel_type(self) -> str:
return "gasoline"
class ElectricCar(Vehicle):
@property
def max_speed(self) -> float:
return 250.0
@property
def fuel_type(self) -> str:
return "electricity"ABC với __subclasshook__
python
from abc import ABC, abstractmethod
class Sized(ABC):
@abstractmethod
def __len__(self) -> int:
pass
@classmethod
def __subclasshook__(cls, C):
"""Cho phép structural subtyping."""
if cls is Sized:
if hasattr(C, "__len__"):
return True
return NotImplemented
# list không kế thừa Sized, nhưng có __len__
print(isinstance([], Sized)) # True
print(isinstance("hello", Sized)) # True
print(isinstance(42, Sized)) # FalseProtocol vs ABC: Khi nào dùng gì?
So sánh Chi tiết
| Aspect | Protocol | ABC |
|---|---|---|
| Typing | Structural | Nominal |
| Inheritance | Không cần | Bắt buộc |
| Runtime check | Không (chỉ static) | Có (isinstance) |
| Default implementation | Không | Có |
| Flexibility | Cao | Thấp hơn |
| Third-party classes | ✅ Hoạt động | ❌ Cần wrapper |
Khi nào dùng Protocol
python
from typing import Protocol
# ✅ Dùng Protocol khi:
# 1. Cần duck typing với type hints
# 2. Làm việc với third-party classes
# 3. Không muốn ép buộc inheritance
class JSONSerializable(Protocol):
def to_json(self) -> str: ...
# Third-party class có to_json() tự động compatible
# Không cần modify source codeKhi nào dùng ABC
python
from abc import ABC, abstractmethod
# ✅ Dùng ABC khi:
# 1. Cần shared implementation
# 2. Cần runtime isinstance() checks
# 3. Muốn enforce contract rõ ràng
class Repository(ABC):
"""Base repository với shared logic."""
@abstractmethod
def get(self, id: int):
pass
@abstractmethod
def save(self, entity):
pass
def get_or_create(self, id: int, default_factory):
"""Shared implementation."""
entity = self.get(id)
if entity is None:
entity = default_factory()
self.save(entity)
return entityHybrid Approach: Protocol + ABC
python
from typing import Protocol, runtime_checkable
from abc import ABC, abstractmethod
# Protocol cho type checking
@runtime_checkable
class Persistable(Protocol):
def save(self) -> None: ...
def load(self) -> None: ...
# ABC cho shared implementation
class BasePersistable(ABC):
@abstractmethod
def save(self) -> None:
pass
@abstractmethod
def load(self) -> None:
pass
def backup(self) -> None:
"""Shared backup logic."""
self.save()
print("Backup created")
# Class có thể implement Protocol mà không kế thừa
class Config:
def save(self) -> None:
print("Saving config...")
def load(self) -> None:
print("Loading config...")
# Hoặc kế thừa ABC để có shared implementation
class UserSettings(BasePersistable):
def save(self) -> None:
print("Saving user settings...")
def load(self) -> None:
print("Loading user settings...")
# Cả hai đều compatible với Persistable Protocol
def persist(obj: Persistable) -> None:
obj.save()
persist(Config()) # ✅ OK
persist(UserSettings()) # ✅ OK@runtime_checkable Decorator
Cho phép dùng isinstance() với Protocol:
python
from typing import Protocol, runtime_checkable
@runtime_checkable
class Closeable(Protocol):
def close(self) -> None: ...
class FileHandler:
def close(self) -> None:
print("Closing file...")
class DatabaseConnection:
def close(self) -> None:
print("Closing connection...")
# Runtime check hoạt động
print(isinstance(FileHandler(), Closeable)) # True
print(isinstance(DatabaseConnection(), Closeable)) # True
print(isinstance("string", Closeable)) # False
def cleanup(resource: object) -> None:
if isinstance(resource, Closeable):
resource.close()⚠️ LIMITATION
@runtime_checkable chỉ check method names, không check signatures!
python
@runtime_checkable
class Adder(Protocol):
def add(self, x: int, y: int) -> int: ...
class BadAdder:
def add(self) -> str: # Wrong signature!
return "oops"
# ❌ isinstance() vẫn trả về True!
print(isinstance(BadAdder(), Adder)) # True (chỉ check có method 'add')Built-in Protocols
Python có nhiều built-in protocols trong typing và collections.abc:
python
from typing import (
Iterable, # __iter__
Iterator, # __iter__, __next__
Callable, # __call__
Hashable, # __hash__
Sized, # __len__
Container, # __contains__
Reversible, # __reversed__
SupportsInt, # __int__
SupportsFloat, # __float__
SupportsAbs, # __abs__
SupportsRound, # __round__
)
from collections.abc import (
Mapping, # dict-like
MutableMapping,
Sequence, # list-like
MutableSequence,
Set, # set-like
MutableSet,
)
# Sử dụng built-in protocols
def process_items(items: Iterable[str]) -> list[str]:
return [item.upper() for item in items]
process_items(["a", "b", "c"]) # ✅ list
process_items(("a", "b", "c")) # ✅ tuple
process_items({"a", "b", "c"}) # ✅ set
process_items("abc") # ✅ strReal-World Patterns
Repository Pattern với Protocol
python
from typing import Protocol, TypeVar, Generic
from dataclasses import dataclass
T = TypeVar('T')
@dataclass
class User:
id: int
name: str
email: str
class Repository(Protocol[T]):
"""Generic repository protocol."""
def get(self, id: int) -> T | None: ...
def get_all(self) -> list[T]: ...
def save(self, entity: T) -> T: ...
def delete(self, id: int) -> bool: ...
class InMemoryUserRepository:
"""In-memory implementation for testing."""
def __init__(self):
self._users: dict[int, User] = {}
self._next_id = 1
def get(self, id: int) -> User | None:
return self._users.get(id)
def get_all(self) -> list[User]:
return list(self._users.values())
def save(self, entity: User) -> User:
if entity.id == 0:
entity.id = self._next_id
self._next_id += 1
self._users[entity.id] = entity
return entity
def delete(self, id: int) -> bool:
if id in self._users:
del self._users[id]
return True
return False
class UserService:
"""Service sử dụng Repository protocol."""
def __init__(self, repo: Repository[User]):
self.repo = repo
def create_user(self, name: str, email: str) -> User:
user = User(id=0, name=name, email=email)
return self.repo.save(user)
# Dependency injection với Protocol
repo = InMemoryUserRepository()
service = UserService(repo)
user = service.create_user("Alice", "alice@example.com")Plugin System với Protocol
python
from typing import Protocol
from dataclasses import dataclass
@dataclass
class Event:
type: str
data: dict
class EventHandler(Protocol):
"""Protocol cho event handlers."""
def can_handle(self, event: Event) -> bool: ...
def handle(self, event: Event) -> None: ...
class LoggingHandler:
def can_handle(self, event: Event) -> bool:
return True # Handle all events
def handle(self, event: Event) -> None:
print(f"[LOG] {event.type}: {event.data}")
class AlertHandler:
def __init__(self, alert_types: set[str]):
self.alert_types = alert_types
def can_handle(self, event: Event) -> bool:
return event.type in self.alert_types
def handle(self, event: Event) -> None:
print(f"🚨 ALERT: {event.type} - {event.data}")
class EventDispatcher:
def __init__(self):
self.handlers: list[EventHandler] = []
def register(self, handler: EventHandler) -> None:
self.handlers.append(handler)
def dispatch(self, event: Event) -> None:
for handler in self.handlers:
if handler.can_handle(event):
handler.handle(event)
# Usage
dispatcher = EventDispatcher()
dispatcher.register(LoggingHandler())
dispatcher.register(AlertHandler({"error", "critical"}))
dispatcher.dispatch(Event("info", {"message": "User logged in"}))
dispatcher.dispatch(Event("error", {"message": "Database connection failed"}))Production Pitfalls ⚠️
1. Protocol không enforce implementation
python
from typing import Protocol
class Serializable(Protocol):
def serialize(self) -> bytes: ...
class BadClass:
pass # Không có serialize()
def save(obj: Serializable) -> None:
data = obj.serialize() # Runtime error!
# Type checker cảnh báo, nhưng code vẫn chạy được
save(BadClass()) # ❌ AttributeError at runtime2. Circular Protocol Dependencies
python
from typing import Protocol
# ❌ BAD: Circular dependency
class A(Protocol):
def get_b(self) -> "B": ...
class B(Protocol):
def get_a(self) -> A: ...
# ✅ GOOD: Dùng TYPE_CHECKING
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .b import B
class A(Protocol):
def get_b(self) -> "B": ...3. Quên @runtime_checkable khi cần isinstance()
python
from typing import Protocol
class Closeable(Protocol):
def close(self) -> None: ...
class File:
def close(self) -> None:
pass
# ❌ BAD: Protocol không có @runtime_checkable
# isinstance(File(), Closeable) # TypeError!
# ✅ GOOD: Thêm decorator
from typing import runtime_checkable
@runtime_checkable
class Closeable(Protocol):
def close(self) -> None: ...
isinstance(File(), Closeable) # True4. ABC với Missing Abstract Methods
python
from abc import ABC, abstractmethod
class Base(ABC):
@abstractmethod
def method_a(self) -> None:
pass
@abstractmethod
def method_b(self) -> None:
pass
# ❌ BAD: Quên implement method_b
class Incomplete(Base):
def method_a(self) -> None:
print("A")
# TypeError: Can't instantiate abstract class Incomplete
# with abstract method method_b
# obj = Incomplete()Bảng Tóm tắt
python
# === PROTOCOL (Structural Typing) ===
from typing import Protocol, runtime_checkable
class MyProtocol(Protocol):
attr: str # Required attribute
def method(self, x: int) -> str: ... # Required method
# Runtime checkable
@runtime_checkable
class Checkable(Protocol):
def check(self) -> bool: ...
isinstance(obj, Checkable) # Works!
# === ABC (Nominal Typing) ===
from abc import ABC, abstractmethod
class MyABC(ABC):
@abstractmethod
def required_method(self) -> None:
pass
def shared_method(self) -> str:
"""Concrete method với implementation."""
return "shared"
# === WHEN TO USE ===
# Protocol: Duck typing, third-party classes, flexibility
# ABC: Shared implementation, runtime checks, strict contracts
# === BUILT-IN PROTOCOLS ===
from typing import Iterable, Iterator, Callable, Hashable, Sized
from collections.abc import Mapping, Sequence, SetCross-links
- Prerequisites: Type Hinting, Inheritance & MRO
- Related: Metaclasses - Class creation
- Related: Decorators -
@abstractmethod,@runtime_checkable - Next: Asyncio Fundamentals