如何重构代码解决‘局部变量data在赋值前被引用’错误?
我写了两个功能逻辑相近的视图,分别按日期范围和月份范围返回Milk记录的聚合数据,但按月查询的视图抛出“局部变量'data'在赋值前被引用”错误,按日期查询的视图却能正常运行。以下是相关代码及问题详情:
相关代码
forms.py
class DateInput(forms.DateInput): input_type = 'date' class MonthInput(forms.DateTimeInput): input_type = 'month' class DateRangeInputForm(forms.Form): start = forms.DateField(widget=DateInput()) end = forms.DateField(widget=DateInput()) class MonthRangeInputForm(forms.Form): start = forms.DateField(widget=MonthInput()) end = forms.DateField(widget=MonthInput())
可正常运行的按日期查询视图
def milk_records_range_per_day(self, request): if request.method == "POST": form = forms.DateRangeInputForm(request.POST) if form.is_valid(): data = (models.Milk.objects.filter(milking_date__date__range=( form.cleaned_data['start'], form.cleaned_data['end'])) .annotate(date=functions.TruncDate("milking_date")) .values("date") .annotate(amt=Sum('amount_in_kgs'))) labels = [c['date'].strftime("%d-%m-%Y") for c in data] values = [x['amt'] for x in data] # 后续逻辑省略
报错的按月查询视图
def milk_records_range_per_month(self, request): if request.method == "POST": form = forms.MonthRangeInputForm(request.POST) if form.is_valid(): data = (models.Milk.objects.filter(milking_date__month__range=( form.cleaned_data['start'],form.cleaned_data['end'])) .annotate(month=functions.TruncMonth("milking_date")) .values("month") .annotate(amt=Sum('amount_in_kgs'))) labels = [c['month'].strftime("%m-%Y") for c in data] values = [x['amt'] for x in data] # 后续逻辑省略
Milk模型
class Milk(models.Model): amount_in_kgs = models.PositiveIntegerField(validators=[MinValueValidator(0), MaxValueValidator(50)]) milking_date = models.DateTimeField(auto_now_add=True) # 其他字段省略
问题原因分析
表单验证失败导致data未赋值:
MonthRangeInputForm使用input_type='month'的控件,提交的是YYYY-MM格式的字符串,但DateField默认无法解析这种格式,会导致form.is_valid()返回False,此时data变量没有被赋值,后续代码引用data就会抛出“局部变量'data'在赋值前被引用”的错误。而按日期查询的表单用的是date类型控件,提交的是完整日期,DateField能正常解析,所以表单验证大概率能通过。过滤条件错误:
milking_date__month__range是错误用法,__month提取的是月份数字(1-12),无法处理跨年份的月份范围(比如从2023-11到2024-02就会因为月份数字11>2而过滤不到数据)。
解决方案
步骤1:修正表单字段的日期解析格式
修改MonthRangeInputForm,给DateField指定支持YYYY-MM格式的input_formats参数,让表单能正确解析月份输入:
class MonthRangeInputForm(forms.Form): start = forms.DateField(widget=MonthInput(), input_formats=['%Y-%m']) end = forms.DateField(widget=MonthInput(), input_formats=['%Y-%m'])
步骤2:修正查询的过滤条件
把错误的milking_date__month__range改成基于完整日期范围的过滤,同时处理月份的首尾日期:
import calendar def milk_records_range_per_month(self, request): if request.method == "POST": form = forms.MonthRangeInputForm(request.POST) # 初始化data为空QuerySet,避免验证失败时无赋值 data = models.Milk.objects.none() if form.is_valid(): start_date = form.cleaned_data['start'] # 获取结束月份的最后一天 end_year, end_month = form.cleaned_data['end'].year, form.cleaned_data['end'].month end_month_last_day = calendar.monthrange(end_year, end_month)[1] end_date = form.cleaned_data['end'].replace(day=end_month_last_day) data = (models.Milk.objects.filter(milking_date__range=(start_date, end_date)) .annotate(month=functions.TruncMonth("milking_date")) .values("month") .annotate(amt=Sum('amount_in_kgs'))) labels = [c['month'].strftime("%m-%Y") for c in data] values = [x['amt'] for x in data] # 后续逻辑省略
步骤3:处理表单验证失败的情况
在if form.is_valid()之前给data赋默认值(比如空QuerySet),确保无论表单是否验证通过,data变量都有值,避免报错。
内容的提问来源于stack exchange,提问作者Peter Evance

