• Python __call__详解


    可以调用的对象

    关于 __call__ 方法,不得不先提到一个概念,就是可调用对象(callable),我们平时自定义的函数、内置函数和类都属于可调用对象,但凡是可以把一对括号()应用到某个对象身上都可称之为可调用对象,判断对象是否为可调用对象可以用函数 callable

    如果在类中实现了 __call__ 方法,那么实例对象也将成为一个可调用对象,

    你也许已经知道,在Python中,方法也是一种高等的对象。这意味着他们也可以被传递到方法中就像其他对象一样。这是一个非常惊人的特性。 在Python中,一个特殊的魔术方法可以让类的实例的行为表现的像函数一样,你可以调用他们,将一个函数当做一个参数传到另外一个函数中等等。这是一个非常强大的特性让Python编程更加舒适甜美。 __call__(self, [args...])

    允许一个类的实例像函数一样被调用。实质上说,这意味着 x() 与 x.__call__() 是相同的。注意 __call__ 参数可变。这意味着你可以定义 __call__ 为其他你想要的函数,无论有多少个参数。

    __call__ 在那些类的实例经常改变状态的时候会非常有效。调用这个实例是一种改变这个对象状态的直接和优雅的做法。用一个实例来表达最好不过了:

    class Entity:
    '''调用实体来改变实体的位置。'''
    
    def __init__(self, size, x, y):
        self.x, self.y = x, y
        self.size = size
    
    def __call__(self, x, y):
        '''改变实体的位置'''
        self.x, self.y = x, y
    
    e = Entity(1, 2, 3) // 创建实例
    e(4, 5) //实例可以象函数那样执行,并传入x y值,修改对象的x y 
    

    实例 --- Flask Wtform

    在wtform的validators就使用了这个特性
    wtform 定义字段的时候可以为每个字段添加校验器, 而每个校验器的特点都是接受两个参数,form, field。
    所以我们可以自己自定义校验器,校验器是可调用对象即可,即函数,对象方法,都可以的。
    比如function url_validate(form, field)

    使用函数定制

    下面提供一个my_length_check()函数,用于验证name长达是否长于50个字符。
    这个函数按照规定接受两个参数,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])
    
    使用闭包和工厂模式

    为了解决上面能传入更多自定义参数, 更灵活定制校验,我们进行改造。
    使用工厂模式,一个能生产校验器的工厂,而这个工厂能接受其他更灵活的参数,看例子:

    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)])
    

    这里涉及两个概念:

    • 工厂模式
      这个例子的length()是一个工厂,他的工作就是执行的时候,生产一个validator校验器,每次调用,传入不同参数,就会生产不同的校验器,所以叫做工厂模式。

    • 闭包
      同时它也是个闭包, 为什么是个闭包?正常情况下执行完length() min, max, message参数就会被回收,不在存在,但是为什么_length()能继续读取min, max, message变量,就是闭包的威力。闭包使得局部变量在函数外被访问成为可能。
      这里的 length() 就是一个闭包,闭包本质上是一个函数,它有两部分组成,_length 函数和变量 min, max, message。闭包使得这些变量的值始终保存在内存中。
      参考:一步一步教你认识Python闭包

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

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

    这里的length(max=5)就是返回了个_length函数,wtform调用校验器的时候实际上就是这样调用的:

    length(max=50)(form, field)
    
    使用类方法__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)])
    

    这次我们使用类来实现,首先实例化这个校验器Length(max=50), 这时返回的是对象实例,然后我们定义了方法__call__方法,说明实例对象是可以调用的, 最后的结果就是Length(max=50)(form, field)。
    这样实现方法是也是很妙的。
    说到这里,类实现方法跟闭包很像,都是把变量封装起来,让真正的校验器能读取到参数。

    闭包避免了使用全局变量,此外,闭包允许将函数与其所操作的某些数据(环境)关连起来。这一点与面向对象编程是非常类似的,在面对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。

    __call__其他作用

    实例对象也可以像函数一样作为可调用对象来用,那么,这个特点在什么场景用得上呢?这个要结合类的特性来说,类可以记录数据(属性),而函数不行(闭包某种意义上也可行),利用这种特性可以实现基于类的装饰器,在类里面记录状态,比如,下面这个例子用于记录函数被调用的次数:

    class Counter:
        def __init__(self, func):
            self.func = func
            self.count = 0
    
        def __call__(self, *args, **kwargs):
            self.count += 1
            return self.func(*args, **kwargs)
    
    @Counter
    def foo():
        pass
    
    for i in range(10):
        foo()
    
    print(foo.count)  # 10
    
    

    首先这里的@Counter是装饰器,执行起来顺序是 foo = Counter(foo), 实例化,把foo函数传到类Counter里面,并存到对象属性里面,然后返回foo = Counter实例。 这时foo已经是Counter实例,而不是本身foo函数。
    当执行foo()的时候,其实已经变成了,执行__call__函数,而这个函数里面是执行了本身的self.func 即foo的实际逻辑, 而且加上了计算调用次数。这样就记录状态了。
    太厉害了,这样的实现方式。

    https://wtforms.readthedocs.io/en/stable/validators.html#custom-validators
    https://stackoverflow.com/questions/9663562/what-is-the-difference-between-init-and-call-in-python
    一步一步教你认识Python闭包
    简述 initnewcall 方法



    作者:大富帅
    链接:https://www.jianshu.com/p/e1d95c4e1697
    来源:简书
    简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
  • 相关阅读:
    微服务架构 技能图谱skill-map
    LiveTelecast直播平台技术图谱skill-map
    OpenResty 技术图谱skill-map
    HearthBuddy 召唤随从的问题
    五子棋AI教程
    HearthAgent A Hearthstone agent
    Monte Carlo Tree Search – beginners guide
    Programming a Hearthstone agent using Monte Carlo Tree Search(chapter one)
    Add hyperlink to textblock wpf
    What do you do as a DevOps?
  • 原文地址:https://www.cnblogs.com/jfdwd/p/11198767.html
Copyright © 2020-2023  润新知