· python· functools· lru_cache· partial· reduce· cached_property· decorator· IoT· SaaS
Python functools — praktisk guide til IoT og SaaS
Python functools i IoT og SaaS: lru_cache konfiguration, partial til route factories, reduce til akkumulering, wraps decorator, cached_property og total_ordering til målersortring.
Af M-Bus Gateway
functools er et af Pythons mest undervurderede standardbiblioteker. Her er mønstrene der bruges dagligt i IoT-gateways og SaaS-backends.
lru_cache — memoization til dyre opslag
# server/src/config.py
from functools import lru_cache
from pydantic_settings import BaseSettings
@lru_cache(maxsize=1)
def get_settings() -> BaseSettings:
"""
Settings indlæses én gang — derefter fra cache.
maxsize=1: Kun ét unikt resultat gemmes (ingen args).
"""
return BaseSettings()
# Ryd cache i tests:
# get_settings.cache_clear()
# Cache-info (til debugging):
# get_settings.cache_info()
# → CacheInfo(hits=42, misses=1, maxsize=1, currsize=1)
@lru_cache(maxsize=256)
def get_zip_code_municipality(zip_code: str) -> str | None:
"""
ZIP → kommunekode opslag. Resultater caches pr. ZIP.
maxsize=256: Passer til antal postnumre i DK (574 i alt).
"""
from server.src.data.zip_codes import ZIP_TO_MUNICIPALITY
return ZIP_TO_MUNICIPALITY.get(zip_code)
# Vigtig regel: lru_cache virker kun på hashable argumenter.
# list, dict, set → TypeError. Brug tuple eller frozenset i stedet.
partial — route og task factories
# server/src/api/router_factory.py
from functools import partial
from fastapi import Depends
from server.src.auth.deps import require_role
# partial lader dig "forudfylde" argumenter til en funktion:
require_landlord = partial(require_role, allowed_roles=["landlord", "super_admin"])
require_admin = partial(require_role, allowed_roles=["super_admin"])
require_tech = partial(require_role, allowed_roles=["technician", "super_admin"])
# Brug i router:
# @router.get("/properties", dependencies=[Depends(require_landlord)])
# partial til Celery-tasks med forudfyldt parameter:
from celery import Celery
app = Celery()
def send_email_task(email_type: str, recipient: str, **kwargs) -> None:
pass # Email-logik
# Factory: Forudfyld email_type
send_settlement_email = partial(send_email_task, email_type="settlement")
send_alarm_email = partial(send_email_task, email_type="alarm")
send_reminder_email = partial(send_email_task, email_type="reminder")
# Brug:
# send_settlement_email(recipient="lejer@example.com", settlement_id="S-001")
reduce — akkumulering af fordelingsresultater
# server/src/distribution/aggregator.py
from functools import reduce
from decimal import Decimal
def total_allocated(allocations: dict[str, Decimal]) -> Decimal:
"""Summér alle fordelingsbeløb via reduce."""
return reduce(lambda acc, v: acc + v, allocations.values(), Decimal("0"))
def combine_distributions(
distributions: list[dict[str, Decimal]]
) -> dict[str, Decimal]:
"""
Kombinér flere fordelingsresultater (f.eks. varme + vand + el).
reduce folder listen til ét samlet dict med summerede beløb.
"""
def merge(acc: dict[str, Decimal], dist: dict[str, Decimal]) -> dict[str, Decimal]:
return {
unit_id: acc.get(unit_id, Decimal("0")) + amount
for unit_id, amount in dist.items()
}
return reduce(merge, distributions, {})
# Eksempel:
heat_dist = {"U-01": Decimal("1200.00"), "U-02": Decimal("980.00")}
water_dist = {"U-01": Decimal("340.00"), "U-02": Decimal("290.00")}
combined = combine_distributions([heat_dist, water_dist])
# → {"U-01": Decimal("1540.00"), "U-02": Decimal("1270.00")}
wraps — korrekte dekoratorer
# server/src/api/decorators.py
from functools import wraps
from typing import Callable, Any
import time
import structlog
log = structlog.get_logger()
def log_duration(func: Callable) -> Callable:
"""
Dekorator der logger kaldetid.
@wraps bevarer __name__, __doc__ og __wrapped__ på den originale funktion.
Uden @wraps: FastAPI/OpenAPI vil se "wrapper" i stedet for "get_properties".
"""
@wraps(func)
async def wrapper(*args: Any, **kwargs: Any) -> Any:
start = time.monotonic()
try:
result = await func(*args, **kwargs)
return result
finally:
duration_ms = (time.monotonic() - start) * 1000
log.info(
"endpoint_duration",
function=func.__name__,
duration_ms=round(duration_ms, 1),
)
return wrapper
def require_feature_flag(flag: str) -> Callable:
"""
Fabrik der returnerer en dekorator med forudfyldt flag-navn.
Kombinerer partial-mønsteret med @wraps.
"""
def decorator(func: Callable) -> Callable:
@wraps(func)
async def wrapper(*args: Any, **kwargs: Any) -> Any:
from server.src.config import get_settings
settings = get_settings()
if not getattr(settings, flag, False):
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="Feature not enabled")
return await func(*args, **kwargs)
return wrapper
return decorator
# Brug:
# @require_feature_flag("oioubl_enabled")
# async def get_oioubl_invoice(settlement_id: str): ...
cached_property — lazy instans-attributter
# gateway/src/wmbus/meter.py
from functools import cached_property
from dataclasses import dataclass
@dataclass
class MeterReading:
"""
cached_property: Beregnes første gang den tilgås,
derefter gemt som instansattribut (ingen genberegning).
Virker kun på klasser der tillader __dict__ (ikke frozen dataclasses).
"""
meter_id: str
total_kwh_raw: float
flow_temp_c: float
return_temp_c: float
@cached_property
def delta_temp(self) -> float:
"""ΔT = flow - retur. Beregnes kun én gang."""
return self.flow_temp_c - self.return_temp_c
@cached_property
def is_suspicious(self) -> bool:
"""
Simpel anomali-check der kombinerer multiple kriterier.
cached_property er ideel her: dyrt at genberegne, bruges sjældent.
"""
return (
self.delta_temp < 5.0 # For lille ΔT → hydraulisk problem
or self.total_kwh_raw < 0 # Negativt forbrug → backwards alarm
or self.total_kwh_raw > 50000 # Urealistisk højt → spike alarm
)
total_ordering — sammenligning af målere
# server/src/models/meter.py
from functools import total_ordering
from datetime import date
@total_ordering
class MeterMidStatus:
"""
total_ordering: Definer __eq__ og ét sammenligningsoperator,
resten genereres automatisk (__le__, __gt__, __ge__).
Bruges til at sortere målere efter MID-udløbsdato.
"""
def __init__(self, meter_id: str, mid_expiry: date):
self.meter_id = meter_id
self.mid_expiry = mid_expiry
def __eq__(self, other: object) -> bool:
if not isinstance(other, MeterMidStatus):
return NotImplemented
return self.mid_expiry == other.mid_expiry
def __lt__(self, other: "MeterMidStatus") -> bool:
if not isinstance(other, MeterMidStatus):
return NotImplemented
return self.mid_expiry < other.mid_expiry
# Sortér liste af målere efter MID-udløb (tidligst udløb først):
meters = [
MeterMidStatus("KAM-001", date(2028, 6, 1)),
MeterMidStatus("ELS-002", date(2026, 3, 15)),
MeterMidStatus("TCH-003", date(2027, 9, 1)),
]
sorted_meters = sorted(meters) # ELS-002, TCH-003, KAM-001
Konklusion
functools giver seks kraftfulde værktøjer: lru_cache til singleton og memoization, partial til route-factories og pre-filled Celery tasks, reduce til distribution-aggregering, wraps til korrekte dekoratorer (kritisk for FastAPI/OpenAPI), cached_property til lazy beregnede instansattributter og total_ordering til automatisk genererede sammenligningsoperatorer. Alle seks bruges i produktionskoden.