• 元编程


    在函数上添加包装器

    问题:

      你想在函数上添加一个包装器,增加额外的操作处理(比如日志、计时等)

    解决方案:

      如果你想使用额外的代码包装一个函数,可以定义一个装饰器函数,例如:

     1 import  time
     2 from functools import wraps
     3 
     4 
     5 def timethis(func):
     6     @wraps(func)
     7     def wrapper(*args,**kwargs):
     8         start = time.time()
     9         result = func(*args,**kwargs)
    10         end = time.time()
    11         print(func.__name__,end-start)
    12         return result
    13     return wrapper
    14 
    15 @timethis           #等同于timethis(countdown)
    16 def countdown(n):
    17     while n > 0:
    18         n -= 1
    19 
    20 countdown(100000)
    21 countdown(1000000)

    以上代码的执行结果为:

    countdown 0.008307218551635742
    countdown 0.10066509246826172

    创建装饰器时保留函数元信息

    问题:

      你写了一个装饰器作用在某个函数上,但是这个函数的重要的元信息比如名字、文档字符串、注解和参数签名都丢失了

    解决方案:

      任何时候你定义装饰器的时候,都应该使用functools 库中的@wraps 装饰器来注解底层包装函数。例如:

     1 import time
     2 from functools import wraps
     3 
     4 def timethis(func):
     5     @wraps(func)
     6     def wrapper(*args, **kwargs):
     7         start = time.time()
     8         result = func(*args, **kwargs)
     9         end = time.time()
    10         print(func.__name__, end-start)
    11         return result
    12     return wrapper
    13 
    14 @timethis
    15 def countdown(n:int):
    16     """
    17     装饰器内部调用的函数
    18     :param n:
    19     :return:
    20     """
    21     while n>0:
    22         n -= 1
    23 
    24     return 'countdown'
    25 
    26 countdown(100000)
    27 print('name:', countdown.__name__)
    28 print('doc:', countdown.__doc__)
    29 print('annotations:', countdown.__annotations__)
    30 
    31 '''
    32 @wraps 有一个重要特征是它能让你通过属性wrapped 直接访问被包装函数。例如:
    33 '''
    34 print('wrapped:', countdown.__wrapped__(100000))
    35 
    36 '''
    37 wrapped 属性还能让被装饰函数正确暴露底层的参数签名信息。例如:
    38 '''
    39 from inspect import signature
    40 print('signatrue:', signature(countdown))

    以上代码的执行结果为:

    countdown 0.008561134338378906
    name: countdown
    doc: 
        装饰器内部调用的函数
        :param n:
        :return:
        
    annotations: {'n': <class 'int'>}
    wrapped: countdown
    signatrue: (n:int)

    解除一个装饰器

    问题:

      一个装饰器已经作用在一个函数上,你想撤销它,直接访问原始的未包装的那个函数

    解决方案:

      假设装饰器是通过@wraps来实现的,那么你可以通过访问__wrapped__ 属性来访问原始函数:

     1 from functools import wraps
     2 
     3 
     4 def decorator(func):
     5     @wraps(func)
     6     def wrapper(x, y):
     7         result = func(x, y)
     8         return result
     9     return wrapper
    10 
    11 @decorator
    12 def add(x, y):
    13     return x + y
    14 
    15 orig_add = add.__wrapped__
    16 print('orig_add:', orig_add(3, 5))

    以上代码的执行结果为:

    orig_add: 8

    定义一个带参数的装饰器

    问题:

      你想定义一个可以接受参数的装饰器

    解决方案:

      我们用一个例子详细阐述下接受参数的处理过程。假设你想写一个装饰器,给函数添加日志功能,当时允许用户指定日志的级别和其他的选项。下面是这个装饰器的定义和使用示例:

     1 from functools import wraps
     2 import logging
     3 
     4 def logged(level, name=None, message=None):
     5     """
     6     Add logging to a function. level is the logging
     7     level, name is the logger name, and message is the
     8     log message. If name and message aren't specified,
     9     they default to the function's module and name.
    10     """
    11     def decorate(func):
    12         logname = name if name else func.__module__
    13         log = logging.getLogger(logname)
    14         logmsg = message if message else func.__name__
    15 
    16         @wraps(func)
    17         def wrapper(*args, **kwargs):
    18             log.log(level, logmsg)
    19             return func(*args, **kwargs)
    20         return wrapper
    21     return decorate
    22 
    23 @logging(logging.DEBUG)         #等同于logging(loggin.DEBUG)(add)(x,y)
    24 def add(x, y):
    25     return x +y
    26 
    27 
    28 @logged(logging.CRITICAL, 'example')
    29 def spam():
    30     print('Spam!')

    可自定义属性的装饰器

    问题:

      你想写一个装饰器来包装一个函数,并且允许用户提供参数在运行时控制装饰器行为

    解决方案:

      引入一个访问函数,使用nolocal 来修改内部变量。然后这个访问函数被作为一个属性赋值给包装函数

     1 def logged(level, name=None, message=None):
     2     '''
     3     Add logging to a function. level is the logging
     4     level, name is the logger name, and message is the
     5     log message. If name and message aren't specified,
     6     they default
     7     '''
     8     def decorate(func):
     9         logname = name if name else func.__module__
    10         log = logging.getLogger(logname)
    11         logmsg = message if message else func.__name__
    12 
    13         @wraps(func)
    14         def wrapper(*args, **kwargs):
    15             log.log(level, logmsg)
    16             return func(*args, **kwargs)
    17 
    18         @attch_wrapper(wrapper)
    19         def set_level(newlevel):
    20             nonlocal level
    21             level = newlevel
    22 
    23         @attch_wrapper(wrapper)
    24         def set_message(newmsg):
    25             nonlocal logmsg
    26             logmsg = newmsg
    27 
    28         return wrapper
    29 
    30     return decorate
    31 
    32 @logged(logging.DEBUG)
    33 def add(x, y):
    34     return x + y
    35 
    36 @logged(logging.CRITICAL, 'example')
    37 def spam():
    38     print('Spam!')
    39 
    40 import logging
    41 logging.basicConfig(level=logging.DEBUG)
    42 print(add(10, 11))

    以上代码的执行结果为:

    21
    DEBUG:__main__:add

    带可选参数的装饰器

     问题:

      你想写一个装饰器,既可以不传参数给它,比如@decorator ,也可以传递可选参数给它,比如@decorator(x,y,z) 

    解决方案:

      下面是日志装饰器的一个修改版本:

     1 from functools import wraps, partial
     2 import logging
     3 
     4 
     5 def logged(func=None, *, level=logging.DEBUG, name=None, message=None):
     6     if func is None:
     7         return partial(logged, level=level, name=name, message=message)
     8 
     9     logname = name if name else func.__module__
    10     log = logging.getLogger(logname)
    11     logmsg = message if message else func.__name__
    12 
    13     @wraps(func)
    14     def wrapper(*args, **kwargs):
    15         log.log(level, logmsg)
    16         return func(*args, **kwargs)
    17 
    18     return wrapper
    19 
    20 @logged
    21 def add(x, y):
    22     return x + y
    23 
    24 @logged(level=logging.CRITICAL, name='example')
    25 def spam():
    26     print('Spam!')
    27 
    28 print(add(10, 20))
    29 print('*'*40)
    30 spam()

    以上代码的执行结果为:

    30
    spam
    ****************************************
    Spam!

    总结:

      这里提到的这个问题就是通常所说的编程一致性问题。当我们使用装饰器的时候,大部分程序员习惯了要么不给它们传递任何参数,要么给它们传递确切参数。其实从技术上来讲,我们可以定义一个所有参数都是可选的装饰器,就像下面这样: 

    @logged
    def add(x, y):
        """
        等同于 add = logged(add)
        这时候,被装饰函数会被当做第一个参数直接传递给logged 装饰器。因此,logged() 中的第一个参数就是被包装函数本身。所有其他参数都必须有默认值
        """
        return x + y
    
    @logged(level=logging.CRITICAL, name='example')
    def spam():
        """
        等同于 spam = logged(level=logging.CRITICAL, name='example')(spam)
        
        初始调用logged() 函数时,被包装函数并没有传递进来。因此在装饰器内,它必
        须是可选的。这个反过来会迫使其他参数必须使用关键字来指定。并且,但这些参数
        被传递进来后,装饰器要返回一个接受一个函数参数并包装它的函数。为了这样做,我
        们使用了一个技巧,就是利用functools.partial 。它会返回一个未完全初始化自
        身,除了被包装函数外其他参数都已经确定下来了。
        """
        print('Spam!')

    利用装饰器强制函数上的类型检查

     问题:

       作为某种编程规约,你想在对函数参数进行强制类型检查

    解决方案:

      在演示实际代码前,先说明我们的目标:能对函数参数类型进行断言,类似下面这样:

     1 from inspect import signature
     2 from functools import wraps
     3 
     4 
     5 def typeassert(*ty_args, **ty_kwargs):
     6     def decorate(func):
     7         if not __debug__:
     8             return func
     9 
    10         sig = signature(func)
    11         bound_types = sig.bind_partial(*ty_args, **ty_kwargs).arguments
    12 
    13         @wraps(func)
    14         def wrapper(*args, **kwargs):
    15             bound_values = sig.bind(*args, **kwargs)
    16             for name, value in bound_values.arguments.items():
    17                 if name in bound_types:
    18                     if not isinstance(value, bound_types[name]):
    19                         raise TypeError(
    20                                 'Argument {} must be {}'.format(name, bound_types[name])
    21                                 )
    22             return func(*args, **kwargs)
    23         return wrapper
    24     return decorate
    25 
    26 
    27 @typeassert(int, z=int)
    28 def spam(x, y, z=42):
    29     print(x, y, z)
    30 
    31 spam(1, 2, 3)
    32 spam(1, 'hello')
    33 '''
    34 报错
    35 spam(1, 'hello', 'world')
    36 TypeError: Argument z must be <class 'int'>
    37 '''

    以上代码的执行结果为:

    1 2 3
    1 hello 42

    其次, 这里还对被包装函数的参数签名进行了检查, 我们使用了inspect.signature() 函数。简单来讲,它运行你提取一个可调用对象的参数签名信息。例如:

     1 from inspect import signature
     2 
     3 
     4 def spam(x, y, z=42):
     5     return x + y
     6 
     7 sig = signature(spam)
     8 print('sig:', sig)
     9 print('sig parameters:', sig.parameters)
    10 print('sig paramenters z:', sig.parameters['z'].name)
    11 print('sig paramenters name:', sig.parameters['z'].default)
    12 print('sig paramenters kind:', sig.parameters['z'].kind)

    以上代码的执行结果为:

    sig: (x, y, z=42)
    sig parameters: OrderedDict([('x', <Parameter "x">), ('y', <Parameter "y">), ('z', <Parameter "z=42">)])
    sig paramenters z: z
    sig paramenters name: 42
    sig paramenters kind: POSITIONAL_OR_KEYWORD

    将装饰器定义为类的一部分

     问题:

      你想在类中定义装饰器,并将其作用在其他函数或方法上

    解决方案

      在类里面定义装饰器很简单,但是你首先要确认它的使用方式。比如到底是作为一个实例方法还是类方法。下面我们用例子来阐述它们的不同:

     1 from functools import wraps
     2 
     3 
     4 class A:
     5     def decorator1(self, func):
     6         @wraps(func)
     7         def wrapper(*args, **kwargs):
     8             print('Decorator 1')
     9             return func(*args, **kwargs)
    10         return wrapper
    11 
    12     @classmethod
    13     def decorator2(cls, func):
    14         @wraps(func)
    15         def wrapper(*args, **kwargs):
    16             print('Decorator 2')
    17             return func(*args, **kwargs)
    18         return wrapper
    19 
    20 a = A()
    21 
    22 @a.decorator1
    23 def spam():
    24     pass
    25 
    26 @A.decorator2
    27 def grok():
    28     pass

    为类和静态方法提供装饰器

    问题:

      你想给类或静态方法提供装饰器

    解决方案:

      给类或静态方法提供装饰器是很简单的,不过要确保装饰器在@classmethod 或@staticmethod 之前。例如:

     1 import time
     2 from functools import wraps
     3 
     4 
     5 def timethis(func):
     6     @wraps(func)
     7     def wrapped(*args, **kwargs):
     8         start = time.time()
     9         result = func(*args, **kwargs)
    10         end = time.time()
    11         print(end-start)
    12         return result
    13     return wrapped
    14 
    15 
    16 class Spam:
    17     @timethis
    18     def instnce_method(self, n):
    19         print(self, n)
    20         while n > 0:
    21             n -= 1
    22 
    23     @classmethod
    24     @timethis
    25     def class_method(cls, n):
    26         print(cls, n)
    27         while n > 0:
    28             n -= 1
    29 
    30 s = Spam()
    31 s.instnce_method(100000)
    32 Spam.class_method(100000)

    以上代码的执行结果为:

    <__main__.Spam object at 0x101445630> 100000
    0.011115074157714844
    <class '__main__.Spam'> 100000
    0.009546995162963867

    装饰器为被包装函数增加参数

    问题:

      你想在装饰器中给被包装函数增加额外的参数,但是不能影响这个函数现有的调用规则

    解决方案:

      可以使用关键字参数来给被包装函数增加额外参数。考虑下面的装饰器:

     1 from  functools import wraps
     2 
     3 
     4 def optional_debug(func):
     5     @wraps(func)
     6     def wrapper(*args,debug=False, **kwargs):
     7         if debug:
     8             print('Calling', func.__name__)
     9         return func(*args, **kwargs)
    10 
    11     return wrapper
    12 
    13 @optional_debug
    14 def spam(a, b, c):
    15     print(a, b, c)
    16 
    17 spam(1, 2, 3)
    18 spam(1, 2, 3, debug=True)

    以上代码的执行结果为:

    1 2 3
    Calling spam
    1 2 3

    使用装饰器扩充类的功能

    问题:

      你想通过反省或者重写类定义的某部分来修改它的行为,但是你又不希望使用继承或元类的方式

    解决方案:

      这种情况可能是类装饰器最好的使用场景了。例如,下面是一个重写了特殊方法getattribute 的类装饰器,可以打印日志:

     1 def log_getattribute(cls):
     2     orig_getattribute = cls.__getattribute__
     3 
     4     def new_getattribute(self, name):
     5         print('getting:', name)
     6         return orig_getattribute(self, name)
     7 
     8     cls.__getattribute__ = new_getattribute
     9     return cls
    10 
    11 
    12 @log_getattribute
    13 class A:
    14     def __init__(self, x):
    15         self.x = x
    16 
    17     def spam(self):
    18         pass
    19 
    20 a = A(50)
    21 print(a.x)
    22 a.spam()

    以上代码的执行结果为:

    getting: x
    50
    getting: spam

    使用元类控制实例的创建

    问题:

      你想通过改变实例创建方式来实现单例、缓存或其他类似的特性

    解决方案:

      你可以定义一个元类并自己实现call () 方法。为了演示,假设你不想任何人创建这个类的实例:

     1 class NoInstances(type):
     2     def __call__(self, *args, **kwargs):
     3         raise TypeError("Can't instantiate directly")
     4 
     5 
     6 class Spam(metaclass=NoInstances):
     7     @staticmethod
     8     def grok(x):
     9         print('不允许实例化~')
    10         print('Spam.grok')
    11 
    12 Spam.grok(50)
    13 
    14 '''
    15 s = Spam()
    16 s.grok()
    17 实例化这种不能调用,报错~
    18 '''
    19 
    20 #实现单例模式
    21 class Singleton(type):
    22     def __init__(self, *args, **kwargs):
    23         self.__instance = None
    24         super().__init__(*args, **kwargs)
    25 
    26     def __call__(self, *args, **kwargs):
    27         if self.__instance is None:
    28             self._instance = super().__call__(*args, **kwargs)
    29             return self.__instance
    30         else:
    31             return self.__instance
    32 
    33 
    34 class Spam(metaclass=Singleton):
    35     def __init__(self):
    36         print('Creating Spam')
    37 
    38 a = Spam()
    39 b = Spam()
    40 c = Spam()
    41 print(a is b and b is c)

    以上代码的执行结果为:

    不允许实例化~
    Spam.grok
    Creating Spam
    Creating Spam
    Creating Spam
    True

    定义有可选参数的元类

    问题:

      你想定义一个元类,允许类定义时提供可选参数,这样可以控制或配置类型的创建过程

    解决方案:

      在定义类的时候,Python 允许我们使用‘‘metaclass‘‘关键字参数来指定特定的元类。例如使用抽象基类:

     1 from abc import ABCMeta, abstractclassmethod
     2 
     3 
     4 class IStream(metaclass=ABCMeta):
     5     @abstractclassmethod
     6     def read(self, maxsize=None):
     7         pass
     8 
     9     @abstractclassmethod
    10     def write(self, date):
    11         pass
    12 
    13 
    14 class MyMeta(type):
    15     @classmethod
    16     def __prepare__(cls, name, bases, *, debug=False, synchronize=False):
    17         pass
    18         super().__prepare__(name, bases)
    19 
    20     def __new__(cls, name, bases, ns, *, debug=False, synchronize=False):
    21         pass
    22         return super().__new__(cls, name, bases, ns)
    23 
    24     def __init__(cls, name, bases, ns, *, debug=False, synchronize=False):
    25         pass
    26         super().__init__(name, bases, ns)
    27 
    28 
    29 class Spam(metaclass=MyMeta, debug=True, synchronize=True):
    30     pass

    *args 和**kwargs 的强制参数签名

    问题:

      你有一个函数或方法,它使用*args 和**kwargs 作为参数,这样使得它比较通用,但有时候你想检查传递进来的参数是不是某个你想要的类型

    解决方案: 

      对任何涉及到操作函数调用签名的问题,你都应该使用inspect 模块中的签名特性。我们最主要关注两个类:Signature 和Parameter 。下面是一个创建函数前面的交互例子: 

     1 from inspect import Signature, Parameter
     2 
     3 
     4 parms = [
     5         Parameter('x', Parameter.POSITIONAL_OR_KEYWORD),
     6         Parameter('y', Parameter.POSITIONAL_OR_KEYWORD, default=42),
     7         Parameter('z', Parameter.KEYWORD_ONLY, default=None)
     8         ]
     9 
    10 sig = Signature(parms)
    11 print(sig)
    12 
    13 
    14 def func(*args, **kwargs):
    15     bound_values = sig.bind(*args, **kwargs)
    16     for name, value in bound_values.arguments.items():
    17         print(name, value)
    18 
    19 func(1, 2, z=3)
    20 print(''.center(30, '*'))
    21 func(1)
    22 print(''.center(30, '*'))
    23 func(1, z=3)
    24 print(''.center(30, '*'))
    25 func(y=2, x=1)
    26 print(''.center(30, '-'))
    27 '''
    28 报错鸟,为啥,因为只能传递三个参数呀,你四不四傻~
    29 '''
    30 #func(1, 2, 3, 4)
    31 
    32 
    33 
    34 '''
    35 另外一种写法
    36 '''
    37 print('另外一种写法'.center(30, '*'))
    38 
    39 from inspect import Signature, Parameter,signature
    40 def make_sig(*names):
    41     parms = [Parameter(name,Parameter.POSITIONAL_OR_KEYWORD) for name in names]
    42     return Signature(parms)
    43 
    44 
    45 class Structure:
    46     __signature__ = make_sig()
    47 
    48     def __init__(self, *args, **kwargs):
    49         bound_values = self.__signature__.bind(*args, **kwargs)
    50         for name, value in bound_values.arguments.items():
    51             setattr(self, name, value)
    52 
    53 
    54 class Stock(Structure):
    55     __signature__ = make_sig('name', 'shares', 'price')
    56 
    57 
    58 class Point(Structure):
    59     __signature__ = make_sig('x', 'y')
    60 
    61 
    62 print(signature(Stock))
    63 s1 = Stock('ACME', 100, 490.1)
    64 '''
    65 报错
    66 '''
    67 #s2 = Stock('ACME', 100)
    68 #s3 = Stock('ACME', 100, 490.1, shares=50)

    以上代码的执行结果为:

    (x, y=42, *, z=None)
    x 1
    y 2
    z 3
    ******************************
    x 1
    ******************************
    x 1
    z 3
    ******************************
    x 1
    y 2
    ------------------------------
    ************另外一种写法************
    (name, shares, price)

    避免重复的属性方法

    问题:

      你在类中需要重复的定义一些执行相同逻辑的属性方法,比如进行类型检查,怎样去简化这些重复代码呢?

    解决方案:

      考虑下一个简单的类,它的属性由属性方法包装:

     1 class Person:
     2     def __init__(self, name, age):
     3         self.name = name
     4         self.age = age
     5 
     6     @property
     7     def name(self):
     8         return self._name
     9 
    10     @name.setter
    11     def name(self, value):
    12         if not isinstance(value, str):
    13             raise TypeError('name must be a string')
    14         self._name = value
    15 
    16     @property
    17     def age(self):
    18         return self._age
    19 
    20     @name.setter
    21     def age(self, value):
    22         if not isinstance(value, int):
    23             raise TypeError('age must be an int')
    24         self._age = value
    25 
    26 
    27 #针对上面的问题,我们需要改良一下
    28 def typed_property(name, expected_type):
    29     storage_name = '_' + name
    30 
    31     @property
    32     def prop(self):
    33         return getattr(self, storage_name)
    34 
    35     @prop.setter
    36     def prop(self, value):
    37         if not isinstance(value, expected_type):
    38             raise TypeError('{} must be a {}'.format(name, expected_type))
    39         setattr(self, storage_name, value)
    40 
    41     return prop
    42 
    43 
    44 #测试实例
    45 class Person:
    46     name = typed_property('name', str)
    47     age = typed_property('age', int)
    48 
    49     def __init__(self, name, age):
    50         self.name = name
    51         self.age = age
    52 
    53 '''
    54 报错
    55 p = Person('这尼玛的是大佬', '18')
    56 '''
    57 
    58 #正常显示输出
    59 p = Person('这尼玛的是大佬', 18)

    在局部变量域中执行代码

    问题:

      你想在使用范围内执行某个代码片段,并且希望在执行后所有的结果都不可见

    解决方案: 

      为了理解这个问题,先试试一个简单场景。首先,在全局命名空间内执行一个代码片段:

     1 a = 13
     2 exec('b = a + 10')
     3 print('b的结果为为:', b)
     4 
     5 '''
     6 #执行下面的函数会报错NameError: name 'c' is not defined
     7 def test():
     8     a = 13
     9     exec('c = a + 5')
    10     print(c)
    11 test()
    12 '''
    13 
    14 #解决方法,local()可以获取函数内部的所有变量并以字典的方式返回,
    15 # 变量名称是字典的key,变量的赋值是value,通过获取local中的c的局部变量在赋值给变量即可
    16 def test():
    17     a = 13
    18     loc = locals()
    19     print('before:', loc)
    20     exec('c = a + 5')
    21     c = loc['c']
    22     print('c:', c)
    23     print('after:', loc)
    24 test()

    以上代码的执行结果为:

    b的结果为为: 23
    before: {'a': 13}
    c: 18
    after: {'a': 13, 'loc': {...}, 'c': 18}
  • 相关阅读:
    allure2生成html报告
    H5如何测试?
    请描述什么是性能测试、什么是负载测试、什么是压力测试?
    Web/App端自动化测试对比
    如果给你一台电梯,请问你如何测试它
    PC、APP、H5三端测试的相同与不同
    App测试工具选择
    app测试和web测试的区别
    unittest单元测试框架详解
    Jmeter之逻辑控制器(Logic Controller)
  • 原文地址:https://www.cnblogs.com/demon89/p/7325941.html
Copyright © 2020-2023  润新知