一、用元类(metaclass)在类定义时,自动把接口注册到路由表
from typing import Callable, Dict, List, Optional, Tuple
import re
class Router:
"""
超级轻量路由器:保存(method, path) -> 的映射,并支持{param}路径参数
"""
_routes: List[Dict] = []
@classmethod
def _complie(cls, path: str):
"""将/users/{user_id}转换为正则,并记录参数名"""
param_names: List[str] = []
def repl(m: re.Match):
name = m.group(1)
param_names.append(name)
return rf"(?P<{name}>[^/]+)"
pattern = "^" + re.sub(r"\{(\w+)\}", repl, path.rstrip("/")) + "/?$"
return re.complie(pattern), param_names
@classmethod
def add_route(
cls,
method: str,
path: str,
handler_factory: Callable[[], Callable],
name: Optional[str] = None,
):
method = method.upper()
pattern, _ = cls._complie(path)
# 简单的去重保护:同方法+同路径正则不允许重复
for r in cls._routes:
if r["method"] == method and r["pattern"].pattern == pattern.pattern:
raise ValueError(f"Duplicate route: {method} {path}")
cls._routes.append(
{
"method": method,
"pattern": pattern,
"factory": handler_factory,
"name": name or path,
"raw_path": path,
}
)
@classmethod
def dispatch(cls, method: str, path: str, **extra):
"""根据method/path匹配到的handler,并非调用extra可传body、json等"""
method = method.upper()
for r in cls._routes:
if r["method"] != method:
continue
m = r["pattern"].match(path)
if m:
handler = r["factory"]()
params = {**m.groupdict(), **extra}
return handler(**params)
raise KeyError(f"Route not found:{method} {path}")
@classmethod
def url_map(cls) -> List[Tuple[str, str, str]]:
"""用于观察当前注册了哪些路由"""
return [(r["method"], r["raw_path"], r["name"]) for r in cls._routes]
# ---关键:定义一个@router装饰器,只做打标签,不做注册
def route(method: str, path: str, *, name: Optional[str] = None):
def deco(func):
setattr(
func, "_route_info", {"method": method.upper(), "path": path, "name": name}
)
return func
return deco
# --核心:元类,在类创建的时候,扫描被@router标记的方法并自动注册到Router--
class APIMeta(type):
def __new__(mcls, name, bases, namespace, **kwargs):
"""
这个作用是在类被创建的时候,扫描类体里被@route打了标签的方法,然后把他们自动注册到全局路由里面Router
元类的__new__何时被调用,当我们在写以下代码的时候,
class UserAPI(BaseAPI):
prefix = "/users"
@route("GET", "/")
def list(self): ...
类体先被执行(相当于运行一段脚本)
- prefix 被放进局部字典 namespace
- list 函数被创建;@route()装饰器会给他增加属性 ._route_info = {。。。}
随后,Python用元类(这里是指APIMeta)调用
"""
# 先让type.__new__按照常规规则创建出类对象cls
cls = super().__new__(mcls, name, bases, namespace, **kwargs)
# 跳过抽象基类,如果类体里面写了 __abstract__ = True,代表这是抽象基类,不参与路由注册,直接返回
if namespace.get("__abstract__", False):
return cls
# 取类上的prefix(例如 “/user”),去掉末尾的斜杠,准备拼接路径
prefix = getattr(cls, "prefix", "").rstrip("/")
# 遍历类体中所有的成员,找到所有可调用并且有_route_info属性的函数,这个_route_info就是我们在装饰器@route里面打的标签
for attr_name, attr_val in namespace.items():
if callable(attr_val) and hasattr(attr_val, "_route_info"):
# 取出装饰器里面写的HTTP方法和相对路径
# 与类级别的prefix 合成完整的路径(保证每一个类的一组接口自动带上统一前缀)
# 生成一个路由名,(如果没有指定,就用类名.类方法)
info = attr_val._troute_info.copy()
method = info["method"]
path = (
f"{prefix}{info['path']}"
if info["path"].startswith("/")
else f"{prefix}/{info['path']}"
)
name_for_route = info.get("name") or f"{name}.{attr_name}"
# 为什么要有factory?
# 请求来的时候,我们需要绑定到实例的可调用对象, 即obj.method,这样方法里的self是可用的,且你可以在实例里面持有状态/依赖
# 这里不直接创建实例,而是注册一个“工厂函数”,等待Router.dispath(.....)真是匹配到路由的时候,再调用这个工厂去拿到最新的实例+已绑定的方法
# 这样做的好处就是 每次请求新建实例,天然的现成安全,容易做依赖注入,以及避免在类创建阶段就实例化(那个时候还没有上下文)
# 闭包参数 make_factory(cls, attr_name) 用参数吧cls/方法名固定住,避免Python闭包“玩绑定”坑
def make_factory(_cls, _method_name):
def factory():
obj = _cls()
return getattr(obj, _method_name)
return factory
# 把路由真正注册到Router里面,
# method:请求方式, path:合成之后的路径,handler_factory:后续分发时用它获得真正的处理函数,name:用来观测和反差
Router.add_route(
method,
path,
handler_factory=make_factory(cls, attr_name),
name=name_for_route,
)
# 最后,返回这个类对象
return cls
# -- 抽象基类,所有API类都从这里继承
#
class BaseAPI(metaclass=APIMeta):
__abstract__ = True # 避免被注册
# 业务示例
class UserAPI(BaseAPI):
prefix = "/users"
@route("GET", "/")
def list(self):
return [{"id": 1, "name": "lg"}, {"id": 2, "name": "zhang"}]
@route("GET", "/{user_id}")
def get(self, user_id: str):
return {"id": user_id, "name": "lg"}
@route("POST", "/")
def create(self, json: Dict):
return {"id": 1, "name": "lg"}
class HealthAPI(BaseAPI):
prefix = "/health"
@route("GET", "/ping")
def ping(self):
return {"status": "ok"}
if __name__ == "__main__":
# 观察路由表(由元类在导入/定义类时自动填充)
print("URL Map:", Router.url_map())
# 模拟请求分发
print("GET /users ->", Router.dispatch("GET", "/users"))
print("GET /users/2 ->", Router.dispatch("GET", "/users/2"))
print("POST /users ->", Router.dispatch("POST", "/users", json={"name": "Linus"}))
print("GET /health/ping ->", Router.dispatch("GET", "/health/ping"))
设计要点:
1.职责分离:@route仅做打标签,真正的注册于装配由元类在类创建阶段统一完成,避免在每个方法里去触碰全局状态
2.类上支持prefix,方法上支持name,route支持{param}路径参数和重复路由冲突检查
3.实例绑定策略,通过handler_factory()保证每次分发可新建实例,天然适配带有状态/依赖注入的场景(也可以改为单利缓存)
4.把Router.add_router换成Flask/fastapi的注册调用即可无缝衔接现有框架;元类负责发现,具体接线交给适配层
装饰器在项目中的典型应用场景
1.鉴权于鉴别
def require_role(role: str):
def deco(fn):
def wrapper(*args, **kwargs):
user = kwargs.get("user")
if not user or role not in user.role:
raise Exception("权限不足")
return fn(*args, **kwargs)
return wrapper
return deco
2.入参校验/模式约束(Valldation)
def validate_json(schema):
def deco(func):
def wrapper(*args, **kwargs):
data = kwargs.get("json", {})
missing = [k for k in schema.get("required", []) if k not in data]
if missing:
raise ValueError(" missing fields:{missing}")
return func(*args, **kwargs)
return wrapper
return deco
3.日志埋点
def log_timing(event_name: str):
def deco(func):
def wrapper(*args, **kwargs):
t0 = time.time()
try:
return func(*args, **kwargs)
finally:
cost = (time.time() - t0) * 1000
print(f"{event_name} took {time.time() - t0:.2f}s")
return wrapper
return deco
4.缓存
from functools import lru_cache
@lru_cache(maxsize=128)
def get_user(user_id: str):
print("get_user")
return {"id": user_id, "name": "lg"}
5.重试与回退
import time, random
def retry(times=3, base=0.1):
def deco(func):
def wrapper(*args, **kwargs):
last = None
for i in range(times):
try:
return func(*args, **kwargs)
except Exception as e:
last = e
time.sleep(base *(2 ** i) * (1 + random.random()))
raise last
return wrapper
return deco
6.事务与资源管理
def transactional(session_factory):
def deco(func):
def wrapper(*args, **kwargs):
session = session_factory()
try:
result = func(*args, session,**kwargs)
session.commit()
return result
except Exception as e:
session.rollback()
raise e
finally:
session.close()
return wrapper
return deco
实现API接口自动注册的Demo。以及装饰器在项目中典型应用场景。&spm=1001.2101.3001.5002&articleId=150501157&d=1&t=3&u=b0cb9e796bb64169a876e799aca4f013)
657

被折叠的 条评论
为什么被折叠?



