python:__set_name__使用

python:__set_name__使用

1 前言

在Python中,我们可以通过__set_name__方法来实现一些特殊的操作。该方法是在定义类的时候被调用,用于设置属性的名称。这样一来,我们就可以在类定义中动态地获取属性的名称,从而更好地完成一些操作。

参考functools中的cached_property类中的__set_name__方法使用,探讨python中__set_name__的使用场景,cached_property源码如下(本文基于python 3.9):

class cached_property:
    def __init__(self, func):
        self.func = func
        self.attrname = None
        self.__doc__ = func.__doc__
        self.lock = RLock()

    def __set_name__(self, owner, name):
        if self.attrname is None:
            self.attrname = name
        elif name != self.attrname:
            raise TypeError(
                "Cannot assign the same cached_property to two different names "
                f"({self.attrname!r} and {name!r})."
            )

    def __get__(self, instance, owner=None):
        if instance is None:
            return self
        if self.attrname is None:
            raise TypeError(
                "Cannot use cached_property instance without calling __set_name__ on it.")
        try:
            cache = instance.__dict__
        except AttributeError:  # not all objects have __dict__ (e.g. class defines slots)
            msg = (
                f"No '__dict__' attribute on {type(instance).__name__!r} "
                f"instance to cache {self.attrname!r} property."
            )
            raise TypeError(msg) from None
        val = cache.get(self.attrname, _NOT_FOUND)
        if val is _NOT_FOUND:
            with self.lock:
                # check if another thread filled cache while we awaited lock
                val = cache.get(self.attrname, _NOT_FOUND)
                if val is _NOT_FOUND:
                    val = self.func(instance)
                    try:
                        cache[self.attrname] = val
                    except TypeError:
                        msg = (
                            f"The '__dict__' attribute on {type(instance).__name__!r} instance "
                            f"does not support item assignment for caching {self.attrname!r} property."
                        )
                        raise TypeError(msg) from None
        return val

    __class_getitem__ = classmethod(GenericAlias)

2 使用

2.1 初识描述器和__set_name__方法的简单使用

参考官方文档:

https://docs.python.org/zh-cn/3.9/reference/datamodel.html#slots

实现描述器中提到:

以下方法仅当一个包含该方法的类(称为 描述器 类)的实例出现于一个 所有者 类中的时候才会起作用(该描述器必须在所有者类或其某个上级类的字典中)。在以下示例中,“属性”指的是名称为所有者类 __dict__ 中的特征属性的键名的属性。

意即:具有以下任一方法(__get__、__set__、__delete__、__set_name__)的类被称为描述器类,而该描述器类的实例对象(类名(*args, **kwargs)为实例对象),必须存在于所有类中,描述器类定义的这些方法,才会生效执行。也就是比如如下所示:

class MyClass:
    def __new__(cls, *args, **kwargs):
        print("__new__")
        return super(cls, MyClass).__new__(cls)

    def __init__(self):
        print("__init__")
        self._name = None

    def set_name(self, value):
        print("set_name", value)
        self._name = value

    def __set_name__(self, owner, name):
        print("__set_name__", owner, name)
        self._name = name

    def __get__(self, instance, owner):
        print("__get__  MyClass", instance, owner)

    def __set__(self, instance, value):
        print("__set__  MyClass", instance, value)

    def __delete__(self, instance):
        print("__delete__  MyClass", instance)


class Person:
    special = MyClass()

上述的MyClass类定义了__set_name__方法,也就是一个描述器类,而Person的special属性为MyClass描述器类的实例,也就是说MyClass描述器类的实例对象,存在于Person所有类中,那么就会在定义Person类时,会自动调用MyClass描述器类的__set_name__方法,上述代码执行结果如下:

__new__
__init__
__set_name__ <class '__main__.Person'> special

可见,__set_name__方法中传入的owner是描述器类实例的所有类Person,而name是所有类Person中,承接这个描述器类实例的属性名称,为special:

在这里插入图片描述

描述器类的方法如下:

  • object.__get__(self, instance, owner=None)

调用此方法以获取所有者类的属性(类属性访问)或该类的实例的属性(实例属性访问)。 可选的 owner 参数是所有者类而 instance 是被用来访问属性的实例,如果通过 owner 来访问属性则返回 None。

此方法应当返回计算得到的属性值或是引发 AttributeError 异常。

PEP 252 指明 __get__() 为带有一至二个参数的可调用对象。 Python 自身内置的描述器支持此规格定义;但是,某些第三方工具可能要求必须带两个参数。 Python 自身的 __getattribute__() 实现总是会传入两个参数,无论它们是否被要求提供。

  • object.__set__(self, instance, value)

调用此方法以设置 instance 指定的所有者类的实例的属性为新值 value。

请注意,添加 __set__() 或 __delete__() 会将描述器变成“数据描述器”。

  • object.__delete__(self, instance)

调用此方法以删除 instance 指定的所有者类的实例的属性。

  • object.__set_name__(self, owner, name)

在所有者类 owner 创建时被调用。描述器会被赋值给 name。

注意: __set_name__() 只是作为 type 构造器的一部分被隐式地调用,因此在某个类被初次创建之后又额外添加一个描述器时,那就需要显式地调用它并且附带适当的形参:

针对此举个栗子:

class MyClass:
    def __new__(cls, *args, **kwargs):
        print("__new__")
        return super(cls, MyClass).__new__(cls)

    def __init__(self):
        print("__init__")
        self._name = None

    def set_name(self, value):
        print("set_name", value)
        self._name = value

    def __set_name__(self, owner, name):
        print("__set_name__", owner, name)
        self._name = name

    def __get__(self, instance, owner):
        print("__get__  MyClass", instance, owner)

    def __set__(self, instance, value):
        print("__set__  MyClass", instance, value)

    def __delete__(self, instance):
        print("__delete__  MyClass", instance)


class Person:
    pass


Person.desc = MyClass()

上述代码执行结果如下:

在这里插入图片描述

上述的栗子可知,我们并没有在Person类中定义MyClass描述器类实例的属性,而是在定义了Person类之后,为其添加属性desc,值为MyClass描述器类实例,可见并没有自动执行MyClass的__set_name__方法,即如上述所说,此时我们需要显式地调用MyClass的__set_name__方法并且附带适当的形参,修改如下:

m = MyClass()
Person.desc = m
m.__set_name__(Person, "desc")

结果如下:

在这里插入图片描述

上述我们调用__set_name__方法时,第一个参数是所有类,即Person;而第二个参数是描述器实例属性的对应名称,即’desc’。可见此时执行的结果,就和我们在定义所有类时就为其属性赋值为描述器类实例的效果一致了。

2.2 创建类对象官方文档说明

创建类对象参考文档:

https://docs.python.org/zh-cn/3.9/reference/datamodel.html#class-object-creation

创建类对象中提到如下:

一旦执行类主体完成填充类命名空间,将通过调用 metaclass(name, bases, namespace, **kwds) 创建类对象(此处的附加关键字参数与传入 __prepare__ 的相同)。

如果类主体中有任何方法引用了 __class__ 或 super,这个类对象会通过零参数形式的 super(). __class__ 所引用,这是由编译器所创建的隐式闭包引用。这使用零参数形式的 super() 能够正确标识正在基于词法作用域来定义的类,而被用于进行当前调用的类或实例则是基于传递给方法的第一个参数来标识的。

CPython implementation detail: 在 CPython 3.6 及之后的版本中,__class__ 单元会作为类命名空间中的 __classcell__ 条目被传给元类。 如果存在,它必须被向上传播给 type.__new__ 调用,以便能正确地初始化该类。 如果不这样做,在 Python 3.8 中将引发 RuntimeError。

当使用默认的元类 type 或者任何最终会调用 type.__new__ 的元类时,以下额外的自定义步骤将在创建类对象之后被发起调用:

  • 首先,type.__new__ 将收集类命名空间中所有定义了 __set_name__() 方法的描述器;
  • 接下来,所有这些 __set_name__ 方法将使用所定义的类和特定描述器所赋的名称进行调用;
  • 最后,将在新类根据方法解析顺序所确定的直接父类上调用 __init_subclass__() 钩子。

在类对象创建之后,它会被传给包含在类定义中的类装饰器(如果有的话),得到的对象将作为已定义的类绑定到局部命名空间。

当通过 type.__new__ 创建一个新类时,提供以作为命名空间形参的对象会被复制到一个新的有序映射并丢弃原对象。这个新副本包装于一个只读代理中,后者则成为类对象的 __dict__ 属性。

因上述提到了type,这里我们简单分析下type类的使用:

参考官方文档type:

https://docs.python.org/zh-cn/3.9/library/functions.html#type

class type(name, bases, dict, **kwds)

传入一个参数时,返回 object 的类型。 返回值是一个 type 对象,通常与 object.__class__ 所返回的对象相同。

推荐使用 isinstance() 内置函数来检测对象的类型,因为它会考虑子类的情况。

传入三个参数时,返回一个新的 type 对象。 这在本质上是 class 语句的一种动态形式,name 字符串即类名并会成为 __name__ 属性;bases 元组包含基类并会成为 __bases__ 属性;如果为空则会添加所有类的终极基类 object。 dict 字典包含类主体的属性和方法定义;它在成为 __dict__ 属性之前可能会被拷贝或包装。

举个简单栗子,下面两条语句会创建相同的 type 对象:

class Xiaoxu:
    age = 0


x = type('Xiaoxu', (), dict(age=0))

print(type(x))
print(x.__name__)
print(x.__bases__)
print(x.__dict__)

print("*" * 10)
print(isinstance(x, Xiaoxu))
# False

print(type(Xiaoxu))
print(Xiaoxu.__name__)
print(Xiaoxu.__bases__)
print(Xiaoxu.__dict__)

print("*" * 10)
print(Xiaoxu.__name__ == x.__name__)
print(Xiaoxu.__bases__ == x.__bases__)
print(Xiaoxu.__dict__ == x.__dict__)
# True
# True
# False

执行结果如下:

<class 'type'>
Xiaoxu
(<class 'object'>,)
{'age': 0, '__module__': '__main__', '__dict__': <attribute '__dict__' of 'Xiaoxu' objects>, '__weakref__': <attribute '__weakref__' of 'Xiaoxu' objects>, '__doc__': None}
**********
False
<class 'type'>
Xiaoxu
(<class 'object'>,)
{'__module__': '__main__', 'age': 0, '__dict__': <attribute '__dict__' of 'Xiaoxu' objects>, '__weakref__': <attribute '__weakref__' of 'Xiaoxu' objects>, '__doc__': None}
**********
True
True
False

提供给三参数形式的关键字参数会被传递给适当的元类机制 (通常为 __init_subclass__()),相当于类定义中关键字 (除了 metaclass) 的行为方式。

在 3.6 版更改: type 的子类如果未重载 type.__new__,将不再能使用一个参数的形式来获取对象的类型。

针对上述的说明,举个栗子,在所有类Owner中创建多个不同的描述器类实例对象属性:

class Ma:
    def __new__(cls, *args, **kwargs):
        print("__new__ Ma")
        return super(cls, Ma).__new__(cls)

    def __init__(self):
        self._name = None
        print("__init__ Ma", self._name)

    def __set_name__(self, owner, name):
        print("__set_name__ Ma", owner, name)
        self._name = name


class Mb:
    def __new__(cls, *args, **kwargs):
        print("__new__ Mb")
        return super(cls, Mb).__new__(cls)

    def __init__(self):
        self._age = None
        print("__init__ Mb", self._age)

    def __set_name__(self, owner, name):
        print("__set_name__ Mb", owner, name)
        self._age = name


class Owner:
    a = Ma()
    b = Mb()

执行结果如下:

在这里插入图片描述

根据上述可知,使用默认的元类 type,也可以自动触发收集类命名空间中所有定义了__set_name__() 方法的描述器,并分别执行其__set_name__() 方法的操作,演示如下:

class Ma:
    def __new__(cls, *args, **kwargs):
        print("__new__ Ma")
        return super(cls, Ma).__new__(cls)

    def __init__(self):
        self._name = None
        print("__init__ Ma", self._name)

    def __set_name__(self, owner, name):
        print("__set_name__ Ma", owner, name)
        self._name = name


class Mb:
    def __new__(cls, *args, **kwargs):
        print("__new__ Mb")
        return super(cls, Mb).__new__(cls)

    def __init__(self):
        self._age = None
        print("__init__ Mb", self._age)

    def __set_name__(self, owner, name):
        print("__set_name__ Mb", owner, name)
        self._age = name


# class Owner:
#     a = Ma()
#     b = Mb()

# type,参数分别为:类名,继承基类元组,类的属性
Owner = type("Owner", (), dict({"a": Ma(), "b": Mb()}))

执行结果和上述一致:

在这里插入图片描述

但是注意,上述方式改为如下:

Owner = type("Owner", (), dict({"a": Ma()}))
Owner.b = Mb()

执行结果如下:

在这里插入图片描述

可见这种方式也不会自动执行描述器Mb。

修改Owner如下:

class Owner:
    a = Ma()
    b = Mb()

    def __new__(cls, *args, **kwargs):
        print("__new__ Owner")
        return super(cls, Owner).__new__(cls)

    def __init__(self):
        print("__init__ Owner")
        print(self.a._name)
        print(self.b._age)

c = Owner()

结果如下:

在这里插入图片描述

其实,实际使用中,描述器类自定义__set_name__方法,更常见于装饰器的使用,如下变式可见:

class Ma:
    def __new__(cls, *args, **kwargs):
        print("__new__ Ma")
        return super(cls, Ma).__new__(cls)

    def __init__(self, func):
        self._name = None
        self.func = func
        print("__init__ Ma", self._name)

    def __set_name__(self, owner, name):
        print("__set_name__ Ma", owner, name)
        self._name = name


class Mb:
    def __new__(cls, *args, **kwargs):
        print("__new__ Mb")
        return super(cls, Mb).__new__(cls)

    def __init__(self, func):
        self._age = None
        self.func = func
        print("__init__ Mb", self._age)

    def __set_name__(self, owner, name):
        print("__set_name__ Mb", owner, name)
        self._age = name


class Owner:
    @Ma
    def a(self):
        pass

    @Mb
    def b(self):
        pass

执行结果:

在这里插入图片描述

据结果可知,和我们上述在所有类中定义描述器类实例属性的效果是一致的。上述变式的重点是,修改描述器的__init__方法,增加func参数,然后在所有类Owner中,描述器作为装饰器修改所有类Owner的方法a或者b,装饰器的形式如下:

@wrap
def run():
	pass

形如:

run = wrap(run)

所以在Owner类中,使用描述器类修饰方法,依然符合前面提到的,描述器类的实例必须存于所有类的属性中,于是就会自动调用描述器类的__set_name__方法。

2.3 __set_name__的详细使用

有了前面的概念分析,然后我们开始参考官方文档,描述器使用指南,进行下述的__set_name__使用分析:

https://docs.python.org/zh-cn/3.9/howto/descriptor.html#member-objects-and-slots

2.3.1 定制名称

当一个类使用描述器时,它可以告知每个描述器使用了什么变量名。

在此示例中, People类具有两个描述器实例 name 和 age。当类People被定义的时候,他回调了 LoggedAccess 中的 __set_name__() 来记录字段名称,让每个描述器拥有自己的 public_name 和 private_name:

import logging

logging.basicConfig(level=logging.INFO)


class LoggedAccess:

    def __set_name__(self, owner, name):
        self.public_name = name
        self.private_name = '_' + name

    def __get__(self, instance, owner):
        value = getattr(instance, self.private_name)
        logging.info('Xiaoxu Accessing %r giving %r', self.public_name, value)
        return value

    def __set__(self, instance, value):
        logging.info('Xiaoxu Access Updating %r to %r', self.public_name, value)
        setattr(instance, self.private_name, value)


class People:
    # First descriptor instance
    name = LoggedAccess()

    # Second descriptor instance
    age = LoggedAccess()

    def __init__(self, name, age):
        self.name = name  # Calls the first descriptor
        self.age = age  # Calls the second descriptor

    def birthday(self):
        self.age += 1


x = People("xiaoxu", 99)
# INFO:root:Xiaoxu Access Updating 'name' to 'xiaoxu'
# INFO:root:Xiaoxu Access Updating 'age' to 99

print(x.name)
# INFO:root:Xiaoxu Accessing 'name' giving 'xiaoxu'
# xiaoxu

print(x.age)
# INFO:root:Xiaoxu Accessing 'age' giving 99
# 99

l = People("xiaoli", 18)
# INFO:root:Xiaoxu Access Updating 'name' to 'xiaoli'
# INFO:root:Xiaoxu Access Updating 'age' to 18

执行结果如下:

在这里插入图片描述

同时,这两个People实例仅包含私有名称:

print(vars(x))
print(vars(l))

结果如下:

在这里插入图片描述

我们调用 vars() 来查找描述器而不触发它:

print(vars(vars(People)['name']))
print(vars(vars(People)['age']))

结果:

{'public_name': 'name', 'private_name': '_name'}
{'public_name': 'age', 'private_name': '_age'}

在这里插入图片描述

此处小结:

descriptor 就是任何一个定义了 __get__(),__set__() 或 __delete__() 的对象

可选地,描述器可以具有 __set_name__() 方法。这仅在描述器需要知道创建它的类或分配给它的类变量名称时使用。(即使该类不是描述器,只要此方法存在就会调用。)

在属性查找期间,描述器由点运算符调用。如果使用 vars(some_class)[descriptor_name] 间接访问描述器,则返回描述器实例而不调用它。

描述器仅在用作类变量时起作用。放入实例时,它们将失效。

描述器的主要目的是提供一个挂钩,允许存储在类变量中的对象控制在属性查找期间发生的情况。

传统上,调用类控制查找过程中发生的事情。描述器反转了这种关系,并允许正在被查询的数据对此进行干涉。

描述器的使用贯穿了整个语言。就是它让函数变成绑定方法。常见工具诸如 classmethod(), staticmethod(),property() 和 functools.cached_property() 都作为描述器实现。

2.3.2 验证器类

验证器是一个用于托管属性访问的描述器。在存储任何数据之前,它会验证新值是否满足各种类型和范围限制。如果不满足这些限制,它将引发异常,从源头上防止数据损坏。

这个 Validator 类既是一个 abstract base class (抽象基类)也是一个托管属性描述器

from abc import ABC, abstractmethod


# XValidator继承了ABC抽象基类,作为一个抽象类使用
# 类似java中通过abstract定义的抽象类
class XValidator(ABC):
    def __set_name__(self, owner, name):
        self.private_name = "_" + name
        self.original_name = name

    def __get__(self, instance, owner):
        return getattr(instance, self.private_name)

    def __set__(self, instance, value):
        self.validate(self.original_name, value)
        setattr(instance, self.private_name, value)

    @abstractmethod
    def validate(self, original_name, value):
        pass

自定义验证器需要从 Validator 继承,并且必须提供 validate() 方法以根据需要测试各种约束。

自定义验证器

这是三个实用的数据验证工具:

  • OneOf 验证值是一组受约束的选项之一。
  • Number 验证值是否为 int 或 float。根据可选参数,它还可以验证值在给定的最小值或最大值之间。
  • String 验证值是否为 str。根据可选参数,它可以验证给定的最小或最大长度。它还可以验证用户定义的 predicate。
class OneOf(XValidator):
    def __init__(self, *options):
        # 将tuple元组形式的options转换为set
        self.options = set(options)

    def validate(self, original_name, value):
        # {val!s}形如test; {val!r}形如'test'
        # !s相当于str(val); !r相当于repr(val)
        if value not in self.options:
            raise ValueError(f'Error field {original_name} set.'
                             f'Expected {value!r} to be one of {self.options!r}')


class Number(XValidator):

    def __init__(self, minvalue=None, maxvalue=None):
        self.minvalue = minvalue
        self.maxvalue = maxvalue

    def validate(self, original_name, value):
        if not isinstance(value, (int, float)):
            raise TypeError(f'Expected {value!r} to be an int or float')
        if self.minvalue is not None and value < self.minvalue:
            raise ValueError(
                f'Expected {value!r} to be at least {self.minvalue!r}'
            )
        if self.maxvalue is not None and value > self.maxvalue:
            raise ValueError(
                f'Expected {value!r} to be no more than {self.maxvalue!r}'
            )


class String(XValidator):

    def __init__(self, minsize=None, maxsize=None, predicate=None):
        self.minsize = minsize
        self.maxsize = maxsize
        self.predicate = predicate

    def validate(self, original_name, value):
        if not isinstance(value, str):
            raise TypeError(f'Expected {value!r} to be an str of {original_name!r}')
        if self.minsize is not None and len(value) < self.minsize:
            raise ValueError(
                f'Expected {value!r} to be no smaller than {self.minsize!r}'
                f' of {original_name!r}'
            )
        if self.maxsize is not None and len(value) > self.maxsize:
            raise ValueError(
                f'Expected {value!r} to be no bigger than {self.maxsize!r}'
                f' of {original_name!r}'
            )
        if self.predicate is not None and not self.predicate(value):
            raise ValueError(
                f'Expected {self.predicate} to be true for {value!r} '
                f'of {original_name!r}'
            )

在真实类中使用数据验证器的方法:

class Component:
    name = String(minsize=3, maxsize=10, predicate=str.isupper)
    kind = OneOf('man', 'woman')
    quantity = Number(minvalue=0)

    def __init__(self, name, kind, quantity):
        self.name = name
        self.kind = kind
        self.quantity = quantity


# XIAOXU将不会报错  ctrl + shift + U
c = Component('xiaoxu', 'man', 5)

执行结果如下:

在这里插入图片描述

其余使用场景的演示:

Component('XIAOXU', 'test', 5)

校验结果:

在这里插入图片描述

又比如:

Component('XIAOXU', 'man', -5)

校验结果:

在这里插入图片描述

Component('XIAOXU', 'man', "yes")

校验结果:

在这里插入图片描述

正确使用场景,将不会抛出异常:

Component('XIAOXU', 'man', 99)

这里再举一个验证器的栗子

from typing import Callable, Any


class Validation:

    def __init__(
            self, validation_function: Callable[[Any], bool], error_msg: str
    ) -> None:
        print("Validation初始化被执行")
        self.validation_function = validation_function  # 传进来的是匿名函数
        self.error_msg = error_msg

    def __call__(self, value):
        print("call被执行")
        if not self.validation_function(value):  # lambda x: isinstance(x, (int, float))
            raise ValueError(f"{value!r} {self.error_msg}")


class Field:  # 描述符类

    def __init__(self, *validations):  # 用*接收,表示可以传多个,目前代码可以理解为传进来的就是一个个Validation的实例
        print("Field初始化被执行")
        self._name = None
        self.validations = validations  # 接收完后的类型是元组

    def __set_name__(self, owner, name):
        print("set_name被执行")
        self._name = name  # 会自动将托管类ClientClass的类属性descriptor带过来

    def __get__(self, instance, owner):
        print("get被执行")
        if instance is None:
            return self
        return instance.__dict__[self._name]

    def validate(self, value):
        print("验证被执行")
        for validation in self.validations:
            validation(value)  # 这是是将对象当成函数执行时,调用Validation的__call__魔法方法

    def __set__(self, instance, value):
        """
        :param self: 指的是Field对象
        :param instance: ClientClass对象
        :param value: 给属性赋值的值
        :return:
        """
        print("set被执行")
        self.validate(value)
        instance.__dict__[self._name] = value
        # 给ClientClass对象赋值  {"descriptor": 42}


class ClientClass:  # 托管类
    descriptor = Field(
        Validation(lambda x: isinstance(x, (int, float, complex)), "is not a number"),
        # Validation(lambda x: x >= 0, "is not >= 0"),
    )


if __name__ == '__main__':
    """
    Validation初始化被执行
    Field初始化被执行
    set_name被执行  # 当Field()赋值给descriptor变量时,执行__set_name__
    ---------------------
    set被执行
    验证被执行
    call被执行
    """
    client = ClientClass()  # 实例化对象
    print("---------------------")
    # 给上面实例化的对象中的属性(Field实例化对象)赋值为42
    client.descriptor = 42

结果如下:

在这里插入图片描述

若改为如下:

client.descriptor = "xiaoxu"

执行验证,结果将抛出异常:

在这里插入图片描述

基于如上,下面再说明一下描述器的概念:

定义与介绍:

一般而言,描述器是一个包含了描述器协议中的方法的属性值。 这些方法有 __get__(), __set__() 和 __delete__()。 如果为某个属性定义了这些方法中的任意一个,它就可以被称为 descriptor。

属性访问的默认行为是从一个对象的字典中获取、设置或删除属性。对于实例来说,a.x 的查找顺序会从 a.__dict__[‘x’] 开始,然后是 type(a).__dict__[‘x’],接下来依次查找 type(a) 的方法解析顺序(MRO)。 如果找到的值是定义了某个描述器方法的对象,则 Python 可能会重写默认行为并转而发起调用描述器方法。这具体发生在优先级链的哪个环节则要根据所定义的描述器方法及其被调用的方式来决定。

描述器是一个强大而通用的协议。 它们是属性、方法、静态方法、类方法和 super() 背后的实现机制。 它们在 Python 内部被广泛使用。 描述器简化了底层的 C 代码并为 Python 的日常程序提供了一组灵活的新工具。

描述器协议

descr.__get__(self, obj, type=None) -> value

descr.__set__(self, obj, value) -> None

descr.__delete__(self, obj) -> None

描述器的方法就这些。一个对象只要定义了以上方法中的任何一个,就被视为描述器,并在被作为属性时覆盖其默认行为。

如果一个对象定义了 __set__() 或 __delete__(),则它会被视为数据描述器。 仅定义了 __get__() 的描述器称为非数据描述器(它们经常被用于方法,但也可以有其他用途)

数据和非数据描述器的不同之处在于,如何计算实例字典中条目的替代值。如果实例的字典具有与数据描述器同名的条目,则数据描述器优先。如果实例的字典具有与非数据描述器同名的条目,则该字典条目优先。

为了使数据描述器成为只读的,应该同时定义 __get__() __set__() ,并在 __set__() 中引发 AttributeError 。用引发异常的占位符定义 __set__() 方法使其成为数据描述器。

描述器调用概述

描述器可以通过 d.__get__(obj) 或 desc.__get__(None, cls) 直接调用。

但更常见的是通过属性访问自动调用描述器。

表达式 obj.x 在命名空间的链中查找obj 的属性 x。如果搜索在实例 __dict__ 之外找到描述器,则根据下面列出的优先级规则调用其 __get__() 方法。

调用的细节取决于 obj 是对象、类还是超类的实例。

通过实例调用

实例查找通过命名空间链进行扫描,数据描述器的优先级最高,其次是实例变量、非数据描述器、类变量,最后是 __getattr__() (如果存在的话)。

如果 a.x 找到了一个描述器,那么将通过 desc.__get__(a, type(a)) 调用它。

点运算符的查找逻辑在 object.__getattribute__() 中。这里是一个等价的纯 Python 实现:

def object_getattribute(obj, name):
    "Emulate PyObject_GenericGetAttr() in Objects/object.c"
    null = object()
    objtype = type(obj)
    cls_var = getattr(objtype, name, null)
    descr_get = getattr(type(cls_var), '__get__', null)
    if descr_get is not null:
        if (hasattr(type(cls_var), '__set__')
            or hasattr(type(cls_var), '__delete__')):
            return descr_get(cls_var, obj, objtype)     # data descriptor
    if hasattr(obj, '__dict__') and name in vars(obj):
        return vars(obj)[name]                          # instance variable
    if descr_get is not null:
        return descr_get(cls_var, obj, objtype)         # non-data descriptor
    if cls_var is not null:
        return cls_var                                  # class variable
    raise AttributeError(name)

请注意,在 __getattribute__() 方法的代码中没有调用 __getattr__() 的钩子。这就是直接调用 __getattribute__() 或调用 super().__getattribute__ 会彻底绕过 __getattr__() 的原因。

相反,当 __getattribute__() 引发 AttributeError 时,点运算符和 getattr() 函数负责调用 __getattr__()。它们的逻辑封装在一个辅助函数中:

def getattr_hook(obj, name):
    "Emulate slot_tp_getattr_hook() in Objects/typeobject.c"
    try:
        return obj.__getattribute__(name)
    except AttributeError:
        if not hasattr(type(obj), '__getattr__'):
            raise
    return type(obj).__getattr__(obj, name)             # __getattr__

通过类调用

像A.x这样的点操作符查找的逻辑在 type.__getattribute__() 中。步骤与 object.__getattribute__() 相似,但是实例字典查找改为搜索类的 method resolution order。

如果找到了一个描述器,那么将通过 desc.__get__(None, A) 调用它。

完整的 C 实现可在 Objects/typeobject.c 中的 type_getattro() 和 _PyType_Lookup() 找到。

通过 super 调用

super 的点操作符查找的逻辑在 super() 返回的对象的 __getattribute__() 方法中。

类似 super(A, obj).m 形式的点分查找将在 obj.__class__.__mro__ 中搜索紧接在 A 之后的基类 B,然后返回 B.__dict__[‘m’].__get__(obj, A)。如果 m 不是描述器,则直接返回其值。

完整的 C 实现可以在 Objects/typeobject.c 的 super_getattro() 中找到。纯 Python 等价实现可以在 Guido’s Tutorial 中找到。

调用逻辑总结

描述器的机制嵌入在 object,type 和 super() 的 __getattribute__() 方法中。

要记住的重要点是:

  • 描述器由 __getattribute__() 方法调用。
  • 类从 object,type 或 super() 继承此机制。
  • 由于描述器的逻辑在 __getattribute__() 中,因而重写该方法会阻止描述器的自动调用。
  • object.__getattribute__() 和 type.__getattribute__() 会用不同的方式调用__get__()。前一个会传入实例,也可以包括类。后一个传入的实例为 None ,并且总是包括类。
  • 数据描述器始终会覆盖实例字典。
  • 非数据描述器会被实例字典覆盖。

2.3.3 自动名称通知

有时,描述器想知道它分配到的具体类变量名。创建新类时,元类 type 将扫描新类的字典。如果有描述器,并且它们定义了 __set_name__(),则使用两个参数调用该方法。owner 是使用描述器的类,name 是分配给描述器的类变量名。

实现的细节在 Objects/typeobject.c 中的 type_new() 和 set_names() 。

由于更新逻辑在 type.__new__() 中,因此通知仅在创建类时发生。之后如果将描述器添加到类中,则需要手动调用 __set_name__()

2.3.4 ORM (对象关系映射)示例

以下代码展示了如何使用数据描述器来实现简单 object relational mapping 框架。

其核心思路是将数据存储在外部数据库中,Python 实例仅持有数据库表中对应的的键。描述器负责对值进行查找或更新:

import pymysql


class Field:
    def __init__(self):
        self.conn = pymysql.connect(host="localhost",
                                    database="xiaoxu",
                                    user="root",
                                    password="123456",
                                    charset="utf8",
                                    port=3306)
        self.cursor = self.conn.cursor()

    def __set_name__(self, owner, name):
        self.fetch = f'SELECT {name} FROM {owner.table} WHERE {owner.key}=%s;'
        self.store = f'UPDATE {owner.table} SET {name}=%s WHERE {owner.key}=%s;'

    def __get__(self, obj, objtype=None):
        self.cursor.execute(self.fetch, [obj.value])
        return self.cursor.fetchone()[0]

    def __set__(self, obj, value):
        self.cursor.execute(self.store, [value, obj.key])
        self.conn.commit()


class People:
    table = "my_people"
    key = "id"
    id = Field()
    my_name = Field()
    my_age = Field()
    birthday = Field()

    def __init__(self, key, value):
        self.key = key
        self.value = value


p = People("id", "2")
print(p.id)
print(p.my_name)
print(p.my_age)
print(p.birthday)

表的数据如下所示:

在这里插入图片描述

数据为:

在这里插入图片描述

执行结果如下:

在这里插入图片描述

更新操作如下:

p.my_name = "小红来了"

执行结果无打印,重新查询数据:

在这里插入图片描述

重新查询数据,已更新成功:

在这里插入图片描述

再次执行如下:

p = People("id", "2")
print(p.id)
print(p.my_name)
print(p.my_age)
print(p.birthday)

可见结果(p.my_name))已经发生改变:

在这里插入图片描述

2.3.5 functools.cached_property使用分析

有了上述的多个栗子针对__set_name__方法的分析,我们再来具体分析下functools.cached_property的使用。

from functools import cached_property


class Xiaoxu:

    @cached_property
    def xiaoxu_names(self):
        print("调用缓存属性:xiaoxu_names", self)
        return "小徐"


x = Xiaoxu()
print(x.xiaoxu_names)
print(x.xiaoxu_names)
# 调用缓存属性:xiaoxu_names <__main__.Xiaoxu object at 0x01B1D880>
# 小徐
# 小徐

执行结果如下:

在这里插入图片描述

可以看到实现了缓存类的属性,而实际是定义的方法,但是执行时是获取的被修饰的实例方法的返回属性值,且具有缓存的效果。

下面分析cached_property的源码来看下如何实现缓存的效果的:

class cached_property:
    def __init__(self, func):
        self.func = func
        self.attrname = None
        self.__doc__ = func.__doc__
        self.lock = RLock()

    def __set_name__(self, owner, name):
        if self.attrname is None:
            self.attrname = name
        elif name != self.attrname:
            raise TypeError(
                "Cannot assign the same cached_property to two different names "
                f"({self.attrname!r} and {name!r})."
            )

    def __get__(self, instance, owner=None):
        if instance is None:
            return self
        if self.attrname is None:
            raise TypeError(
                "Cannot use cached_property instance without calling __set_name__ on it.")
        try:
            cache = instance.__dict__
        except AttributeError:  # not all objects have __dict__ (e.g. class defines slots)
            msg = (
                f"No '__dict__' attribute on {type(instance).__name__!r} "
                f"instance to cache {self.attrname!r} property."
            )
            raise TypeError(msg) from None
        val = cache.get(self.attrname, _NOT_FOUND)
        if val is _NOT_FOUND:
            with self.lock:
                # check if another thread filled cache while we awaited lock
                val = cache.get(self.attrname, _NOT_FOUND)
                if val is _NOT_FOUND:
                    val = self.func(instance)
                    try:
                        cache[self.attrname] = val
                    except TypeError:
                        msg = (
                            f"The '__dict__' attribute on {type(instance).__name__!r} instance "
                            f"does not support item assignment for caching {self.attrname!r} property."
                        )
                        raise TypeError(msg) from None
        return val

    __class_getitem__ = classmethod(GenericAlias)

cached_property类,定义了__set_name__方法,可以作为装饰器来使用,效果根据上述分析的,cached_property类装饰方法时,触发__set_name__方法,将方法的名称作为属性名self.attrname,然后核心是__get__方法,判断如果instance为None,也就是类.方法来调用时,直接返回self,如果instance不为None,也就是通过类实例.方法来调用缓存属性的,那么从类实例的__dict__中获取值(因为若类中定义了__slots__,但是__slots__里面没有定义__dict__属性,那么这种情况可能是没有__dict__的,所以源码判断了获取__dict__失败的场景)。

然后核心实现如下:

try:
    cache = instance.__dict__
except AttributeError:  # not all objects have __dict__ (e.g. class defines slots)
    msg = (
        f"No '__dict__' attribute on {type(instance).__name__!r} "
        f"instance to cache {self.attrname!r} property."
    )
    raise TypeError(msg) from None
val = cache.get(self.attrname, _NOT_FOUND)
if val is _NOT_FOUND:
    with self.lock:
        # check if another thread filled cache while we awaited lock
        val = cache.get(self.attrname, _NOT_FOUND)
        if val is _NOT_FOUND:
            val = self.func(instance)
            try:
                cache[self.attrname] = val
            except TypeError:
                msg = (
                    f"The '__dict__' attribute on {type(instance).__name__!r} instance "
                    f"does not support item assignment for caching {self.attrname!r} property."
                )
                raise TypeError(msg) from None
return val
  • 首先定义了_NOT_FOUND为object(),作为从类实例的__dict__中获取方法名的属性时,若值不存在,则返回定义的默认值,也就是object();上述执行的语句是val = cache.get(self.attrname, _NOT_FOUND);

  • 接下来判断if val is _NOT_FOUND,也就是实例对象的__dict__中还没有缓存该方法名属性值,如果判断成立的情况下,那么就通过with self.lock加锁,加锁后,再次调用cache.get(self.attrname, _NOT_FOUND)从类实例的__dict__中获取该缓存属性值,如果依然为空,那么就调用self.func(instance),也就是通过类实例对象来执行缓存属性的实例方法,由该实现也可知,缓存属性的实例方法不要有其他额外的参数;获取到需要缓存的值后,通过cache[self.attrname] = val将方法返回值缓存到该实例对象的__dict__中,key也就是属性值,即使用了@cached_property修饰的实例方法名称;这里的实现也提示了我们非常重要的一点,就是@cached_property的属性缓存,是基于实例对象来缓存的,如果你重新new,也就是重新定义了一个类实例对象,那么该实例的属性值又需要重新缓存了,这里需要特别注意。

  • 另外特殊说明下,python这里的加锁的判断方式,也就是我们熟知的双重检测锁的使用方式。先从缓存中获取数据并判断数据是否为空,为空的情况下,先加锁,然后再次从缓存中获取数据并判断数据是否为空,如果依然为空,那么就执行数据的生成,并塞入缓存中,如果缓存中第一次或者第二次判断不成立,那么说明缓存中存在该值,直接返回该缓存值即可。这种加锁的前后各有一次获取值并判断的形式,就是双重检测锁。好处是,加锁前的为空判断,可以避免一些不必要的加锁情况,最外面的if判断不成立的情况下,无需加锁,直接返回结果,提升了代码性能;若第一次判断成立,获取到锁之后,第二次还要进行为空判断的目的是,避免在多线程的情况下,假设A、B两个线程都同时进行了第一次判断,同时判断均为空,在判断结果均成立时,如果加锁后没有第二次的判断,那么假设A线程首先抢到锁,进入同步代码块,而B则进入阻塞队列或者说形如乐观锁中B线程在自旋等待锁的释放(比如Java中synchronized的锁升级,升级为轻量级锁时就是使用CAS的自旋锁,也就是乐观锁);当A进行缓存值的获取和设置后,成功释放锁,另外的线程B在锁释放时抢到了锁,因为没有第二次判断,所以立马再次执行了一次缓存值的获取和设置,若我们需要使用这种加锁模式实现单例模式,那么很明显多次的赋值和我们的预期单例是不一样的(缓存值的设置也应当采用单例模式,因为不需要对重复的key设置缓存),所以需要使用双重检测锁来进行过单例模式的实现。反之,若加锁后存在第二次判断,则线程B获取到锁时,首先判断缓存值已经存在,自然就不需要重复的进行缓存值的获取和设置了。

  • 当然,题外话,一般我们在Java中通过双重检测锁实现单例模式时,对于单例的实例对象,我们还需要加上volatile关键字,目的是为了禁止Java中指令的重排序。避免指令重排导致的情况是,假设线程A因为指令重排,先执行变量的赋值,再执行变量的初始化(指令重排导致先赋值再初始化,理想的预期是先初始化引用对象,再赋值给左边的变量;指令重排序在没有改变单线程程序的执行结果的前提下,可以提高程序的执行性能,但是多线程中就可能存在问题),在线程A执行完变量的赋值,还没执行变量的初始化时,如果此时线程B进行为空判断,由于对象已经赋值,那么判断不为空,线程B此时可以自由访问该对象,然后该对象还未初始化,所以线程B访问时将会发生异常。所以参考Spring的源码getHandlerMappings,对于Map对象handlerMappings,使用双重检测锁时,也为handlerMappings添加了volatile关键字。

到此,cached_property类缓存属性的实现方式已经全部分析完毕,下面简单演示下上述提到的,缓存是针对实例对象本身的这个情况

from functools import cached_property


class Xiaoxu:

    @cached_property
    def xiaoxu_names(self):
        print("调用缓存属性:xiaoxu_names", self)
        return "小徐"


x = Xiaoxu()
print(x.xiaoxu_names)
print(x.xiaoxu_names)

y = Xiaoxu()
print(y.xiaoxu_names)

执行结果如下:

在这里插入图片描述

2.3.6 自定义__set_name__方法实现接口的post、get等请求装饰器

最后,以我们自定义的__set_name__方法,来举一个栗子,实现接口的post、get等请求装饰器,并结合pytest单测框架,来实现一个接口的请求,实现如下

文件目录格式如下:

在这里插入图片描述

记得先安装pytest框架:

pip3 install pytest==8.2.1

查看pytest框架的版本:

pytest --version

结果:

pytest 8.2.1

具体实现如下:

AbsHttpHelper.py:

import functools

from requestsCase.AbstHttpReq import *


# import inspect
# class _partialHttp:
#     __slots__ = "func", "args", "keywords", "__dict__", "__weakref__"
#
#     def __instancecheck__(self, instance):
#         return inspect.isclass(instance)
#
#     def __new__(cls, func, /, *args, **keywords):
#         if not callable(func):
#             raise TypeError("the first argument must be callable")
#
#         self = super(_partialHttp, cls).__new__(cls)
#         __self, *arg = args
#         # data_func:获取test单测返回的接口请求数据
#         self.data_func = __self.func
#         # self.func:这里指的是httpJsonPostRequest方法
#         print(f"\n【func】:{func}\n")
#         self.func = func
#         self.method = __self.attrName
#         self.args = args
#         self.keywords = keywords
#
#         self.__name__ = self.data_func.__name__
#         print(f"函数名:{self.__name__}")
#         return self
#
#     def __call__(self, /, *args, **keywords):
#         payloadOrFileOrParams = self.data_func(*args, **keywords)
#         if not isinstance(payloadOrFileOrParams, dict):
#             raise ValueError("request test method must return dict req data.")
#         keywords = {**self.keywords, **payloadOrFileOrParams}
#
#         if self.method.__eq__("get"):
#             if not payloadOrFileOrParams.__contains__("params"):
#                 raise ValueError(f"get请求缺少params参数:{payloadOrFileOrParams},请检查")
#         elif self.method.__eq__("post"):
#             if not payloadOrFileOrParams.__contains__("params"):
#                 raise ValueError(f"post请求缺少payload参数:{payloadOrFileOrParams},请检查")
#         elif self.method.__eq__("file"):
#             if not payloadOrFileOrParams.__contains__("params"):
#                 raise ValueError(f"file请求缺少files参数:{payloadOrFileOrParams},请检查")
#         else:
#             raise ValueError("不支持的请求场景!")
#
#         print("开始请求:")
#         ret = self.func(*self.args, **keywords)
#         print("结果:", ret)
#         return ret


def _partialHttp(httpFunc, __self, head, /, **kwargs):
    # data_func:获取test单测返回的接口请求数据
    data_func = __self.func
    method = __self.attrName
    argsHttp = (head,)

    # data_func是原本的单测对应的方法,这里使用单测方法的元信息如__name__\__doc__\__annotations__等等
    # 将单测方法的元信息,添加到闭包的part方法上
    @functools.wraps(data_func)
    def part(*args, **keywords):
        payloadOrFileOrParams = data_func(*args, **keywords)

        if payloadOrFileOrParams is None:
            payloadOrFileOrParams = {}

        if not isinstance(payloadOrFileOrParams, dict):
            raise ValueError("request test method must return dict req data.")

        if payloadOrFileOrParams == {}:
            if method.__eq__("get"):
                payloadOrFileOrParams["params"] = {}
            elif method.__eq__("post"):
                payloadOrFileOrParams["payload"] = {}
            elif method.__eq__("file"):
                payloadOrFileOrParams["files"] = {}
            else:
                raise ValueError("un support")
        elif method.__eq__("get"):
            if not payloadOrFileOrParams.__contains__("params"):
                raise ValueError(f"get请求缺少params参数:{payloadOrFileOrParams},请检查")
            payloadOrFileOrParams["payload"] = {}
        elif method.__eq__("post"):
            if not payloadOrFileOrParams.__contains__("payload"):
                raise ValueError(f"post请求缺少payload参数:{payloadOrFileOrParams},请检查")
        elif method.__eq__("file"):
            if not payloadOrFileOrParams.__contains__("files"):
                raise ValueError(f"file请求缺少files参数:{payloadOrFileOrParams},请检查")
            payloadOrFileOrParams["payload"] = {}
        else:
            raise ValueError("不支持的请求场景!")

        keywordsHttp = {**kwargs, **payloadOrFileOrParams}

        print(f"开始接口请求,【{argsHttp}】, 【{keywordsHttp}】.")
        ret = httpFunc(*argsHttp, **keywordsHttp)
        print("接口请求结束:", ret)
        return ret

    return part


class BindSelf(AbsHttpBase):
    def __init__(self, func):
        self.func = func
        self.attrName = None
        self.url = None

    def __set_name__(self, owner, name):
        if not self.attrName:
            self.attrName = name
        if self.attrName not in ["get", "post", "file"]:
            raise ValueError("request name must be get or post.")

    def __call__(self, url, /, *args, **kwargs):
        self.url = url
        self_ = self
        print(f"\n【url】:{url}\n【method】:{self_.attrName}")

        def __call__(__self, real_test_func, /, ):
            __self.func = real_test_func
            header = self.common_header_add_group_route("xiaoxu")
            # return _partialHttp(self_.httpJsonPostRequest, __self, head, method=self_.attrName,
            #                    url=self_.url)

            # pytest必须使用function,才能执行case,上面使用类不会执行方法
            return func_wrap(self_, __self, header, )

        from types import MethodType
        return MethodType(__call__, self_)


# BindSelf(get)("url")(test)
# test_case_func = BindSelf(get)("url")(test_case_func)(self, *arg, **kwaargs)
# 如果最终执行的不是function,pytest框架会忽略case执行,
# 所以我将上面的类改为了function的形式:_partialHttp()


def func_wrap(self_, __self, head, /, ):
    return _partialHttp(self_.httpJsonPostRequest, __self, head, method=self_.attrName,
                        url=self_.url)

AbsReqSupport.py:

from requestsCase.AbsHttpHelper import BindSelf


class AbstractReqSupport:
    """
    自定义的装饰器,用于修饰pytest该方法是get方法请求,还是post方法请求或者文件上传等请求
    """

    @BindSelf
    def get(self):
        pass

    @BindSelf
    def post(self):
        # self没有使用到
        pass

    @BindSelf
    def file(self):
        pass

AbstHttpReq.py:

import json
from abc import ABC, ABCMeta

from requestsCase.Requests import *

a_ = ABC
globals()["cookie"] = ""


class AbsHttpBase(metaclass=ABCMeta):

    def httpJsonPostRequest(self, head1, method, payload, url, files=None, params=None):
        print("请求参数:")
        print(json.dumps(payload, indent=2).encode("utf-8").decode("unicode-escape"))
        print("结果如下:")
        res = None
        if not files and payload and not params:
            res = send_method(method, url, data=payload, headers=head1)
        elif files and not payload and not params:
            res = send_method(method, url, files=files, headers=head1)
        elif params and not payload and not files:
            res = send_method(method, url, params=params, headers=head1)
        else:
            # raise ValueError("unsupported request way, please add it.")
            print("unsupported request way to do, please add it.")
        print(json.dumps(res, indent=2).encode("utf-8").decode("unicode-escape"))
        return res

    def common_header_add_group_route(self, token):
        cookie = globals()["cookie"]
        if not cookie:
            cookie = ""
        cookie = cookie.strip()
        user_agent = """Mozilla/5.0(Windows NT 10.0;Win64;x64)AppleWebKit/537.36(KHTML,like Gecko)Chrome/93.0.4577.63 Safari/537.36 """
        lists = cookie.split(" ")
        flag = False
        for i in lists:
            if i.strip().startswith("token"):
                flag = True
        if not flag:
            lists[0] = lists[0] + ";"
            lists.append("token=" + token)
        cookie_new = "".join(lists)
        header = dict()
        header["Cookie"] = cookie_new
        header["User-Agent"] = user_agent
        return header

Requests.py:

import jsonpath
import requests


def send_method(method, url, headers=None, params=None, data=None, files=None):
    if method.lower() == "post" and params is not None and data is None:
        raise ValueError("post请求传参是data,请检查数据参数")
    global response
    if params and not data:
        if isinstance(params, dict):
            response = requests.request(method, url, params=params, headers=headers)
            return response.json()
        else:
            print("params should be dict type!")
    elif not params:
        if files:
            response = requests.post(url, data, headers=headers, files=files)
        elif files and data:
            response = requests.request(method, url, json=data, headers=headers, files=files)
        elif not files and data:
            response = requests.request(method, url, json=data, headers=headers)
        else:
            response = requests.request(method, url, headers=headers)
        return response.json()
    else:
        print("request maybe is wrong.")


def get_key_value(data, pattern):
    if isinstance(data, dict):
        if pattern.startswith("$"):
            return jsonpath.jsonpath(data, pattern)
        else:
            print("pattern must start with '$'")
    else:
        print("use jsonpath, data must be dict type")

根据pytest框架,我自定义的单测类TestReqCaseDemo.py:

import warnings

import pytest

from requestsCase.AbsReqSupport import AbstractReqSupport

warnings.filterwarnings("ignore")

py = pytest


class TestCase:

    @AbstractReqSupport.get("http://localhost:8800/fruit/queryFruits")
    def test_request(self):
        params = {
            "fruitId": "1006"
        }
        data = dict()
        data["params"] = params
        return data

执行前,说明下这个接口是我本地Java项目实现的get请求接口,部分源码如下:

@Controller
@RequestMapping(value = "/fruit")
@Slf4j
public class FruitController {
    @Autowired
    QueryFruitService queryFruitService;

    @Autowired
    ProcessorImpl<QueryFruitRequest, QueryFruitsResults> processor;

    @ResponseBody
    @RequestMapping(value = "/fruitsBySup", method = RequestMethod.GET)
    public List<FruitVo> queryFruitsBySupplier() {
        return null;
    }

    @ResponseBody
    @RequestMapping(value = "/queryFruits", method = RequestMethod.GET)
    public CommResult<QueryFruitsResults> queryFruits(QueryFruitRequest queryFruitRequest) {
        Map<String, Object> params = Maps.newHashMap();

        return processor.process(queryFruitRequest, new ProcessorCallBack<QueryFruitRequest, QueryFruitsResults>() {
            @Override
            public void validate(QueryFruitRequest params1) throws CommExp {

            }

            @Override
            public QueryFruitsResults doProcess() throws CommExp {
                log.info(String.format("请求进来了,传入的参数是:%s", queryFruitRequest));
                List<FruitVo> fruitVos = queryFruitService.queryFruitsByCondition(queryFruitRequest);
                QueryFruitsResults queryFruitsResults = new QueryFruitsResults();
                if (!CollectionUtils.isEmpty(fruitVos)) {
                    queryFruitsResults.setFruitVoList(fruitVos);
                }
                return queryFruitsResults;
            }
        });
    }
}

服务请求:

在这里插入图片描述

原本使用postman请求方式如下:

在这里插入图片描述

这里我们使用pytest结合实现的get请求装饰器,来发起接口请求:

对单测py文件,open in terminal并执行如下命令:

pytest TestReqCaseDemo.py -k "request" -s

执行结果如下:

collecting ... 
【url】:http://localhost:8800/fruit/queryFruits
【method】:get
collected 1 item                                                                                                                       

TestReqCaseDemo.py 开始接口请求,({'Cookie': ';token=xiaoxu', 'User-Agent': 'Mozilla/5.0(Windows NT 10.0;Win64;x64)AppleWebKit/537.36(K
HTML,like Gecko)Chrome/93.0.4577.63 Safari/537.36 '},)】, 【{'method': 'get', 'url': 'http://localhost:8800/fruit/queryFruits', 'params'
: {'fruitId': '1006'}, 'payload': {}}.
请求参数:
{}
结果如下:
{
  "success": true,
  "resData": {
    "fruitVoList": [
      {
        "createTime": "22-7-4 上午2:04",
        "modifyTime": "22-7-4 上午2:04",
        "extraInformation": null,
        "fruitNumber": 1006,
        "fruitName": "黑布林",
        "unitPrice": "6.98",
        "crossOutPrice": "8.98",
        "unitWeight": 500,
        "supplierId": 877,
        "fruitStockCount": 600,
        "fruitSaleCount": 112
      }
    ]
  },
  "code": "Fruit_Mall_952700200",
  "msg": "Success",
  "errorDesc": null
}
接口请求结束: {'success': True, 'resData': {'fruitVoList': [{'createTime': '22-7-4 上午2:04', 'modifyTime': '22-7-4 上午2:04', 'extraInf
ormation': None, 'fruitNumber': 1006, 'fruitName': '黑布林', 'unitPrice': '6.98', 'crossOutPrice': '8.98', 'unitWeight': 500, 'supplierI
d': 877, 'fruitStockCount': 600, 'fruitSaleCount': 112}]}, 'code': 'Fruit_Mall_952700200', 'msg': 'Success', 'errorDesc': None}

可见,单测执行的接口get请求效果,和postman的接口请求是一致的,且编码的形式更为灵活强大。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/643927.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【408精华知识】指令的寻址方式

文章目录 一、指令寻址&#xff08;一&#xff09;顺序寻址&#xff08;二&#xff09;跳跃寻址 二、数据寻址&#xff08;一&#xff09;隐含寻址&#xff08;二&#xff09;立即&#xff08;数&#xff09;寻址&#xff08;三&#xff09;直接寻址&#xff08;四&#xff09;…

在ubuntu22.04里网站源码连不上mysql数据库

在ubuntu22.04里网站源码连不上mysql数据库。后来找到了原因。 连不上的时候有报错信息&#xff1a; ERROR 1698 (28000): Access denied for user rootlocalhost 用在网上搜索该报错信息&#xff0c;找到了两篇有用的文章&#xff0c;用这两篇文章里的处理方法解决了问题。 …

HCIP-Datacom-ARST自选题库__ISIS简答【3道题】

1.IS-1S是链路状态路由协议&#xff0c;便用SPF算法进行路由计算。某园区同时部署了IPv4和IPV6井运行IS-IS实现网络的互联互通&#xff0c;如图所示&#xff0c;该网络IPv4和IPV6开销相同&#xff0c;R1和R4只支持IPV4。缺省情况下&#xff0c;计算形成的IPv6最短路径树中&…

深入理解 Mysql 分层架构:从存储引擎到查询优化器的内部机制解析

一、基础架构 1.连接器 1.会先连接到这个数据库上&#xff0c;这时候接待你的就是连接器。连接器负责跟客户端建立连接、获取权限、维持和管理连接 2.用户密码连接成功之后&#xff0c;会从权限表中拿出你的权限&#xff0c;后续操作权限都依赖于此时拿出的权限,这就意味着当链…

vue 表单些某项 v-if 控制后,想在显示时添加验证

效果: 可以为<el-form-item>添加 key 然后prop正常写就行 (key需要唯一值) <el-form-item label"设置" v-if"advanced_setting" key"threshold" prop"threshold"><el-inputv-model"form_Warning.threshold"p…

亲测使用frp获得访问者真实ip

怎么访问都只有127.0.0.1这个内网ip,获取不到访问者的真实ip 1.打开frp的配置文件(一般是frpc.toml&#xff0c;无需设置frps.toml) 在每一个tcp协议中添加 transport.proxyProtocolVersion "v2" 实例&#xff1a; # frpc.toml [[proxies]] name "web" …

Quartus 联合 ModelSim 仿真 IP 核(RAM)

文章目录 ModelSim 路径设置创建 RAM进行仿真 本文主要介绍如何在包含 IP 核的 Quartus 项目中使用 Modelsim 进行仿真&#xff0c;本文基于 IP 核 RAM: 2-PORT&#xff0c;其他 IP 核类似。 ModelSim 路径设置 点击 Tools->Options 点击 EDA Tool Options&#xff0c;设置…

Baidu Comate For Xcode 你的AI编程助手

前言 Baidu Comate 基于文心大模型&#xff0c;结合百度编程大数据&#xff0c;为你生成优质编程代码 你的AI编程助手&#xff0c;你的编码效率提升好帮手 Baidu Comate 释放“十倍”软件生产力 一、Xcode 安装配置 Baidu Comate 安装 已安装Xcode的情况下&#xff0c;下载B…

Flask实现文件上传/下载【基础版】

目录 前言 一.文件上传 1.1一些<input>相关上传属性 1.1.1multiple 1.1.2accept 1.2Flask后台接收文件提交 1.3Flask后台接收多个文件 二.保护文件上传 2.1限制文件上传大小 2.2验证文件名 2.3验证文件内容 三.文件下载 3.1使用send_file()方法下载文件 前言…

java如何获取IP和IP的归属地?

在Java中&#xff0c;获取IP地址通常指的是获取本地机器的IP地址或者通过某种方式&#xff08;如HTTP请求&#xff09;获取的远程IP地址。代码案例如下: 而要获取IP的归属地&#xff08;地理位置信息&#xff09;&#xff0c;则通常需要使用第三方IP地址查询服务&#xff0c;我…

Advanced Installer 问题集锦

1、界面在主题中显示的图标&#xff0c;如logo、发布者名称、产品名称就算在设计界面时删除&#xff0c;但是下次打开工程依然存在 解决办法&#xff1a;“可见”属性设置为禁用 2、在不关闭软件的情况下&#xff0c;使用"文件->打开"来切换项目&#xff0c;再次…

C++的数据结构(十二):图

在计算机科学中&#xff0c;图是一种非线性数据结构&#xff0c;它表示对象之间的关系&#xff0c;例如通信网络的连接、社交网络中人与人之间的联系&#xff0c;或者是地图上的路径和地标。 图由顶点&#xff08;或称为节点&#xff09;和边组成。边连接着两个顶点&#xff0c…

AI代码生成,真实工程与展望

某AI的代码生成&#xff0c;比另外某ai&#xff0c;略好一丢丢&#xff0c;比实际工程代码呢&#xff0c;差点细节。 所以&#xff0c;这注定是一个过渡的时代&#xff0c;一代过渡的人。 因为更庞大的上下文和知识体系&#xff0c;AI更有能力负责架构&#xff0c;而人类需要…

【openlayers系统学习】1.6下载要素,将要素数据序列化为 GeoJSON并下载

六、下载要素 下载要素 上传数据并编辑后&#xff0c;我们想让用户下载结果。为此&#xff0c;我们将要素数据序列化为 GeoJSON&#xff0c;并创建一个带有 download​ 属性的 <a>​ 元素&#xff0c;该属性会触发浏览器的文件保存对话框。同时&#xff0c;我们将在地图…

【WEEK13】 【DAY4】Shiro Part 4【English Version】

2024.5.23 Thursday Continued from 【WEEK13】 【DAY3】Shiro Part 3【English Version】 Table of Contents 15.6. Integrate Shiro with MyBatis15.6.1. Modify pom.xml15.6.2. Create application.yaml15.6.3. Connect to the database15.6.4. Modify application.propert…

C++:List的使用和模拟实现

✨✨✨学习的道路很枯燥&#xff0c;希望我们能并肩走下来! 文章目录 目录 文章目录 前言 一 list的介绍及使用 1.1 list的介绍 1.2 list的使用 1.2.1 list的构造 1.2.2 list iterator的使用 1.2.3 list capacity 1.2.4 list element access 1.2.5 list modifiers …

智能家居6 -- 配置 ini文件优化设备添加

不知道什么是ini的朋友可以先看这篇:一文带你入门ini格式-CSDN博客 准备 如下图: 在src 下面添加 ini.c 在inc 下面添加 ini.h 在 receive_interface.c 里面包含头文件&#xff0c;把之前添加的设备类注释掉 这时候就可以把相关设备的(.c .h)文件给删掉了 如下图: 修改/添…

2024上海初中生古诗文大会倒计时4个月:单选题真题解析(持续)

现在距离2024年初中生古诗文大会还有4个多月时间&#xff0c;我们继续来看10道选择题真题和详细解析&#xff0c;以下题目截取自我独家制作的在线真题集&#xff0c;都是来自于历届真题&#xff0c;去重、合并后&#xff0c;每道题都有参考答案和解析。 为帮助孩子自测和练习&…

isscc2024 short course4 In-memory Computing Architectures

新兴的ML加速器方法&#xff1a;内存计算架构 1. 概述 内存计算&#xff08;In-memory Computing&#xff09;架构是一种新兴的机器学习加速器方法&#xff0c;通过将计算能力集成到存储器中&#xff0c;以减少数据移动的延迟和能耗&#xff0c;从而提高计算效率和性能。这种方…

PY32F003+RTL8710(AT) 实现获取天气情况

一、RTL8710主要AT指令 1、ATSR&#xff1a;模块重启 2、ATSE1&#xff1a;开启回显 3、ATPW1&#xff1a;station模式 4、ATPNssid,password,,&#xff1a;连接到AP 5、ATPK1&#xff1a;设置自动接收 6、ATPC0,v1.yiketianqi.com,80&#xff1a;与网站建立TCP连接 7、ATPT125…