手动用extend_schema装饰函数引发RecursionError问题排查
1. 动态装饰引发递归错误的核心原因
你在__init__方法中每次实例化视图集时,都会用extend_schema重新装饰一次list_history方法。但extend_schema的本质是给原方法套一层包装函数,每次装饰都会生成一个新的包装层。当drf-spectacular多次生成schema(比如缓存失效、多次请求schema接口)时,视图集被反复实例化,list_history方法会被层层嵌套装饰。随着嵌套层数不断累积,最终超过Python的递归深度限制,触发RecursionError。
drf-spectacular在处理schema时,会调用方法的装饰器链来解析接口元数据,多层嵌套的装饰器会导致get_operation和is_excluded等方法在遍历装饰器链时陷入无限递归。
2. 报错偶发的原因
drf-spectacular默认会对生成的schema进行缓存,前几次刷新swagger页面时,直接读取缓存的schema结果,不会重新触发视图集实例化和装饰逻辑。当缓存过期、或者服务器进程中多次处理schema生成请求时,视图集被多次实例化,list_history的装饰层数逐渐累积,直到超过递归深度阈值才会报错,因此表现为偶发。
不要在__init__中动态装饰方法,改用类级别的extend_schema_view来实现延迟绑定序列化器,这样只会在类定义时处理一次装饰逻辑,不会重复嵌套:
from drf_spectacular.utils import extend_schema, extend_schema_view from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.response import Response class HistoricalModelViewSet(viewsets.ModelViewSet): # 子类必须定义这个属性 historical_serializer_class = None @action(detail=False, methods=["get"], url_path="history", url_name="list-history-list") def list_history(self, request): # 补充你的history_items和context获取逻辑 history_items = self.get_queryset().history.all() context = self.get_serializer_context() serializer = self.historical_serializer_class(history_items, many=True, context=context) return Response(serializer.data) # 用extend_schema_view实现子类序列化器的动态绑定 schema = extend_schema_view( list_history=extend_schema( responses=lambda self: {200: self.historical_serializer_class(many=True)} ) )
这样修改后,extend_schema的responses参数通过lambda延迟求值,只有在生成schema时才会访问实例的historical_serializer_class,同时只会对list_history方法装饰一次,彻底避免重复嵌套导致的递归错误。
另外,也可以在子类中显式给list_history添加extend_schema装饰,这种方式更直观,但需要每个子类都重复写装饰代码:
class StuffViewSet(HistoricalModelViewSet): historical_serializer_class = StuffHistoricalSerializer @extend_schema(responses={200: StuffHistoricalSerializer(many=True)}) def list_history(self, request): return super().list_history(request)
内容的提问来源于stack exchange,提问作者himmip

