• 让Python更优雅更易读(第二集)


    友情链接

    让Python更优雅更易读(第一集)

    1.装饰器

    1.1装饰器特别适合用来实现以下功能

    1. 运行时校验:在执行阶段进行特定校验,当校验通不过时终止执行。 适合原因:装饰器可以方便地在函数执行前介入,并且可以读取所有参数辅助校验。
    2.  注入额外参数:在函数被调用时自动注入额外的调用参数。适合原因:装饰器的位置在函数头部,非常靠近参数被定义的位置,关联性强。
    3. 缓存执行结果:通过调用参数等输入信息,直接缓存函数执行结果。
    4. 注册函数:将被装饰函数注册为某个外部流程的一部分。适合原因:在定义函数时可以直接完成注册,关联性强。
    5. 替换为复杂对象:将原函数(方法)替换为更复杂的对象,比如类实例或特殊的描述符对象

    1.2装饰器简单实现

    复制代码
    import time
    def cal_time(func):
        def wrapper(*args,**kwargs):
            t1=time.time()
            result=func(*args,**kwargs)
            t2=time.time()
            print(f"{func.__name__} running time: {t2-t1} secs.")
            return result
        return wrapper
    复制代码

    cal_time装饰器接收待装饰函数func作为唯一的位置参数,并在函数内定义了一个新函数:wrapper。

    @cal_time
    def second2():
        time.sleep(2)
    
    second2()#second2 running time: 2.0001144409179688 secs.

    一个无参数装饰器,实现起来较为简单。假如你想实现一个接收参数的装饰器,代码会更复杂一些。

    复制代码
    import time
    def cal_time(print_args=False):
        def decorator(func):
            def wrapper(*args,**kwargs):
                t1=time.time()
                result=func(*args,**kwargs)
                t2=time.time()
                if print_args:
                    print(f'args: {args},kwargs:{kwargs}')
                print(f"{func.__name__} running time: {t2-t1} secs.")
                return result
            return wrapper
        return decorator
    
    @cal_time(print_args=True)
    def second2():
        time.sleep(2)
    
    second2()
    #args: (),kwargs:{}
    #second2 running time: 2.0001144409179688 secs.
    复制代码
    #先进行一次调用,传入装饰器参数,获得第一层内嵌函数
    #进行第二次调用,获取第二层内嵌函数wrapper
    _decorator = cal_time(print_args=True)
    sleepTime = _decorator(second2)

    1.3使用functools.wraps()修饰包装函数

    复制代码
    def calls_counter(func):
        """装饰器:记录函数被调用多少次"""
        counter = 0
        def decorated(*args, **kwargs):
            nonlocal counter
            counter +=1
            return func(*args,**kwargs)
        def print_counter():
            print(f'counter:{counter}')
        #给函数增加额外函数,打印统计函数被调用的次数
        decorated.print_counter = print_counter
        return decorated
    
    @cal_time()
    @calls_counter
    def second2():
        time.sleep(2)
    
    复制代码

    这是一个记录函数被调用多少次的装饰器

    我们发现当我们同时使用上述两个装饰器的时候报错了

    Traceback (most recent call last):
      File "F:/pythonProject1/AutomaticTesting/single.py", line 33, in 
        second2.print_counter()
    AttributeError: 'function' object has no attribute 'print_counter'

    首先,由calls_counter对函数进行包装,此时的second2变成了新的包装函数,包含print_counter属性

    使用cal_time包装后,second2变成了cal_time提供的包装函数,原包装函数额外的print_counter属性被自然地丢掉了

    要解决上述问题只要引入装饰器wraps就可以了

    复制代码
    import time
    from functools import wraps
    
    def cal_time(print_args=False):
        def decorator(func):
            @wraps(func)
            def wrapper(*args,**kwargs):
                ...
    
    def calls_counter(func):
        """装饰器:记录函数被调用多少次"""
        counter = 0
    
        @wraps(func)
        def decorated(*args, **kwargs):
            ...
    
    @cal_time()
    @calls_counter
    def second2():
        time.sleep(2)
    #
    second2()
    second2.print_counter()
    #second2 running time: 2.0001144409179688 secs.
    #counter:1
    复制代码

    1.4可选参数的装饰器

    以上数的cal_time为例

    有了参数以后我们不仅在装饰器使用时候@必须带上()

    复制代码
    def cal_time(func=None,*,print_args=False):
        def decorator(_func):
            @wraps(_func)
            def wrapper(*args,**kwargs):
                t1=time.time()
                result=func(*args,**kwargs)
                t2=time.time()
                if print_args:
                    print(f'args: {args},kwargs:{kwargs}')
                print(f"{_func.__name__} running time: {t2-t1} secs.")
                return result
            return wrapper
        if func is None:
            return decorator
        else:
            return decorator(func)
    @cal_time
    @calls_counter
    def second2():
    time.sleep(2)
    复制代码

    这时候调用就不需要()了

    1.5用类来实现装饰器(函数替换)

    能否用装饰器形式使用只有一个判断标准,就是是否是可调用的对象

    如果一个类实现了__call__魔法方法,那么他的实例就是可调用对象

    现在我们把计时装饰器改写

    复制代码
    import time
    from functools import wraps
    class cal_time:
        """装饰器:记录函数用时"""
        def __init__(self,print_arg=False):
            self.print_arg = print_arg
    
        def __call__(self, func):
            @wraps(func)
            def wrapper(*args,**kwargs):
                t1=time.time()
                result=func(*args,**kwargs)
                t2=time.time()
                if self.print_arg:
                    print(f'args: {args},kwargs:{kwargs}')
                print(f"{func.__name__} running time: {t2-t1} secs.")
                return result
            return wrapper
    复制代码

     2数据模型与描述符

    数据模型有关的方法,基本都以双下划线开头和结尾,它们通常被称为魔法方法

    例如:我们打印对象的时候输出的是<类名+内存地址>

    1
    2
    3
    4
    5
    6
    class Person:
     
        def __init__(self, name):
            self.name = name
     
    print(Person("yetangjian"))#<__main__.Person object at 0x000001BA41805FD0>

     __str__就是Python数据模型里最基础的一部分。当对象需要当作字符串使用时,我们可以用__str__方法来定义对象的字符串化结果

    注:除了print()以外,str()与.format()函数同样也会触发__str__方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Person:
     
        ...
     
        def __str__(self):
            return self.name
     
    print(Person("yetangjian")) #yetangjian
    print(f'l am {Person("yetangjian")}') #l am yetangjian

     常见魔法方法

    01. __repr__

    在如下的例子中,使用了一个{name!r}这样的语法

    变量名后的!r表示优先使用repr方法,再使用str方法。针对字符串类型会自动给变量加上引号,省去了手动添加的麻烦。

    1
    2
    3
    name='yetangjian'
    age = 18
    print(f"{name!r},{age!r}")#'yetangjian',18

     同样我们实现的方法与str方法类似,我们依旧使用上述的例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Person:
     
        ...
     
        def __repr__(self):
            return f"{self.name!r},{self.age!r}"
     
    p=Person("yetangjian",80)
    print(repr(p))#'yetangjian',80
     02.__format__

    定义对象在字符串格式化时的行为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Person:
     
        ...
     
        def __format__(self, format_spec):
            if format_spec == "all":
                return f"{self.name!r},{self.age!r}"
            else:
                return f"{self.name!r}"
     
    p=Person("yetangjian",80)
    print(f"all:{p:all}") #all:'yetangjian',80
    print("only name:{p:simple}".format(p=p)) #only name:'yetangjian'

    模板语法不仅适用于format,同样适用于f-string

    03比较运算符重载
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    class Num:
     
        def __init__(self,number):
            self.n = number
        #等于
        def __eq__(self, other):
            if isinstance(other,self.__class__):
                return other.n == self.n
            return False
        #不等于
        def __ne__(self, other):
            return not (self == other)
     
        def __lt__(self, other):
            if isinstance(other,self.__class__):
                return self.n < other.n
            #不支持某种运算,可以返回NotImplemented
            return NotImplemented
        #小于等于
        def __le__(self, other):
            return self.__lt__(other) or self.__eq__(other)
     
    num1 = Num(5)
    num2 = Num(10)
    print(num1 <= num2) #True

     但是我们会发现重载这些运算符号代码量实在太大,而且较为重复。下面推荐一个工具,简化这个工作量

    @total_ordering

    使用functools下的这个装饰器,我们只需要实现__eq__方法,__lt__、__le__、__gt__、__ge__四个方法里随意挑一个实现即可,@total_ordering会帮你自动补全剩下的所有方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    from functools import total_ordering
     
    @total_ordering
    class Num:
     
        def __init__(self,number):
            self.n = number
        #等于
        def __eq__(self, other):
            if isinstance(other,self.__class__):
                return other.n == self.n
            return False
     
        def __lt__(self, other):
            if isinstance(other,self.__class__):
                return self.n < other.n
            #不支持某种运算,可以返回NotImplemented
            return NotImplemented
     
    num1 = Num(5)
    num2 = Num(10)
    print(num1 <= num2) #True

     描述符

     使用property做校验
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Count:
     
        def __init__(self,c):
            self.__math = c
        @property
        def math(self):
            return self.__math
        @math.setter
        def math(self,v):
            if v > 50:
                raise ValueError("数字大于100")
            self.__math = v
     
    c = Count(5)
    c.math = 40
    print(c.math) #40

     描述符(descriptor)是Python对象模型里的一种特殊协议,它主要和4个魔法方法有关: __get__、__set__、__delete__和__set_name__

    任何一个实现了__get__、__set__或__delete__的类,都可以称为描述符类,它的实例则叫作描述符对象

    __get__
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class Info:
        def __get__(self, instance, owner=None):
            """
            __get__方法存在两个参数
            instance:当通过实例来访问描述符属性,该参数为实例对象;
                     如果通过类访问,则为None
            owner:描述符对象所绑定的类
            """
            print(f'__get__,{instance},{owner}')
            if not instance:
                return self
     
    class Foo:
        #要使用一个描述符,最常见的方式是把它的实例对象设置为其他类(常被称为owner类)的属性
        bar = Info()
     
    print(Foo.bar)
    print(Foo().bar)
    1
    2
    3
    4
    5
    6
    7
    8
    """
    通过类来访问,所以instance为None,返回描述符本身
    __get__,None,
    <__main__.Info object at 0x0000000001D644F0>
    通过实例来访问
    __get__,<__main__.Foo object at 0x00000000026149D0>,
    None
    """
     __set__
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Info:
        ......
     
        def __set__(self, instance, value):
            """
            __set__方法存在两个参数
            instance:属性当前绑定的实例对象
            value:待设置的属性值
            """
            print(f'__set__,{instance},{value}')
     
     
    Foo().bar = 10#__set__,<__main__.Foo object at 0x0000000001DE49D0>,10

     描述符的__set__仅对实例起作用,对类不起作用。这和__get__方法不一样

    使用描述符实现校验
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    class IntegerField:
        """整型字段,只允许一定范围内的整型值
        :param min_value: 允许的最小值
        :param max_value: 允许的最大值
        """
     
        def __init__(self, min_value, max_value):
            self.min_value = min_value
            self.max_value = max_value
     
     
        def __get__(self, instance,owner=None):
            # 当不是通过实例访问时,直接返回描述符对象
            if not instance:
                return self
                # 返回保存在实例字典里的值
            return instance.__dict__['_integer_field']
     
        def __set__(self, instance, value):
            # 校验后将值保存在实例字典里
            value = self._validate_value(value)
            instance.__dict__['_integer_field'] = value
     
        def _validate_value(self, value):
            """校验值是否为符合要求的整数"""
            try:
                value = int(value)
            except (TypeError, ValueError):
                raise ValueError('value is not a valid integer!')
            if not (self.min_value <= value <= self.max_value):
                raise ValueError(f'value must between {self.min_value} and {self.max_value}!')
            return value

     因为每个描述符对象都是owner类的属性,而不是类实例的属性,所以我们用的都是instance.dict而不是用self.dict。如果把值都存入self中就会存在互相覆盖,值冲突的情况

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Person:
        age = IntegerField(min_value=10,max_value=100)
     
        def __init__(self,age):
            self.age = age
     
    p = Person(110)
    """
    raise ValueError(f'value must between {self.min_value} and {self.max_value}!')
    ValueError: value must between 10 and 100!
    """

     

  • 相关阅读:
    以太坊“共识层”客户端prysm和teku对比选型
    angular记录
    C++ 移动构造函数详解
    修改克隆虚拟机的静态ip地址
    vue3+vite 学习 创建项目
    降维算法实战项目(1)—使用PCA对二维数据降维(Python代码+数据集)
    架构师之路,从「存储选型」起步
    【译】使用最新预览版查看您的拉请求注释
    Java中使用alibaba easyexcel导出Excel,合并单元格
    STM32F407ZGT6|实现中断操作
  • 原文地址:https://www.cnblogs.com/yetangjian/p/16583249.html