为何Django DecimalField允许存储Float或字符串?如何避免用浮点数处理价格?
为什么Django DecimalField会返回非Decimal类型?怎么避免用浮点数处理价格?
这问题确实容易让人懵,我来给你理清楚背后的逻辑,再给你几个靠谱的解决方案。
先解释你看到的现象
你测试的三种创建方式返回不同类型的核心原因是:Django的字段转换/验证并不是在你赋值的瞬间就触发的。
当你用create()传入字符串或浮点数时:
- Django会把这些值转换成符合DecimalField要求的格式存入数据库(所以数据库里存的是正确的十进制值)
- 但内存中刚创建的
article实例,它的unit_price属性还是你传入的原始类型(字符串或浮点数)——因为没有触发字段的to_python()转换逻辑 - 只有当你重新从数据库获取这个实例(比如
article = Article.objects.get(pk=article.pk)),或者主动调用article.full_clean()触发验证,unit_price才会变成decimal.Decimal类型
而当你直接传入Decimal('2.3')时,因为已经是目标类型,所以内存中的属性自然就是Decimal。
确保永远不用浮点数处理价格的最佳实践
处理价格这类对精度敏感的数据,绝对要杜绝浮点数(浮点数的二进制存储会带来精度丢失),这里有几个关键方法:
1. 始终手动传入Decimal类型的值
在创建或更新实例时,主动把输入转换成decimal.Decimal,而且一定要用字符串作为Decimal的参数(避免浮点数转字符串时的精度问题):
from decimal import Decimal # 正确做法 article = Article.objects.create(unit_price=Decimal('2.3')) # 如果是从其他地方获取的数值(比如前端传的字符串),直接转Decimal price_str = request.POST.get('unit_price') article = Article.objects.create(unit_price=Decimal(price_str))
2. 强制触发字段验证与转换
在保存实例前调用full_clean(),这会让Django自动执行字段的to_python()转换,同时验证数据是否符合max_digits、decimal_places的要求,避免无效数据:
article = Article(unit_price="2.3") article.full_clean() # 这里会把unit_price转换成Decimal article.save() type(article.unit_price) # 此时就是decimal.Decimal
你也可以在模型的save方法里自动调用full_clean(),确保每次保存都经过验证:
from django.db import models from decimal import Decimal class Article(models.Model): unit_price = models.DecimalField(max_digits=9, decimal_places=2) def save(self, *args, **kwargs): self.full_clean() super().save(*args, **kwargs)
3. 使用表单/序列化器处理输入
如果是通过用户输入创建实例,用Django的ModelForm或者DRF的序列化器,它们会自动帮你完成类型转换和验证:
# 示例:ModelForm from django import forms class ArticleForm(forms.ModelForm): class Meta: model = Article fields = ['unit_price'] # 当你用表单数据创建实例时,已经是Decimal类型了 form = ArticleForm({'unit_price': '2.3'}) if form.is_valid(): article = form.save() type(article.unit_price) # decimal.Decimal
4. 绝对避免直接使用浮点数
永远不要用Decimal(2.3)这种方式转换浮点数,因为浮点数本身就有精度问题(比如2.3在二进制里是无限循环的),直接转Decimal会保留这种误差。如果必须处理浮点数,先把它转成字符串再转Decimal:
# 不推荐 bad_price = Decimal(2.3) # 会得到类似Decimal('2.29999999999999982236431605997495353221893310546875') # 推荐 good_price = Decimal(str(2.3)) # 得到Decimal('2.3')
内容的提问来源于stack exchange,提问作者David Dahan
相关产品推荐
相关产品推荐

