【Python面试题】写一个用元类(metaclass)实现API接口自动注册的Demo。以及装饰器在项目中典型应用场景。

一、用元类(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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值