Python黑魔法系列-02.可调用对象

论坛 期权论坛 期权     
小白的技术客栈   2019-7-13 17:59   3126   0
我们刚开始学习面向对象编程时,都是从类的概念开始学起的。类是对具体事物的抽象,类的名称是一个名词,类中包含属性和方法。
从概念层面来看,类是对具体事物的抽象。从语法层面来看,类就是将数据和行为打包。如果我们活学活用,其实也可以把类当做一个函数来使用,或者说,把一个函数封装成一个类。把函数封装成类具有许多好处。首先,我们可以在类的初始化函数中保存部分参数,从而减少函数调用时传递的参数个数;其次,我们可以将一个大的函数拆分成若干个小的函数,从而提高代码的可读性;最后,我们将多个紧密相关的函数组织在同一个命名空间下,有利于代码维护。
我们已经理解了将一个函数组织成类的好处,接下来的问题是,如何把一个类的对象当成普通函数来调用呢?在Python中,可以通过运算符重载来实现。Python的运算符重载通过特殊的命名来完成。其中,
  1. __call__
复制代码
方法将对象变成一个可调用的对象。只要在类中实现了
  1. __call__
复制代码
方法,我们就可以像普通函数那样调用一个类对象了。
有了上述基本认知之后,我们就看一个例子吧。代码如下:
import timefrom functools import wrapsclass Timeit:    def __init__(self, fn):        self.fn = fn    def __call__(self, *args, **kwargs):        start = time.time()        ret = self.fn(*args, **kwargs)        print(time.time() - start)        return ret@Timeitdef sleep():    # sleep函数将赋值给Timeit类的fn属性    time.sleep(3)    return 1# 拆解开来,等价于# sleep = Timeit(sleep)# 调用sleep()方法>>> sleep()3.0007045269012451上述代码其实使用类来实现了对函数的装饰。
如何理解呢?
from functools import wrapsdef fn():    passdef wrap():    passwraps(fn)(wrap)f = wraps(fn)(wrap)dir(f)['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__wrapped__']f.__name__fnwrap.__name__fndef f():    '''this is f'''    passdef g():    '''this is g'''    passf.__name__'f'f.__doc__'this is f'g.__name__'g'g.__doc__'this is g'wraps(f)(g) # 把f的一些属性复制给gg.__name__'f'g.__doc__'this is f'# 柯里化函数# 把f的一些属性,复制给g# Timeit还可以这样写import timefrom functools import wrapsclass Timeit:    def __init__(self, fn):        wraps(fn)(self)    def __call__(self, *args, **kwargs):        start = time.time()        ret = self.__wrapped__(*args, **kwargs)        print(time.time() - start)        return ret经过修改之后,
import timefrom functools import wrapsclass Timeit:    def __init__(self, fn):        self.wrapped = wraps(fn)(self)    def __call__(self, *args, **kwargs):        start = time.time()        ret = self.wrapped.__wrapped__(*args, **kwargs)  # wrapped.__wrapped__ = fn        print(time.time() - start)        return ret@Timeitdef add(x, y):    """ret x + y    """    return x + yadd.__name__'add'add.__doc__'ret x + y'add(2, 3)0.05单例类的装饰器,
class Singleton:    def __init__(self, cls):        wraps(cls)(self)        print(cls)        self.instance = None        print(self.instance)            def __call__(self, *args, **kwargs):        print(self.instance)        if self.instance is None:            self.instance = self.__wrapped__(*args, **kwargs)        print(self.instance)        return self.instance    @Singletonclass A:    """this is A"""    passNonea1 = A()Nonea2 = A()a1 is a2Truea = A()a.__doc__'this is A'当调用实例时,使用
  1. __call__
复制代码
方法。这并不是循环定义:如果定义了,Python就会为实例应用函数调用表达式运行
  1. __call__
复制代码
方法。这样可以让类实例的外观和用法类似于函数。
class Callee:    def __call__(self, *args, **kwargs):        print('Called: ', args, kwargs)        c = Callee()c(1, 2, 3)  # c现在是可调用的Called: (1, 2, 3) {}c(1, 2, 3, x=4, y=5)Called: (1, 2, 3) {'y': 5, 'x': 4}
  1. __call__
复制代码
方法支持适合函数的所有的参数传递。传递给实例的任何内容都会传递给该方法,包括通常隐式的实例参数。
再看一个例子:
class Prod:    def __init__(self, value):  # 只接收一个参数        self.value = value            def __call__(self, other):        return self.value * other    In [47]: x = Prod(3)  # 把3保留在状态中In [48]: x(4)  # 4(传递) * 3(状态)Out[48]: 12In [49]: x(5)Out[49]: 15接下来看看可调用对象。可以通过小括号的方式调用的对象。
class F:    passfn = F()fn_dir = set(dir(fn))class A:    passa = A()a_dir = set(dir(a))fn_dir.difference(a_dir)# set() # 得到一个空集合In [9]: class Add:   ...:     def __call__(self, a, b):   ...:         print('{} + {}'.format(a, b))   ...:         return a + b   ...:     In [10]: add = Add()In [11]: add(1, 2)1 + 2Out[11]: 3>>> class Adder:...     def __call__(self, x, y):...         return x + y...     ... >>> Adder()(3, 5)8In [15]: callable(fn)Out[15]: FalseIn [16]: callable(lambda x: x)Out[16]: True>>> def fn():...     pass... >>> fn.__call__>>> fn.__class__>>> class Fn:...     def __call__(self):...         print('{} called'.format(self))...         ...     ... >>> f = Fn()>>> f() called>>> callable(f)True当一个对象,只要实现
  1. __call__
复制代码
方法,就可以通过小括号(像函数调用一样)来调用。当调用一个可调用对象时,实际上调用的
  1. __call__
复制代码
方法。所有函数都是可调用对象。所有的函数都是
  1. function
复制代码
的实例。
看一个不可调用的对象,
In [17]: class NotCallable:    ...:     pass    ...: In [18]: nc = NotCallable()In [19]: nc()---------------------------------------------------------------------------TypeError                                 Traceback (most recent call last) in ()----> 1 nc()TypeError: 'NotCallable' object is not callableIn [20]: callable(nc)Out[20]: FalseIn [21]: dir(NotCallable.__init__)Out[21]: ['__call__', '__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__objclass__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__text_signature__']内置函数callable用于判断一个对象是否是可调用对象。
再看一个小例子:
In [22]: class Adder:    ...:     def __call__(self, x, y):    ...:         return x + y    ...:     In [23]: print(Adder()(3, 5)) # 这里的两个括号,第一个小括号相当于实例化类Adder,第二个相当于方法调用,调用了`__call__` 方法8再看缓存的另一种写法:
import datetimeimport inspectfrom functools import wrapsclass Cache:    def __init__(self, size=128, expire=0):        self.size = size        self.expire = expire        self.data = {}    @staticmethod    def make_key(fn, args, kwargs):        ret = []        names = set()        params = inspect.signature(fn).parameters        keys = list(params.keys())        for i, arg in enumerate(args):            ret.append((keys, arg))            names.add(keys)        ret.extend(kwargs.keys())        for k, v in params.items():            if k not in names:                ret.append((k, v.default))        ret.sort(key=lambda x: x[0])        return '&'.join(['{}={}'.format(name, arg) for name, arg in ret])    def __call__(self, fn):        @wraps(fn)        def wrap(*args, **kwargs):            key = self.make_key(fn, args, kwargs)            now = datetime.datetime.now().timestamp()            if key in self.data.keys():                value, timestamp, _ = self.data[key]                if expire == 0 or now - timestamp = self.size:                # 过期清理                if self.expire != 0:                    expires = set()                    for k, (_, timestamp, _) in self.data.items():                        if now - timestamp >= self.expire:                            expires.add(k)                    for k in expires:                        self.data.pop(k)            if len(self.data) >= self.size:                # 换出                k = sorted(self.data.items(), key=lambda x: x[1][2])[0][0]                self.data.pop(k)            self.data[key] = (value, now, now)            return value        return wrap@Cache()def add(x, y):    return x + yadd(1, 2)再看单例类的另一种实现方法,
class Singleton:    def __init__(self, cls):        self.cls = cls        self.instance = None    def __call__(self, *args, **kwargs):        if self.instance is None:            self.instance = self.cls(*args, **kwargs)        return self.instance@Singletonclass A:    passa = A()a1 = A()a is a1True    用
  1. __call__
复制代码
来实现可调用对象,和闭包是殊途同归的,通常是为了封装一些内部状态。非常复杂的装饰器,使用类来写,可以方便拆分逻辑。
好了,今天就到这里,下次再见。


分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:5
帖子:1
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP