You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django DRF序列化列表报'list'无user_project属性错误

问题场景

定义了存在多对一关联关系的Project和Shift两个模型,需要统计指定Project下所有Shift对象的相关指标。
预期实现流程:

  • 用户发送携带指定Project参数的GET请求
  • 后端调用统计函数计算该项目下所有Shift的统计指标,将结果存入列表后返回
  • 列表经序列化后响应给用户
    实际发起请求时触发报错:

AttributeError: 'list' object has no attribute 'user_project'
序列化模型实例或QuerySet时无异常,本次待序列化的是普通Python列表,已为返回值编写独立序列化器但始终无法正常运行,判断为序列化器编写错误,同时需要更优的实现方案。

关联代码

models.py

class Project(models.Model):
    user = models.ForeignKey('User', on_delete=models.CASCADE)
    task = models.CharField(max_length=128)
    technical_requirement = models.TextField()
    customer = models.CharField(max_length=64, blank=True)
    customer_email = models.EmailField(blank=True)
    start_of_the_project = models.DateField()
    salary_per_hour = models.FloatField()
    project_cost = models.FloatField(blank=True, default=0)
    project_duration = models.DurationField(blank=True, default=datetime.timedelta(0))

class Shift(models.Model):
    user_project = models.ForeignKey('Project', on_delete=models.CASCADE)
    shift_start_time = models.DateTimeField()
    shift_end_time = models.DateTimeField()
    shift_duration_time = models.DurationField(blank=True, null=True)
    salary_per_shift = models.FloatField(blank=True, null=True)

serializers.py(原错误版本)

class ShiftStatisticSerializer(serializers.Serializer):
    user_project = serializers.PrimaryKeyRelatedField(queryset=Project.objects.all(), many=True)
    number_of_shifts = serializers.IntegerField()
    number_of_hours = serializers.FloatField()
    duration_mean = serializers.FloatField()
    salary_mean = serializers.FloatField()
    project_cost = serializers.FloatField()

views.py(原错误版本)

class ShiftStatisticView(APIView):
    queryset = Shift.objects.all()
    authentication_classes = [authentication.JWTAuthentication]
    permission_classes = [permissions.IsAuthenticated]

def get(self, request):
    serializer = ShiftStatisticSerializer(data=request.data, partial=True)
    serializer.is_valid(raise_exception=True)
    valid = serializer.validated_data.pop('user_project')
    query = self.get_queryset()
    statistic = project_services.get_shift_statistics(validated_data=valid[0], queryset=query)
    serializer = ShiftStatisticSerializer(statistic)
    return Response(serializer.data)

def get_queryset(self):
    return self.queryset.filter(user_project_id=self.request.data['user_project'])

project_services.py(原错误版本)

def get_shift_statistics(validated_data, queryset):
    df = pd.DataFrame(list(queryset.values()))
    number_of_shifts = df["id"].count()
    number_of_hours = df["shift_duration_time"].sum() / np.timedelta64(1, 'h')
    duration_mean = df["shift_duration_time"].mean() / np.timedelta64(1, 'h')
    salary_mean = df["salary_per_shift"].mean()
    statistics = [validated_data,
                  number_of_shifts,
                  number_of_hours,
                  duration_mean,
                  salary_mean,
                  validated_data.project_cost,]
    return statistics

报错追踪

第一个报错完整栈信息:

Traceback (most recent call last):
  File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
  File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
    return view_func(*args, **kwargs)
  File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/views/generic/base.py", line 84, in view
    return self.dispatch(request, *args, **kwargs)
  File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/views.py", line 509, in dispatch
    response = self.handle_exception(exc)
  File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/views.py", line 469, in handle_exception
    self.raise_uncaught_exception(exc)
  File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception
    raise exc
  File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/views.py", line 506, in dispatch
    response = handler(request, *args, **kwargs)
  File "/Users/Tokio/Developer/ShiftBot/shift/views.py", line 83, in get
    return Response(serializer.data)
  File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/serializers.py", line 555, in data
    ret = super().data
  File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/serializers.py", line 253, in data
    self._data = self.to_representation(self.instance)
  File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/serializers.py", line 509, in to_representation
    attribute = field.get_attribute(instance)
  File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/relations.py", line 538, in get_attribute
    relationship = get_attribute(instance, self.source_attrs)
  File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/fields.py", line 97, in get_attribute
    instance = getattr(instance, attr)
AttributeError: 'list' object has no attribute 'user_project'

尝试移除many=True、queryset参数后触发第二个报错:

AssertionError: Relational field must provide a querysetargument, overrideget_queryset, or set read_only=True.
第二个报错完整栈信息:

Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/threading.py", line 1009, in _bootstrap_inner
    self.run()
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/threading.py", line 946, in run
    self._target(*self._args, **self._kwargs)
  File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/utils/autoreload.py", line 64, in wrapper
    fn(*args, **kwargs)
  File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/core/management/commands/runserver.py", line 134, in inner_run
    self.check(display_num_errors=True)
  File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/core/management/base.py", line 487, in check
    all_issues = checks.run_checks(
  File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/core/checks/registry.py", line 88, in run_checks
    new_errors = check(app_configs=app_configs, databases=databases)
  File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/core/checks/urls.py", line 14, in check_url_config
    return check_resolver(resolver)
  File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/core/checks/urls.py", line 24, in check_resolver
    return check_method()
  File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/urls/resolvers.py", line 480, in check
    for pattern in self.url_patterns:
  File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/utils/functional.py", line 49, in __get__
    res = instance.__dict__[self.name] = self.func(instance)
  File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/urls/resolvers.py", line 696, in url_patterns
    patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
  File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/utils/functional.py", line 49, in __get__
    res = instance.__dict__[self.name] = self.func(instance)
  File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/urls/resolvers.py", line 689, in urlconf_module
    return import_module(self.urlconf_name)
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1050, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1027, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1006, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 688, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 883, in exec_module
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "/Users/Tokio/Developer/ShiftBot/ShiftBot/urls.py", line 7, in <module>
    path('shift/', include('shift.urls')),
  File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/django/urls/conf.py", line 38, in include
    urlconf_module = import_module(urlconf_module)
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1050, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1027, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1006, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 688, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 883, in exec_module
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "/Users/Tokio/Developer/ShiftBot/shift/urls.py", line 2, in <module>
    from shift import views
  File "/Users/Tokio/Developer/ShiftBot/shift/views.py", line 10, in <module>
    from shift.serializers import ProjectSerializer, ShiftSerializer, ShiftStatisticSerializer
  File "/Users/Tokio/Developer/ShiftBot/shift/serializers.py", line 31, in <module>
    class ShiftStatisticSerializer(serializers.Serializer):
  File "/Users/Tokio/Developer/ShiftBot/shift/serializers.py", line 32, in ShiftStatisticSerializer
    user_project = serializers.PrimaryKeyRelatedField()
  File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/relations.py", line 252, in __init__
    super().__init__(**kwargs)
  File "/Users/Tokio/Developer/ShiftBot/venv/lib/python3.10/site-packages/rest_framework/relations.py", line 107, in __init__
    assert self.queryset is not None or kwargs.get('read_only'), (
AssertionError: Relational field must provide a `queryset` argument, override `get_queryset`, or set read_only=`True`.
根因分析
  • 序列化器默认按字段名从传入实例中取属性/键值,原服务层返回的是按位置排列的普通列表,既没有user_project属性,也不支持按键名取值,直接触发属性不存在错误。
  • user_project字段配置错误:单条统计结果对应单个Project,不需要many=True;该字段仅用于序列化输出,不需要反序列化校验逻辑,未设置read_only=True又去掉queryset时,DRF会触发断言报错。
  • GET请求参数错误从request.data读取,GET参数存放在request.query_params中,request.data仅用于POST/PUT等带请求体的请求场景。
  • 用pandas转DataFrame做聚合属于冗余操作,Django ORM内置的聚合函数性能更高,可避免额外的内存和类型转换开销。
  • 原逻辑未校验Project归属,存在越权访问风险,用户可传入任意Project ID查看其他用户的项目统计数据。
修复方案

1. 修正序列化器

输出场景下的关联字段设置read_only=True即可,不需要配置queryset,单条统计对应单个项目,移除many=True:

from rest_framework import serializers

class ShiftStatisticSerializer(serializers.Serializer):
    user_project = serializers.PrimaryKeyRelatedField(read_only=True)
    number_of_shifts = serializers.IntegerField()
    number_of_hours = serializers.FloatField()
    duration_mean = serializers.FloatField()
    salary_mean = serializers.FloatField()
    project_cost = serializers.FloatField()

如果不需要返回完整的关联对象序列化结果,仅需要返回项目ID,也可以直接用serializers.IntegerField()定义该字段。

2. 修正服务层逻辑

返回键名和序列化器字段一一对应的字典,不要返回位置列表;优先用ORM聚合替代pandas计算,性能更高:

from django.db.models import Count, Avg, Sum
from datetime import timedelta

# ORM聚合实现(推荐)
def get_shift_statistics(validated_project, queryset):
    stats = queryset.aggregate(
        number_of_shifts=Count("id"),
        total_duration=Sum("shift_duration_time"),
        duration_mean=Avg("shift_duration_time"),
        salary_mean=Avg("salary_per_shift")
    )
    # 时间间隔转小时单位
    total_hours = stats["total_duration"] / timedelta(hours=1) if stats["total_duration"] else 0
    avg_duration_hours = stats["duration_mean"] / timedelta(hours=1) if stats["duration_mean"] else 0
    
    return {
        "user_project": validated_project,
        "number_of_shifts": stats["number_of_shifts"],
        "number_of_hours": round(total_hours, 2),
        "duration_mean": round(avg_duration_hours, 2),
        "salary_mean": round(stats["salary_mean"], 2) if stats["salary_mean"] else 0,
        "project_cost": validated_project.project_cost
    }

# 原pandas逻辑修正版(不推荐)
# def get_shift_statistics(validated_data, queryset):
#     df = pd.DataFrame(list(queryset.values()))
#     number_of_shifts = df["id"].count()
#     number_of_hours = df["shift_duration_time"].sum() / np.timedelta64(1, 'h')
#     duration_mean = df["shift_duration_time"].mean() / np.timedelta64(1, 'h')
#     salary_mean = df["salary_per_shift"].mean()
#     return {
#         "user_project": validated_data,
#         "number_of_shifts": number_of_shifts,
#         "number_of_hours": number_of_hours,
#         "duration_mean": duration_mean,
#         "salary_mean": salary_mean,
#         "project_cost": validated_data.project_cost
#     }

3. 修正视图逻辑

GET参数从request.query_params读取,增加项目归属校验避免越权:

from rest_framework.views import
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 00:18:22