Skip to content

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.Protocol cho 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 runtime

Structural vs Nominal Typing

Nominal Typing (Inheritance-based)

Type được xác định bởi tên classinheritance 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 Animal

Structural 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))  # ✅ OK

Protocol 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))           # ✅ OK

Protocol 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])     # ✅ float

Callable 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.00

Abstract 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))  # False

Protocol vs ABC: Khi nào dùng gì?

So sánh Chi tiết

AspectProtocolABC
TypingStructuralNominal
InheritanceKhông cầnBắt buộc
Runtime checkKhông (chỉ static)Có (isinstance)
Default implementationKhông
FlexibilityCaoThấ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 code

Khi 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 entity

Hybrid 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 typingcollections.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")            # ✅ str

Real-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 runtime

2. 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)  # True

4. 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, Set