Django模型验证器介绍与源码分析


Posted in Python onSeptember 08, 2020

前言

在Django的模型字段参数中,有一个参数叫做validators,这个参数是用来指定当前字段需要使用的验证器,也就是对字段数据的合法性进行验证,比如大小、类型等。

Django的验证器可以分为模型相关的验证器和表单相关的验证器,它们基本类似,但在使用上有区别。

本文讨论的是模型相关的验证器。

一、自定义验证器

一个验证器其实就是一个可调用的对象(函数或类),接收一个初始输入值作为参数,对这个值进行一系列逻辑判断,如果不满足某些规则或者条件,则表示验证不通过,抛出一个ValidationError异常。如果满足条件则通过验证,不返回任何内容(也就是默认的return None),可以继续下一步。

验证器具有重要作用,可以被重用在别的字段上,是工具类型的逻辑封装。

下面是一个验证器的例子,它只允许偶数通过验证:

from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _

def validate_even(value):
 if value % 2 != 0:
 raise ValidationError(
  _('%(value)s is not an even number'),
  params={'value': value},
 )

通过下面的方式,将偶数验证器应用在字段上:

from django.db import models

class MyModel(models.Model):
 even_field = models.IntegerField(validators=[validate_even])

因为验证器运行之前,(输入的)数据会被转换为 Python 对象,因此我们可以将同样的验证器用在 Django form 表单中(事实上Django为表单提供了另外一些验证器):

from django import forms

class MyForm(forms.Form):
 even_field = forms.IntegerField(validators=[validate_even])

你还可以通过Python的魔法方法__cal__()编写更复杂的可配置的验证器,比如Django内置的RegexValidator验证器就是这么干的。

验证器也可以是一个类,但这时候就比较复杂了,需要确保它可以被迁移框架序列化,确保编写了deconstruction()和__eq__()方法。这种做法很难找到参考文献和博文,要靠自己摸索或者研究DJango源码。

二、工作机制

让我们来测试一下上面写的验证器:

>>> from .models import MyModel
>>> a = MyModel.objects.create(even_field=3)
>>> a
<MyModel: MyModel object (1)>
>>> a.even_field
3

什么?!!!不是说只有偶数才能通过验证吗?这里我提供了数字3,可是为什么创建成功了??

我们接着在admin站点中注册MyModel模型,然后在图形化界面后台中创建MyModel的实例,你会发现这个时候验证器起作用了,奇数是无法通过表单验证的!

Django模型验证器介绍与源码分析

为什么会这样??

这就要从Django的源码说起!

Django是这么设计的:

  • 模型的验证器不会在调用save()方法的时候自动执行
  • 表单的验证器会在调用save()方法的时候自动执行

为什么这么设计?个人猜测,Django官方为了序列化、链式调用等功能的兼容性,没有自动进行验证操作。

这个设计在源码中是怎么体现的?

  • Django的模型相关源码中,没有is_valid()方法,也不会自动调用full_clean() 方法,所以Django不会自动进行模型验证。但是它依然提供了四个重要的验证方法,也就是full_clean() 、clean_fields() 、clean() 和validate_unique(),一会细说
  • Django的表单系统forms的相关源码中,表单在save之前会自动执行一个is_valid()方法,这个方法里会调用验证器。

表单的内容在其它章节中讲解。

下面介绍Django模型的验证步骤和四个方法:

模型验证的步骤:

  • 如果你手动调用了full_clean()方法,那么会依次自动调用下面的三个方法
  • clean_fields():验证各个字段的合法性
  • clean():验证模型级别的合法性
  • validate_unique():验证字段的独一无二性

本质上,后面三个方法是具体实现,full_clean()是领头羊,实际操作中,你完全可以具体使用其中一个或多个。用了full_clean()就等于后面三个都用。

full_clean()

签名:Model.full_clean(exclude=None, validate_unique=True)

  • exclude用于指定某些字段不进行验证,也就是所谓的例外字段
  • validate_unique用于指定是否调用validate_unique()方法

让我们看下它的源代码:

def full_clean(self, exclude=None, validate_unique=True):

 errors = {}
 if exclude is None:
  exclude = []
 else:
  exclude = list(exclude)

 try:
  self.clean_fields(exclude=exclude) #1
 except ValidationError as e:
  errors = e.update_error_dict(errors)

 try:
  self.clean() #2
 except ValidationError as e:
  errors = e.update_error_dict(errors)

 if validate_unique:
  for name in errors:
  if name != NON_FIELD_ERRORS and name not in exclude:
   exclude.append(name)
  try:
  self.validate_unique(exclude=exclude) #3
  except ValidationError as e:
  errors = e.update_error_dict(errors)

 if errors:
  raise ValidationError(errors)

可以看出,它依次调用了其它三个方法,如果最后的errors中有内容,则抛出ValidationError异常。

我们最好不要去修改full_clean()方法的源代码,一般也不用重写它,直接调用即可。

模型的save()方法不会自动调用full_clean()方法,你必须手动调用。

如果调用验证器后,抛出ValidationError异常,Django会将所有的异常信息放置在e.message_dict字典中供使用。比如下面的例子:

# 在视图中我们可以这么做
from django.core.exceptions import ValidationError
try:
 article.full_clean()
except ValidationError as e:
 # 在这里做一些异常处理操作
 pass

在模型定义中我们可以如下重写save()方法,实现自动验证功能,不需要在视图中反复调用了:

# models.py
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _

def validate_even(value):
 if value % 2 != 0:
 raise ValidationError(
  _('%(value)s is not an even number'),
  params={'value': value},
 )
 
from django.db import models

class MyModel(models.Model):
 even_field = models.IntegerField(validators=[validate_even])

 def save(self, *args, **kwargs): # 重写save方法是关键
 try:
  self.full_clean() 
  super().save(*args, **kwargs)
 except ValidationError as e:
  print('模型验证没通过: %s' % e.message_dict)

执行过程展示:

>>> from .models import MyModel
>>> a = MyModel.objects.create(even_field=5)
模型验证没通过: {'even_field': ['5 is not an even number']}

这样,我们就实现了自动的模型验证。

小技巧:可以通过打印e来查看,Django怎么封装的错误信息,给我们提供了哪些键值,比如上例中,我们可以使用e.message_dict['even_field']。

clean_fields()

签名:Model.clean_fields(exclude=None)

参数同上,看下它的源代码:

def clean_fields(self, exclude=None):

  if exclude is None:
   exclude = []

  errors = {}
  for f in self._meta.fields:
   if f.name in exclude:
    continue

   raw_value = getattr(self, f.attname)
   if f.blank and raw_value in f.empty_values:
    continue
   try:
    setattr(self, f.attname, f.clean(raw_value, self)) #核心是这一句
   except ValidationError as e:
    errors[f.name] = e.error_list

  if errors:
   raise ValidationError(errors)

我们最好也不要去修改和重写它的源代码。

这个方法本质上就是循环模型中的所有字段,找出其中定义了验证器的那些,并执行它们。

我们前面自定义的偶数验证器,其实就是在这里被调用的。

clean()

这个方法很特别,我们看看它的源代码:

def clean(self):
  """
  Hook for doing any extra model-wide validation after clean() has been
  called on every field by self.clean_fields. Any ValidationError raised
  by this method will not be associated with a particular field; it will
  have a special-case association with the field defined by NON_FIELD_ERRORS.
  """
  pass

什么都没有!实际上,这个方法是给你留了个钩子,你需要重写它,然后在里面编写模型级别的验证,比如修改模型的属性,以及跨字段相关的验证逻辑。

下面我们通过一个例子来展示它的用法:

import datetime
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _

class Article(models.Model):
 content = models.TextField()
 status = models.CharField(max_length=32)
 pub_date = models.DateField(blank=True, null=True)

 def clean(self):
  # 不允许草稿文章具有发布日期字段
  if self.status == '草稿' and self.pub_date is not None:
   raise ValidationError(_('草稿文章尚未发布,不应该有发布日期!'))
  # 如果已发布的文章还没有设置发布日期,则将发布日期设置为当天
  if self.status == '已发布' and self.pub_date is None:
   self.pub_date = datetime.date.today()

# 更多Django技术文章请访问https://www.liujiangblog.com

说明:

  • gettext_lazy在这里无关紧要
  • 在Article模型中重写了clean方法,它不需要接受其它参数
  • 第一个if判断,不允许草稿文章具有发布日期字段。如果你提供了,对不起,抛出ValidationError异常
  • 第二个if判断,如果已发布的文章还没有设置发布日期,则将发布日期设置为当天
  • 这是一个跨字段的,全局性的验证方法,它不像我们一开始自定义的验证器那样,不是作为一个验证器参数进行提供,而是写在clean方法中了,一定要注意区别。

clean()方法写好了,我们就可以在Article模型中重写save()方法了:

def save(self, *args, **kwargs):

  from django.core.exceptions import NON_FIELD_ERRORS

  try:
   self.full_clean()
   super().save(*args, **kwargs)
  except ValidationError as e:
   print('验证没通过: %s' % e.message_dict[NON_FIELD_ERRORS])

注意:这里我们导入了NON_FIELD_ERRORS,在最后打印了e.message_dict[NON_FIELD_ERRORS],这是为什么呢?

因为,clean()中编写的都是模型级别、跨字段的验证方法,没有具体和某个字段绑定,所以Django提供了一个NON_FIELD_ERRORS关键字,用来说明这不是某个字段引起的异常,而是非字段相关的错误。

如果你非要将错误定位到某个具体的字段,也不是不可以的,如下例子所示:

class Article(models.Model):
 ...
 def clean(self):
  if self.status == '草稿' and self.pub_date is not None:
   raise ValidationError({'pub_date': _('草稿文章尚未发布,不应该有发布日期!')})
  ...

甚至,你可以如下方式,映射字段和错误信息:

raise ValidationError({
 'title': ValidationError(_('Missing title.'), code='required'),
 'pub_date': ValidationError(_('Invalid date.'), code='invalid'),
})

这些技巧,本质上就是给ValidationError异常类提供信息参数。

validate_unique()

签名:Model.validate_unique(exclude=None)

它的源代码也很简单:

def validate_unique(self, exclude=None):

  unique_checks, date_checks = self._get_unique_checks(exclude=exclude)

  errors = self._perform_unique_checks(unique_checks)
  date_errors = self._perform_date_checks(date_checks)

  for k, v in date_errors.items():
   errors.setdefault(k, []).extend(v)

  if errors:
   raise ValidationError(errors)

这个方法类似clean_fields(),只不过它只用来验证模型中的唯一性约束是否满足,而不是字段的值是否满足验证需求。

如果你提供了exclude参数,那么该参数包含的所有字段都不会进行唯一性验证。

我们最好也不要去修改和重写它的源代码。

总结

Django中模型验证器的使用套路:

  • 编写字段级别的验证器,在字段中作为参数指定
  • 或者编写clean()方法,实现模型级别、跨字段的验证功能
  • 重写save()方法,调用full_clean(),实现全自动的验证
  • 或者在视图中,通过模型实例调用full_clean()方法,实现手动验证

三、内置验证器

验证器的作用很重要,需求也很广泛,Django为此内置了一些验证器,我们直接拿来使用即可:

RegexValidator

这是正则匹配验证器。用于对输入的值进行正则搜索,如果命中,则平安无事,如果没命中则弹出 ValidationError 异常。

数字签名:class RegexValidator(regex=None, message=None, code=None, inverse_match=None, flags=0)

参数说明:

  • regex:用于匹配的正则表达式
  • message:自定义异常错误信息。默认是"Enter a valid value"
  • code:自定义错误码。默认是"invalid"
  • inverse_match:将通过和不通过验证的判断逻辑反转。也就是未命中则平安,命中则出错。
  • flags:编译正则表达式时使用的正则flags。默认为0。

EmailValidator

数字签名:class EmailValidator(message=None, code=None, whitelist=None)

邮件格式验证器。

参数说明:

  • message: 自定义错误信息,默认为 "Enter a valid email address"。
  • code: 自定义错误码,默认为"invalid"。
  • whitelist:邮件域名白名单,默认为['localhost']。

URLValidator

数字签名:class URLValidator(schemes=None, regex=None, message=None, code=None)

RegexValidator的子类,用于验证url的格式是否正确。

schemes:指定URL/URI的协议模式,默认值为['http', 'https', 'ftp', 'ftps']

validate_email

EmailValidator的一个实例,未做任何自定义。

validate_slug

一个确保输入值是字母、数字、下划线和连字符组合的RegexValidator的实例。

validate_unicode_slug

上面的Unicode编码版本

validate_ipv4_address

一个RegexValidator的实例,用于判断输入值是否为ipv4格式

validate_ipv6_address

上面的ipv6版本

validate_ipv46_address

同时支持ipv4和ipv6

validate_comma_separated_integer_list

判断输入是否是一个以逗号分隔的数字列表,一个RegexValidator的实例。

int_list_validator

数字签名:int_list_validator(sep=', ', message=None, code='invalid', allow_negative=False)

判断一个由数字组成的字符串是否以指定的sep分隔。allow_negative用于反转判断逻辑。

MaxValueValidator

签名:class MaxValueValidator(limit_value, message=None)

是否超过指定最大值

MinValueValidator

签名:class MinValueValidator(limit_value, message=None)

是否小于指定的最小值

MaxLengthValidator

签名:class MaxLengthValidator(limit_value, message=None)

输入值的长度是否超过限定值

MinLengthValidator

输入值的长度是否小于限定值

DecimalValidator

签名:lass DecimalValidator(max_digits, decimal_places)

数字验证器。当发生下面情况时弹出异常:

  • 输入值超过max_digits
  • 输入值的位数超过decimal_places
  • 输入值大于最大位数与小数位数之差。(待确认)

FileExtensionValidator

签名:class FileExtensionValidator(allowed_extensions, message, code)

文件扩展名不在合法性列表中。合法性列表通过参数allowed_extensions指定。

validate_image_file_extension

通过pillow库确定一个图片文件的扩展名是合法的

ProhibitNullCharactersValidator

签名:class ProhibitNullCharactersValidator(message=None, code=None)

对输入值进行 str(value) 操作,转换成字符串,然后如果这个字符串中包含1个以上的空字符'\x00',则验证失败。

更多特性请参考官方文档

更多技术文章请访问: https://www.liujiangblog.com

更多视频教程请访问: https://www.liujiangblog.com/video/

总结

到此这篇关于Django模型验证器介绍与源码分析的文章就介绍到这了,更多相关Django模型验证器与源码内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Python 相关文章推荐
python实现模拟按键,自动翻页看u17漫画
Mar 17 Python
从Python的源码浅要剖析Python的内存管理
Apr 16 Python
通过数据库对Django进行删除字段和删除模型的操作
Jul 21 Python
django从请求到响应的过程深入讲解
Aug 01 Python
破解安装Pycharm的方法
Oct 19 Python
python numpy数组的索引和切片的操作方法
Oct 20 Python
Django框架orM与自定义SQL语句混合事务控制操作
Jun 27 Python
Python搭建Spark分布式集群环境
Jul 05 Python
TensorFlow实现打印每一层的输出
Jan 21 Python
python环境下安装opencv库的方法
Mar 05 Python
PyQt5 界面显示无响应的实现
Mar 26 Python
python scrapy简单模拟登录的代码分析
Jul 21 Python
Python unittest discover批量执行代码实例
Sep 08 #Python
Python selenium实现断言3种方法解析
Sep 08 #Python
什么是Python包的循环导入
Sep 08 #Python
聊聊python中的循环遍历
Sep 07 #Python
详解python中的闭包
Sep 07 #Python
python logging模块的使用
Sep 07 #Python
了解一下python内建模块collections
Sep 07 #Python
You might like
php变量作用域的深入解析
2013/06/03 PHP
PHP临时文件的安全性分析
2014/07/04 PHP
php生成带logo二维码方法小结
2016/04/08 PHP
PHP二维索引数组的遍历实例分析【2种方式】
2019/06/24 PHP
javascript之大字符串的连接的StringBuffer 类
2007/05/08 Javascript
JavaScript采用递归算法计算阶乘实例
2015/08/04 Javascript
NodeJS实现阿里大鱼短信通知发送
2016/01/17 NodeJs
懒加载实现的分页&amp;&amp;网站footer自适应
2016/12/21 Javascript
利用jquery禁止外层滚动条的滚动
2017/01/05 Javascript
Vue.js实战之通过监听滚动事件实现动态锚点
2017/04/04 Javascript
JavaScript数据结构之广义表的定义与表示方法详解
2017/04/12 Javascript
基于JavaScript实现活动倒计时效果
2017/04/20 Javascript
基于Vue2.0+ElementUI实现表格翻页功能
2017/10/23 Javascript
js实现HTML中Select二级联动的实例
2018/01/05 Javascript
使用veloticy-ui生成文字动画效果
2018/02/08 Javascript
vue 自定义提示框(Toast)组件的实现代码
2018/08/17 Javascript
vue 解决循环引用组件报错的问题
2018/09/06 Javascript
angularjs http与后台交互的实现示例
2018/12/21 Javascript
通过实例解析js简易模块加载器
2019/06/17 Javascript
Vue + Node.js + MongoDB图片上传组件实现图片预览和删除功能详解
2020/04/29 Javascript
vue3使用vue-count-to组件的实现
2020/12/25 Vue.js
[39:18]完美世界DOTA2联赛PWL S3 Forest vs LBZS 第二场 12.17
2020/12/19 DOTA
用Python的urllib库提交WEB表单
2009/02/24 Python
Python通过PIL获取图片主要颜色并和颜色库进行对比的方法
2015/03/19 Python
Python的iOS自动化打包实例代码
2018/11/22 Python
python实现栅栏加解密 支持密钥加密
2019/03/20 Python
jupyter note 实现将数据保存为word
2020/04/14 Python
法学研究生自我鉴定范文
2013/12/04 职场文书
六查六看剖析材料
2014/02/15 职场文书
产品委托授权书范本
2014/09/16 职场文书
公安机关纪律作风整顿个人剖析材料材料
2014/10/10 职场文书
政风行风评议个人心得体会
2014/10/29 职场文书
2014年工商所工作总结
2014/12/09 职场文书
新教师个人工作总结
2015/02/06 职场文书
一次MySQL启动导致的事故实战记录
2021/09/15 MySQL
python垃圾回收机制原理分析
2022/04/13 Python