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 for循环的巧妙运用(迭代、列表生成式)
Sep 26 Python
python购物车程序简单代码
Apr 18 Python
Python爬虫之网页图片抓取的方法
Jul 16 Python
Python使用统计函数绘制简单图形实例代码
May 15 Python
图文详解python安装Scrapy框架步骤
May 20 Python
Pyqt5 基本界面组件之inputDialog的使用
Jun 25 Python
Flask框架实现的前端RSA加密与后端Python解密功能详解
Aug 13 Python
Python3 解决读取中文文件txt编码的问题
Dec 20 Python
Keras - GPU ID 和显存占用设定步骤
Jun 22 Python
在django中实现choices字段获取对应字段值
Jul 12 Python
Python如何实现机器人聊天
Sep 10 Python
Python图片检索之以图搜图
May 31 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
paypal即时到账php实现代码
2010/11/28 PHP
PHP 读取大文件的X行到Y行内容的实现代码
2013/06/24 PHP
thinkphp特殊标签用法概述
2014/11/24 PHP
PHP 数据结构队列(SplQueue)和优先队列(SplPriorityQueue)简单使用实例
2015/05/12 PHP
PHP中set_include_path()函数相关用法分析
2016/07/18 PHP
Yii2数据库操作常用方法小结
2017/05/04 PHP
Laravel如何同时连接多个数据库详解
2019/08/13 PHP
javascript 按回车键相应按钮提交事件
2009/11/02 Javascript
js 取时间差去掉周六周日实现代码
2012/12/25 Javascript
jquery实现带复选框的表格行选中删除时高亮显示
2013/08/01 Javascript
如何在MVC应用程序中使用Jquery
2014/11/17 Javascript
使用CoffeeScrip优美方式编写javascript代码
2015/10/28 Javascript
简单谈谈Vue 模板各类数据绑定
2016/09/25 Javascript
基于JS实现横线提示输入验证码随验证码输入消失(js验证码的实现)
2016/10/27 Javascript
JS常用正则表达式总结【经典】
2017/05/12 Javascript
解决vue-cli创建项目的loader问题
2018/03/13 Javascript
使用weixin-java-miniapp配置进行单个小程序的配置详解
2019/03/29 Javascript
浅谈Vue页面级缓存解决方案feb-alive(上)
2019/04/14 Javascript
javascript写一个ajax自动拦截并下载数据代码实例
2019/09/07 Javascript
微信小程序跳转到其他网页(外部链接)的实现方法
2019/09/20 Javascript
vue中使用GraphQL的实例代码
2019/11/04 Javascript
python创建列表并给列表赋初始值的方法
2015/07/28 Python
详解Python 2.6 升级至 Python 2.7 的实践心得
2017/04/27 Python
python 修改本地网络配置的方法
2019/08/14 Python
python 带时区的日期格式化操作
2020/10/23 Python
荷兰时尚精品店:Labels Fashion
2020/03/22 全球购物
会计电算化应届生求职信
2013/11/03 职场文书
社区清明节活动总结
2014/07/04 职场文书
爱国主义教育演讲稿
2014/08/26 职场文书
银行培训心得体会范文
2016/01/09 职场文书
九年级数学教学反思
2016/02/17 职场文书
入伍志愿书怎么写?
2019/07/19 职场文书
python通配符之glob模块的使用详解
2021/04/24 Python
Python  lambda匿名函数和三元运算符
2022/04/19 Python
Nginx安装配置详解
2022/06/25 Servers
JS前端轻量fabric.js系列之画布初始化
2022/08/05 Javascript