10. 类相关操作的扩展

  1. 包装器初始

如果我们想要增加一个包装器,类似注解,并且基于这个注解来增加额外的处理操作

那么可以如下的书写

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版本导致的,所以并不特别适用

  1. 包装器的进阶,增加参数

如果我们希望包装器可以增加一些函数的话,我们可以给装饰器增加一些函数

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修改函数内部变量

  1. 装饰器的函数进阶

这里我们看看带可选参数的装饰器

如果想要一个装饰器,支持可选参数的话,那么其实也很简单,跟普通函数中的可选参数一样

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!’)

如果一个参数是可选的,那么最好就给他初始化,不然可能出现使用问题

  1. 类装饰器的高级示例

我们来看利用装饰器进行类型检查,一般来说当希望在函数上进行类型检查的时候,一般是直接在参数上增加注解

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

  1. 将装饰器定义为类得一部分

如果将装饰器定义为类的一部分,首先要明确这个装饰器是实例方法还是类方法

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

  1. 装饰器就是类

如果把装饰器定义为一个类来使用的话,需要在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 进行绑定

  1. 类方法和静态方法提供装饰器

这一部分好说,直接加就行,不过需要注意,一定要放在@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

  1. 利用装饰器给被包装函数增加参数

如果是希望增加额外的参数,那么最好的方式是使用关键字参数给被包装函数增加额外参数

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

  1. 类装饰器
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__方法

  1. 元类控制实例创建

如果希望改变实例创建的方式来完成单例,缓存或者其他类型的特性,那么可以考虑定义元类,在元类中实现__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

  1. 元类的参数

一般来说,我们一般就使用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方法则是执行其他的一些初始化工作

  1. 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):

通过这种方式,我们可以实现自己的参数校验

  1. 给标准类设定编程规约

我们可以利用元类来做一点,一般来说,元类都是继承自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在类创建之后使用

  1. 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__

第三个参数还可以包含其他的关键字参数,比如元类

  1. 利用闭包来避免重复的属性方法

如果需要重复定义一些相同逻辑的属性方法,如何简化代码,比如有一个简单的类,中间有多个属性方法,逻辑一致

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

这样我们就可以统一进行类型校验了

这样利用闭包记录了名称,并将其数据实际存储在类中

  1. 利用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]

>>>

  1. 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执行的时候,使用的变量都是从实际局部变量拷贝的一个值,然后进行了修改,这种修改不会覆盖原本的值

  1. 分析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

发表评论

邮箱地址不会被公开。 必填项已用*标注