Python 闭包和__call__详解及开发实践

背景

最近在使用wtform做后台的表单验证时候,跟很多框架使用的方法一样,它是结合ORM的对象提供校验, wtform 默认提供了不少的校验器,但是有的字段需要自己编写一些业务相关的校验器。
自定义的每个校验器的特点都是接受两个参数,form, field。
所以我们自己自定义校验器,校验器都必须是可调用对象即可,即函数,对象方法,都可以的。
比如function url_validate(form, field)
我们可以使用下面几种方式来实现


使用函数定制

def my_length_check(form, field):
    if len(field.data) > 50:
        raise ValidationError('Field must be less than 50 characters')

class MyForm(Form):
    name = StringField('Name', [InputRequired(), my_length_check])

上面提供一个my_length_check()函数,用于验证name长达是否长于50个字符。
这个函数按照规定接受两个参数,form, field,然后就可以根据两个参数进行判断。
这样做是可以的,但是问题是:

  • 如果我想自定义错误信息怎么办?而且里面的限制是50, 已经被写死了,我不能修改,如果我想改最小值,和最大值呢?
  • 错误信息我也不能自定义?
    所以, 这里只能固定接受两个参数,不能再传入自定参数,没得更多的自定义了,显然不行。

使用闭包

我们先简单了解一下闭包的概念:闭包能使局部变量在函数外被访问
先来看个例子:

def A():
    num = 10 # 局部变量
print(num)  # NameError: name 'num' is not defined

定义在函数内的变量是局部变量,局部变量的作用范围只能是函数内部范围内,它不能在函数外引用。
因为 num 是定义在函数内的局部变量,局部变量在函数外引用是失败的。

我们看看闭包的具体实现:

def A():
    # A 是外围函数
    msg = "I like python in A"
    def B():
        # B 是嵌套函数
        print(msg)
    return B

b = A()
# 输出 I like python in A
b()

正常理解下,函数中的局部变量仅在函数的执行期间可用,所以 A() 执行过后,我们会认为 A函数里面的 msg 变量将不再可被外界读取。然而,在这里我们发现 A() 执行完之后,得到的对象 b,在b()调用的时候 msg 变量的值正常输出了。
这就是闭包的作用,闭包使得局部变量在函数外能被访问。

回到我们刚才的例子,使用闭包给wtform增加校验器:

def length(min=-1, max=-1, message=None):
    if not message:
        message = 'Must be between %d and %d characters long.' % (min, max)

    def _length(form, field):
        l = field.data and len(field.data) or 0
        if l < min or max != -1 and l > max:
            raise ValidationError(message)

    return _length

class MyForm(Form):
    name = StringField('Name', [InputRequired(), length(max=50, message="长度不符合!")])

length函数是个闭包, 为什么是个闭包?正常情况下执行完length() min, max, message参数就会被回收,不再存在,但是因为闭包的特性,_length 能读取到里面的min, max, message。

这个length(min, max, message) 函数,调用的时候,传入了更多的参数来灵活决定校验器,因为他里面就是返回一个_length的校验器,这个校验器还是遵循规则,只接受form, field 参数, 但是在length() 这个外层却能接受更多参数,而这些参数也能被内层的_length()所使用。

所以最后实际上校验器的调用是这样的:

 length(max=50, message="长度不符合!")(form, field)

length() 就是一个闭包,闭包本质上是一个函数,它有两部分组成,_length 函数和变量 min, max, message。闭包使得这些变量的值始终保存在内存中。


使用 __call__() 实现

__call__() 是一个特殊的魔术方法, 它可以让类的实例的行为表现的像函数一样。所以如果在类中实现了 __call__() 方法,那么实例对象也将成为一个可调用对象。

class Length(object):
    def __init__(self, min=-1, max=-1, message=None):
        self.min = min
        self.max = max
        if not message:
            message = u'Field must be between %i and %i characters long.' % (min, max)
        self.message = message

    def __call__(self, form, field):
        l = field.data and len(field.data) or 0
        if l < self.min or self.max != -1 and l > self.max:
            raise ValidationError(self.message)

class MyForm(Form):
    name = StringField('Name', [InputRequired(), Length(max=50)])

这次我们使用类来实现,首先实例化这个校验器

len_validator  = Length(max=50, message="长度不合符哟!")
len_validator(form, field)

实例化对象后返回的len_validator对象,由于我们实现了__call__()方法,说明len_validator这个对象能像函数一样被调用,而__call__()函数内部能读取对象内部的变量,因此对象的min, max, message属性它都能读取到。而且它接受的参数是form, field,符合扩展校验器规范。


闭包和__call__()的其他应用:单例模式

使用闭包

def singleton(cls):
    _instance = {}

    def inner():
        if cls not in _instance:
            _instance[cls] = cls()
        return _instance[cls]
    return inner
    
@singleton
class Cls(object):
    def __init__(self):
        pass

cls1 = Cls()
cls2 = Cls()
print(id(cls1) == id(cls2))

Cls前面增加了singleton装饰器,所以定义Cls的时候相当于 Cls = singleton(Cls), 而singleton(Cls) 返回的是inner函数对象,这个inner()就是一个闭包,能读取到 _instance 属性。 所以每次执行Cls()相当于执行inner()函数,这个inner() 每次都能读取singleton() 初始化过的闭包变量_instance, 判断_instance是否被填充,从而作出单例返回。

使用__call__()

class Singleton(object):
    def __init__(self, cls):
        self._cls = cls
        self._instance = {}
    def __call__(self):
        if self._cls not in self._instance:
            self._instance[self._cls] = self._cls()
        return self._instance[self._cls]

@Singleton
class Cls2(object):
    def __init__(self):
        pass

cls1 = Cls2()
cls2 = Cls2()
print(id(cls1) == id(cls2))

Cls类定义前面增加了装饰器,所以定义Cls的时候相当于 Cls = singleton(Cls), 而singleton(Cls) 返回的是singleton的实例对象(假设是singleton_obj),因此Cls = singleton_obj, 当执行Cls()相当于执行singleton_obj(), 这时就会触发到__call__()方法,到里面判断_instance是否被填充,从而作出单例返回


总结

对于使用闭包和__call__特性,总结一下:

  • 闭包避免了使用全局变量,此外,闭包允许将函数与其所操作的某些数据(环境)关连起来, 这一点跟面向对象编程是很像的。
  • __call__() 是能让对象实例变成可执行的魔术方法,能读取实例绑定的其他变量

关于我

如果文章对你有收获,可以收藏转发,这会给我一个大大鼓励哟!
另外可以关注我公众号【码农富哥】 (coder2025),我会持续输出原创的算法,计算机基础文章!

猜你喜欢

转载自blog.csdn.net/a519640026/article/details/104215333