M-Bus Gateway
← Tilbage til blog
· Python· ABC· abstract· OOP· design patterns· IoT· backend

Python Abstract Base Classes — ABC og abstractmethod mønstre

Python ABC og abstractmethod til IoT-platform: abstract repository pattern, plugin-system med ABCMeta, interface-håndhævelse, multiple inheritance med MRO og sammenligning med Protocol.

Af M-Bus Gateway

ABC og abstractmethod fra Python's abc-modul håndhæver interfaces ved instansiering. Her er de praktiske mønstre til IoT-platform og repository pattern.


ABC grundlæggende

from abc import ABC, abstractmethod
from typing import AsyncIterator
from datetime import datetime
import uuid


class MeterReader(ABC):
    """Abstract base class for alle måler-læsere."""

    @abstractmethod
    async def read(self, meter_id: str) -> float:
        """Læs aktuelt forbrug fra måler."""
        ...

    @abstractmethod
    async def read_history(
        self,
        meter_id: str,
        since: datetime,
    ) -> AsyncIterator[tuple[datetime, float]]:
        """Stream historiske aflæsninger."""
        ...

    @property
    @abstractmethod
    def protocol(self) -> str:
        """Protokol-identifikator (fx 'wmbus', 'modbus')."""
        ...

    def __init_subclass__(cls, **kwargs):
        """Hook: Kør validering ved subclass-definition."""
        super().__init_subclass__(**kwargs)
        if not hasattr(cls, "SUPPORTED_MANUFACTURERS"):
            raise TypeError(
                f"{cls.__name__} skal definere SUPPORTED_MANUFACTURERS"
            )


# Instansiering af ABC fejler:
reader = MeterReader()    # TypeError: Can't instantiate abstract class

Abstract repository pattern

# server/src/repositories/base.py

from abc import ABC, abstractmethod
from typing import TypeVar, Generic
import uuid

T = TypeVar("T")


class BaseRepository(ABC, Generic[T]):
    """
    Abstract repository — definerer CRUD-interface.
    Konkrete implementeringer: SQL, mock, cache-wrapper.
    """

    @abstractmethod
    async def get(self, id: uuid.UUID) -> T | None:
        ...

    @abstractmethod
    async def get_by_tenant(
        self,
        tenant_id: uuid.UUID,
        limit: int = 100,
        offset: int = 0,
    ) -> list[T]:
        ...

    @abstractmethod
    async def create(self, obj: T) -> T:
        ...

    @abstractmethod
    async def update(self, obj: T) -> T:
        ...

    @abstractmethod
    async def soft_delete(self, id: uuid.UUID, deleted_by: uuid.UUID) -> bool:
        ...

    @abstractmethod
    async def count(self, tenant_id: uuid.UUID) -> int:
        ...


# Konkret SQL-implementering:
class SQLReadingRepository(BaseRepository[Reading]):
    SUPPORTED_MANUFACTURERS = ["*"]

    def __init__(self, session: AsyncSession):
        self._session = session

    async def get(self, id: uuid.UUID) -> Reading | None:
        return await self._session.get(Reading, id)

    async def get_by_tenant(
        self,
        tenant_id: uuid.UUID,
        limit: int = 100,
        offset: int = 0,
    ) -> list[Reading]:
        stmt = (
            select(Reading)
            .where(Reading.tenant_id == tenant_id)
            .where(Reading.deleted_at.is_(None))
            .limit(limit)
            .offset(offset)
        )
        return (await self._session.execute(stmt)).scalars().all()

    # ... øvrige abstract methods implementeret ...


# Mock-implementering til tests:
class InMemoryReadingRepository(BaseRepository[Reading]):
    SUPPORTED_MANUFACTURERS = ["*"]

    def __init__(self):
        self._store: dict[uuid.UUID, Reading] = {}

    async def get(self, id: uuid.UUID) -> Reading | None:
        return self._store.get(id)

    async def create(self, obj: Reading) -> Reading:
        self._store[obj.id] = obj
        return obj

    # ... øvrige abstract methods ...

Plugin-system med ABCMeta

# gateway/src/drivers/base.py
# Plugin-system til wM-Bus-drivere for forskellige fabrikater

from abc import ABCMeta, abstractmethod


class DriverRegistry:
    """Registry over alle tilgængelige wM-Bus-drivere."""
    _drivers: dict[str, type] = {}

    @classmethod
    def register(cls, manufacturer_id: str):
        """Decorator til at registrere driver."""
        def decorator(driver_cls):
            cls._drivers[manufacturer_id] = driver_cls
            return driver_cls
        return decorator

    @classmethod
    def get(cls, manufacturer_id: str) -> type | None:
        return cls._drivers.get(manufacturer_id)


class WmbusDriverMeta(ABCMeta):
    """
    Metaclass der auto-registrerer drivere i DriverRegistry.
    Kræver MANUFACTURER_ID class-attribut.
    """
    def __new__(mcs, name, bases, namespace, **kwargs):
        cls = super().__new__(mcs, name, bases, namespace, **kwargs)

        # Registrér kun konkrete klasser (ikke ABC'en selv):
        if hasattr(cls, "MANUFACTURER_ID") and cls.MANUFACTURER_ID:
            DriverRegistry._drivers[cls.MANUFACTURER_ID] = cls

        return cls


class WmbusDriver(ABC, metaclass=WmbusDriverMeta):
    """Abstract base class for alle wM-Bus-drivere."""
    MANUFACTURER_ID: str = ""

    @abstractmethod
    def parse(self, raw_bytes: bytes) -> dict:
        """Parser rå telegram til struktureret data."""
        ...

    @abstractmethod
    def supports_encryption(self) -> bool:
        """Understøtter denne driver AES-128 CTR?"""
        ...

    @classmethod
    @abstractmethod
    def get_wmbusmeters_driver_name(cls) -> str:
        """Returnér wmbusmeters driver-navn (fx 'apator162')."""
        ...


# Konkrete drivere registreres automatisk:
class KamstrupDriver(WmbusDriver):
    MANUFACTURER_ID = "KAM"

    def parse(self, raw_bytes: bytes) -> dict:
        # Kamstrup-specifik parsing
        return {"manufacturer": "Kamstrup", "value": ...}

    def supports_encryption(self) -> bool:
        return True

    @classmethod
    def get_wmbusmeters_driver_name(cls) -> str:
        return "multical21"


class EngelmannDriver(WmbusDriver):
    MANUFACTURER_ID = "ELS"

    def parse(self, raw_bytes: bytes) -> dict:
        return {"manufacturer": "Engelmann", "value": ...}

    def supports_encryption(self) -> bool:
        return True

    @classmethod
    def get_wmbusmeters_driver_name(cls) -> str:
        return "sensostar"


# Brug:
driver_cls = DriverRegistry.get("KAM")
if driver_cls:
    driver = driver_cls()
    data = driver.parse(raw_telegram)

Abstract vs. Protocol — hvornår hvad?

# ABC — brug når:
#   - Du vil håndhæve interface ved instansiering (fejl ved oprettelse)
#   - Klasser skal eksplicit arve (nominativ subtyping)
#   - Du har fælles implementeret kode i base class
#   - Plugin-system med auto-registrering via metaclass

# Protocol — brug når:
#   - Eksisterende klasser skal bruges uden at ændre dem (duck typing)
#   - Tredjepartsklasser skal satisfie interface
#   - Type-checking uden runtime overhead
#   - Test-mocks der ikke skal arve

# Eksempel på hybrid (ABC + Protocol):
from typing import Protocol, runtime_checkable

@runtime_checkable
class Readable(Protocol):
    async def read(self, meter_id: str) -> float: ...


class MeterReader(ABC):
    """ABC giver fælles kode + håndhævet interface."""

    @abstractmethod
    async def read(self, meter_id: str) -> float: ...

    async def read_with_retry(self, meter_id: str, retries: int = 3) -> float:
        """Fælles implementering i base class."""
        for attempt in range(retries):
            try:
                return await self.read(meter_id)
            except ConnectionError:
                if attempt == retries - 1:
                    raise
                await asyncio.sleep(2 ** attempt)


# Verificér at implementation satisfier Protocol:
reader = WmbusReader()
assert isinstance(reader, Readable)    # True (runtime_checkable)

Multiple inheritance med MRO

# Forsigtigt med multiple inheritance — brug MRO korrekt:

class Serializable(ABC):
    @abstractmethod
    def to_dict(self) -> dict: ...

    @abstractmethod
    def to_json(self) -> str: ...


class Auditable(ABC):
    @abstractmethod
    def audit_info(self) -> dict: ...


class TimestampedRepository(BaseRepository[T], Auditable):
    """Multiple inheritance — kræver alle abstract methods."""

    def audit_info(self) -> dict:
        return {
            "created_at": datetime.utcnow().isoformat(),
            "class": type(self).__name__,
        }

    # + alle BaseRepository abstract methods ...


# MRO-check:
print(TimestampedRepository.__mro__)
# [TimestampedRepository, BaseRepository, Auditable, ABC, Generic, object]

# super() følger MRO automatisk:
class CachingRepository(TimestampedRepository[T]):
    def __init__(self, inner: BaseRepository[T], cache):
        self._inner = inner
        self._cache = cache

    async def get(self, id: uuid.UUID) -> T | None:
        cached = await self._cache.get(str(id))
        if cached:
            return cached
        result = await self._inner.get(id)
        if result:
            await self._cache.set(str(id), result, ttl=300)
        return result

Konklusion

ABC og abstractmethod håndhæver interfaces ved instansiering — runtime fejl ved forsøg på at oprette ufuldstændig klasse. Abstract repository pattern (BaseRepository[T]) definerer CRUD-interface og muliggør mock-implementeringer i tests. Plugin-systemer med ABCMeta auto-registrerer drivere (Kamstrup, Engelmann, Diehl) ved class-definition. Brug ABC til nominativ subtyping med fælles kode; brug Protocol til duck typing af eksisterende klasser.

Se Python Protocol guide eller FastAPI dependency injection guide.