10. 类相关操作的扩展
- 包装器初始
如果我们想要增加一个包装器,类似注解,并且基于这个注解来增加额外的处理操作
那么可以如下的书写
import time
from functools import wraps def timethis(func): ”’ Decorator that reports the execution time. ”’ @wraps(func) def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() print(func.__name__, end-start) return result return wrapper |
对于这个函数,我们可以如下的进行使用
>>> @timethis
… def countdown(n): … ”’ … Counts down … ”’ … while n > 0: … n -= 1 … >>> countdown(100000) countdown 0.008917808532714844 >>> countdown(10000000) countdown 0.87188299392912 >>> |
这样我们直接在函数上面增加了这个注解,就可以进行使用,本质上就如同里面的wrapper函数一样,进行了包裹,返回了一个新的函数
内置的装饰器,比如@staticmethod @classmethod @property远离一样
在上面我们直接将传入的*arg和**kwargs传给了函数,这样保证了原始函数的调用
还有就是必须要在上面增加@functools.wraps(f)
从而保留原始函数的元数据
如果不进行增加的话,会丢失所有有用的元数据信息
增加了这个wraps注解之后,我们可以利用__wrapped__直接访问被包装的函数,但是这样的行为并不稳定
就比如
from functools import wraps
def decorator1(func): @wraps(func) def wrapper(*args, **kwargs): print(‘Decorator 1’) return func(*args, **kwargs) return wrapper def decorator2(func): @wraps(func) def wrapper(*args, **kwargs): print(‘Decorator 2’) return func(*args, **kwargs) return wrapper @decorator1 @decorator2 def add(x, y): return x + y |
如果我们直接利用add.__wrapped__(2,3)执行,可能调用到最原始的函数,也可能调用到被decorator2包装的函数,这是不同的python版本导致的,所以并不特别适用
- 包装器的进阶,增加参数
如果我们希望包装器可以增加一些函数的话,我们可以给装饰器增加一些函数
from functools import wraps
import logging def logged(level, name=None, message=None): “”” Add logging to a function. level is the logging level, name is the logger name, and message is the log message. If name and message aren’t specified, they default to the function’s module and name. “”” def decorate(func): logname = name if name else func.__module__ log = logging.getLogger(logname) logmsg = message if message else func.__name__ @wraps(func) def wrapper(*args, **kwargs): log.log(level, logmsg) return func(*args, **kwargs) return wrapper return decorate # Example use @logged(logging.DEBUG) def add(x, y): return x + y @logged(logging.CRITICAL, ‘example’) def spam(): print(‘Spam!’) |
我们我们首先定义了一个函数,接受参数,其次内部定义decorate这个函数接受一个函数作为参数,最后内部利用wrapper进行包裹逻辑的执行
而如果想要支持用户可以在运行的时候修改logging的参数的话,那么我们可以这么做
from functools import wraps, partial
import logging # Utility decorator to attach a function as an attribute of obj def attach_wrapper(obj, func=None): if func is None: return partial(attach_wrapper, obj) setattr(obj, func.__name__, func) return func def logged(level, name=None, message=None): ”’ Add logging to a function. level is the logging level, name is the logger name, and message is the log message. If name and message aren’t specified, they default to the function’s module and name. ”’ def decorate(func): logname = name if name else func.__module__ log = logging.getLogger(logname) logmsg = message if message else func.__name__ @wraps(func) def wrapper(*args, **kwargs): log.log(level, logmsg) return func(*args, **kwargs) # Attach setter functions @attach_wrapper(wrapper) def set_level(newlevel): nonlocal level level = newlevel @attach_wrapper(wrapper) def set_message(newmsg): nonlocal logmsg logmsg = newmsg return wrapper return decorate # Example use @logged(logging.DEBUG) def add(x, y): return x + y @logged(logging.CRITICAL, ‘example’) def spam(): print(‘Spam!’) |
这我们我们封装了set_message() set_level()
并使用nolocal修改函数内部变量
- 装饰器的函数进阶
这里我们看看带可选参数的装饰器
如果想要一个装饰器,支持可选参数的话,那么其实也很简单,跟普通函数中的可选参数一样
def logged(func=None, *, level=logging.DEBUG, name=None, message=None):
if func is None: return partial(logged, level=level, name=name, message=message) logname = name if name else func.__module__ log = logging.getLogger(logname) logmsg = message if message else func.__name__ @wraps(func) def wrapper(*args, **kwargs): log.log(level, logmsg) return func(*args, **kwargs) return wrapper # Example use @logged def add(x, y): return x + y @logged(level=logging.CRITICAL, name=’example’) def spam(): print(‘Spam!’) |
如果一个参数是可选的,那么最好就给他初始化,不然可能出现使用问题
- 类装饰器的高级示例
我们来看利用装饰器进行类型检查,一般来说当希望在函数上进行类型检查的时候,一般是直接在参数上增加注解
def spam(x:int, y, z:int = 42):
print(x,y,z) |
但是这里我们希望实现一个注解可以进行类型检查
from inspect import signature
from functools import wraps def typeassert(*ty_args, **ty_kwargs): def decorate(func): # If in optimized mode, disable type checking if not __debug__: return func # Map function argument names to supplied types sig = signature(func) bound_types = sig.bind_partial(*ty_args, **ty_kwargs).arguments @wraps(func) def wrapper(*args, **kwargs): bound_values = sig.bind(*args, **kwargs) # Enforce type assertions across supplied arguments for name, value in bound_values.arguments.items(): if name in bound_types: if not isinstance(value, bound_types[name]): raise TypeError( ‘Argument {} must be {}’.format(name, bound_types[name]) ) return func(*args, **kwargs) return wrapper return decorate |
有了这个之后,我们就可以进行参数类型检查了
>>> @typeassert(int, z=int)
… def spam(x, y, z=42):
… print(x, y, z)
需要注意,装饰器其实就是函数定义的时候调用一次,
我们首先使用了inspect.signature() 函数,方便提取一个可以调用对象的参数签名信息
对于上面的spam,我们调用签名函数,可以得到
>>> sig.parameters
mappingproxy(OrderedDict([(‘x’, <Parameter at 0x10077a050 ‘x’>),
(‘y’, <Parameter at 0x10077a158 ‘y’>), (‘z’, <Parameter at 0x10077a1b0 ‘z’>)]))
我们使用了bind_partial()方法来执行从指定类型和名称的部分绑定
>>> bound_types = sig.bind_partial(int,z=int)
然后我们又使用了sig.bind方法,进行了实际的调用时传参和指定类型的绑定
>>> bound_values = sig.bind(1, 2, 3)
>>> bound_values.arguments OrderedDict([(‘x’, 1), (‘y’, 2), (‘z’, 3)]) |
从而进行了校验
>>> for name, value in bound_values.arguments.items():
… if name in bound_types.arguments: … if not isinstance(value, bound_types.arguments[name]): … raise TypeError() |
- 将装饰器定义为类得一部分
如果将装饰器定义为类的一部分,首先要明确这个装饰器是实例方法还是类方法
class A:
# Decorator as an instance method def decorator1(self, func): @wraps(func) def wrapper(*args, **kwargs): print(‘Decorator 1’) return func(*args, **kwargs) return wrapper # Decorator as a class method @classmethod def decorator2(cls, func): @wraps(func) def wrapper(*args, **kwargs): print(‘Decorator 2’) return func(*args, **kwargs) return wrapper |
可以看得出两者的区别是在外层包裹的时候分别传入了self和cls
方便去访问类级别或者self级别的属性
对于定义在类中装饰器的使用,可以参考为
@a.decorator1
def spam(): pass |
- 装饰器就是类
如果把装饰器定义为一个类来使用的话,需要在init中保存函数,__call__() 和 __get__() 方法中也需要定义
import types
from functools import wraps class Profiled: def __init__(self, func): wraps(func)(self) self.ncalls = 0 def __call__(self, *args, **kwargs): self.ncalls += 1 return self.__wrapped__(*args, **kwargs) def __get__(self, instance, cls): if instance is None: return self else: return types.MethodType(self, instance) |
这样我们就可以进行简单的使用这个装饰器了
其中上面的init和call相对好理解,主要是 __get__() 方法,如果不书写这个get的话,可能调用的时候会出现很奇怪的问题
因为根据装饰器的协议,我们本质上相当于实现了__get__ 属性,如果希望可以调用到原有instance,那么就需要利用type.Mehtod 进行绑定
- 类方法和静态方法提供装饰器
这一部分好说,直接加就行,不过需要注意,一定要放在@classmethod和@staticmethod下面
def timethis(func):
@wraps(func) def wrapper(*args, **kwargs): start = time.time() r = func(*args, **kwargs) end = time.time() print(end-start) return r return wrapper @classmethod @timethis def class_method(cls, n): print(cls, n) while n > 0: n -= 1 |
这是由于classmethod和staticmethod的特殊包裹方式导致的,如果不这么放,会导致调用出错,就比如和abstractmethod的搭配
class A(metaclass=ABCMeta):
@classmethod @abstractmethod def method(cls): pass |
- 利用装饰器给被包装函数增加参数
如果是希望增加额外的参数,那么最好的方式是使用关键字参数给被包装函数增加额外参数
def optional_debug(func):
@wraps(func) def wrapper(*args, debug=False, **kwargs): if debug: print(‘Calling’, func.__name__) return func(*args, **kwargs) return wrapper |
这样需要给新增的额外参数增加默认值
这样我们使用注解的时候就可以
>>> @optional_debug
… def spam(a,b,c): … print(a,b,c) |
需要注意的是新增的额外关键字参数需要添加在 *args 和 **kwargs参数之中,而且需要注意,需要判断原本函数之中,是否已经存在同名的参数
这一个可以利用inspect函数来做到
inspect.getargspec(func).args
- 类装饰器
def log_getattribute(cls):
# Get the original implementation orig_getattribute = cls.__getattribute__ # Make a new definition def new_getattribute(self, name): print(‘getting:’, name) return orig_getattribute(self, name) # Attach to the class and return cls.__getattribute__ = new_getattribute return cls # Example use @log_getattribute class A: def __init__(self,x): self.x = x def spam(self): pass |
上面我们就成功修改了__getattribute__方法
- 元类控制实例创建
如果希望改变实例创建的方式来完成单例,缓存或者其他类型的特性,那么可以考虑定义元类,在元类中实现__call__()方法
比如我们希望定义一个无法被实例化的类
class NoInstances(type):
def __call__(self, *args, **kwargs): raise TypeError(“Can’t instantiate directly”) # Example class Spam(metaclass=NoInstances): @staticmethod def grok(x): print(‘Spam.grok’) |
这样这个spam一旦被实例化,就会报错
import weakref
class Cached(type): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__cache = weakref.WeakValueDictionary() def __call__(self, *args): if args in self.__cache: return self.__cache[args] else: obj = super().__call__(*args) self.__cache[args] = obj return obj # Example class Spam(metaclass=Cached): def __init__(self, name): print(‘Creating Spam({!r})’.format(name)) self.name = name |
这样在使用的时候就会有一个缓存字典
>>> a = Spam(‘Guido’)
Creating Spam(‘Guido’)
>>> b = Spam(‘Diana’)
Creating Spam(‘Diana’)
>>> c = Spam(‘Guido’) # Cached
>>> a is b
False
>>> a is c # Cached value returned
True
- 元类的参数
一般来说,我们一般就使用metaclass来指定特定的元类
如果我们想要给python定义带有参数的元类,那么可以
class Spam(metaclass=MyMeta, debug=True, synchronize=True):
pass
不过为了让元类支持这些关键词参数,需要在__perpare__() __new__()方法以及 __init__() 方法中都使用关键词参数
class MyMeta(type):
# Optional @classmethod def __prepare__(cls, name, bases, *, debug=False, synchronize=False): # Custom processing pass return super().__prepare__(name, bases) # Required def __new__(cls, name, bases, ns, *, debug=False, synchronize=False): # Custom processing pass return super().__new__(cls, name, bases, ns) # Required def __init__(self, name, bases, ns, *, debug=False, synchronize=False): # Custom processing pass super().__init__(name, bases, ns) |
上面的prepare方法会在类定义开执行前进行调用,创建类命名空间,一般来说是返回一个字典或者其他的映射对象,new 方法则是实例化最终的类对象, init方法则是执行其他的一些初始化工作
- Inspect模块的Signature和Parameter
如果设计到获取函数调用时的参数,都可以考虑使用Inspect模块下的Signature和Parameter
>>> from inspect import Signature, Parameter
>>> # Make a signature for a func(x, y=42, *, z=None) >>> parms = [ Parameter(‘x’, Parameter.POSITIONAL_OR_KEYWORD), … Parameter(‘y’, Parameter.POSITIONAL_OR_KEYWORD, default=42), … Parameter(‘z’, Parameter.KEYWORD_ONLY, default=None) ] >>> sig = Signature(parms) >>> print(sig) (x, y=42, *, z=None) >>> |
当我们有了这样一个签名对象,就可以利用bind方法将其绑定上去
>>> def func(*args, **kwargs):
… bound_values = sig.bind(*args, **kwargs)
… for name, value in bound_values.arguments.items():
… print(name,value)
这种时候,如果我们传入超过Paramater数量的参数,就会报错
>>> func(1, 2, 3, 4)
Traceback (most recent call last):
通过这种方式,我们可以实现自己的参数校验
- 给标准类设定编程规约
我们可以利用元类来做一点,一般来说,元类都是继承自type并定义了其 new 或者 init方法
这里我们先给出一个元类会拒绝任何的带有大小写名字作为方法名的类
class NoMixedCaseMeta(type):
def __new__(cls, clsname, bases, clsdict): for name in clsdict: if name.lower() != name: raise TypeError(‘Bad attribute name: ‘ + name) return super().__new__(cls, clsname, bases, clsdict) class A(Root): def foo_bar(self): # Ok pass |
具体定义new方法还是init方法取决于如何想要使用结果类,new在类创建之前使用
Init在类创建之后使用
- new_class函数动态创建类
利用types.new_class() 来初始化新的类对象,只需要提供类的名称,父类元组,关键字参数,以及一个用成员变量填充类字典的回调函数
def __init__(self, name, shares, price):
self.name = name self.shares = shares self.price = price def cost(self): return self.shares * self.price cls_dict = { ‘__init__’ : __init__, ‘cost’ : cost, } # Make a class import types Stock = types.new_class(‘Stock’, (), {}, lambda ns: ns.update(cls_dict)) Stock.__module__ = __name__ |
第三个参数还可以包含其他的关键字参数,比如元类
- 利用闭包来避免重复的属性方法
如果需要重复定义一些相同逻辑的属性方法,如何简化代码,比如有一个简单的类,中间有多个属性方法,逻辑一致
class Person:
def __init__(self, name ,age): self.name = name self.age = age @property def name(self): return self._name @name.setter def name(self, value): if not isinstance(value, str): raise TypeError(‘name must be a string’) self._name = value @property def age(self): return self._age @age.setter def age(self, value): if not isinstance(value, int): raise TypeError(‘age must be an int’) self._age = value |
如果是每一个属性有一个这样的名字,那么书写起来也太麻烦了
那么不如定义一个闭包函数来统一封装这段逻辑
def typed_property(name, expected_type):
storage_name = ‘_’ + name @property def prop(self): return getattr(self, storage_name) @prop.setter def prop(self, value): if not isinstance(value, expected_type): raise TypeError(‘{} must be a {}’.format(name, expected_type)) setattr(self, storage_name, value) return prop # Example use class Person: name = typed_property(‘name’, str) age = typed_property(‘age’, int) def __init__(self, name, age): self.name = name self.age = age |
这样我们就可以统一进行类型校验了
这样利用闭包记录了名称,并将其数据实际存储在类中
- 利用contextmanager定义上下文管理器
利用contexlib模块中的@contextmanager装饰器可以实现一个简单的上下文管理器
import time
from contextlib import contextmanager @contextmanager def timethis(label): start = time.time() try: yield finally: end = time.time() print(‘{}: {}’.format(label, end – start)) # Example use with timethis(‘counting’): n = 10000000 while n > 0: n -= 1 |
上面我们利用contextmanager来包裹了一个函数,然后在函数内部,我们利用yield原语
可以说的是yield之前的代码会在with的时候指定,并且可以返回yield返回的数据等同于__enter__()
yield之后的代码作为 __exit__() 执行,如果出现了异常,会在yield哪里抛出
那么我们就可以按照这个特性,实现一个替换代码,当且只有不抛出异常的时候才会进行替换
@contextmanager
def list_transaction(orig_list): working = list(orig_list) yield working orig_list[:] = working |
>>> items = [1, 2, 3]
>>> with list_transaction(items) as working:
… working.append(4)
… working.append(5)
…
>>> items
[1, 2, 3, 4, 5]
>>> with list_transaction(items) as working:
… working.append(6)
… working.append(7)
… raise RuntimeError(‘oops’)
…
Traceback (most recent call last):
File “<stdin>”, line 4, in <module>
RuntimeError: oops
>>> items
[1, 2, 3, 4, 5]
>>>
- Python的局部变量域
Exec执行的代码会存储在一个局部变量域中
这里我们可以直接看下
>>> a = 13
>>> exec(‘b = a + 1’)
>>> print(b)
14
如果在一个函数中执行上面的代码,那会抛出异常
>>> def test():
… a = 13
… exec(‘b = a + 1’)
… print(b)
如果希望修改这个代码的话,需要手动赋予b这个值,从局部变量字典中取出对应的值,进行赋值
loc = locals()
exec(‘b = a + 1’)
b =loc[‘b’]
我们利用locals函数获取到了局部变量字典,从而赋予了b的值,这里提一嘴,exec执行的时候,使用的变量都是从实际局部变量拷贝的一个值,然后进行了修改,这种修改不会覆盖原本的值
- 分析python源码
如果希望分析python源码,那么可以使用ast的parse函数,会将源码编译为一个可悲分析的抽象语法树 AST
>>> ex = ast.parse(‘2 + 3*4 + x’, mode=’eval’)
>>> ast.dump(ex)
“Expression(body=BinOp(left=BinOp(left=Num(n=2), op=Add(),
right=BinOp(left=Num(n=3), op=Mult(), right=Num(n=4))), op=Add(),
right=Name(id=’x’, ctx=Load())))”
如果希望将自己的代码反编译为低级的字节码的话
可以使用dis模块来进行输出任何的python反编译结果
>>> def countdown(n):
… while n > 0: … print(‘T-minus’, n) … n -= 1 … print(‘Blastoff!’) … >>> import dis >>> dis.dis(countdown) 2 0 SETUP_LOOP 30 (to 32) >> 2 LOAD_FAST 0 (n) 4 LOAD_CONST 1 (0) 6 COMPARE_OP 4 (>) 8 POP_JUMP_IF_FALSE 30 |