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 = funcself.attrname = Noneself.__doc__ = func.__doc__self.lock = RLock()def __set_name__(self, owner, name):if self.attrname is None:self.attrname = nameelif 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 selfif 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 Noneval = cache.get(self.attrname, _NOT_FOUND)if val is _NOT_FOUND:with self.lock:# check if another thread filled cache while we awaited lockval = cache.get(self.attrname, _NOT_FOUND)if val is _NOT_FOUND:val = self.func(instance)try:cache[self.attrname] = valexcept 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 Nonereturn 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 = Nonedef set_name(self, value):print("set_name", value)self._name = valuedef __set_name__(self, owner, name):print("__set_name__", owner, name)self._name = namedef __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 = Nonedef set_name(self, value):print("set_name", value)self._name = valuedef __set_name__(self, owner, name):print("__set_name__", owner, name)self._name = namedef __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:passPerson.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 = 0x = type('Xiaoxu', (), dict(age=0))print(type(x))
print(x.__name__)
print(x.__bases__)
print(x.__dict__)print("*" * 10)
print(isinstance(x, Xiaoxu))
# Falseprint(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 = Noneprint("__init__ Ma", self._name)def __set_name__(self, owner, name):print("__set_name__ Ma", owner, name)self._name = nameclass Mb:def __new__(cls, *args, **kwargs):print("__new__ Mb")return super(cls, Mb).__new__(cls)def __init__(self):self._age = Noneprint("__init__ Mb", self._age)def __set_name__(self, owner, name):print("__set_name__ Mb", owner, name)self._age = nameclass 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 = Noneprint("__init__ Ma", self._name)def __set_name__(self, owner, name):print("__set_name__ Ma", owner, name)self._name = nameclass Mb:def __new__(cls, *args, **kwargs):print("__new__ Mb")return super(cls, Mb).__new__(cls)def __init__(self):self._age = Noneprint("__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 = Noneself.func = funcprint("__init__ Ma", self._name)def __set_name__(self, owner, name):print("__set_name__ Ma", owner, name)self._name = nameclass Mb:def __new__(cls, *args, **kwargs):print("__new__ Mb")return super(cls, Mb).__new__(cls)def __init__(self, func):self._age = Noneself.func = funcprint("__init__ Mb", self._age)def __set_name__(self, owner, name):print("__set_name__ Mb", owner, name)self._age = nameclass Owner:@Madef a(self):pass@Mbdef 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 logginglogging.basicConfig(level=logging.INFO)class LoggedAccess:def __set_name__(self, owner, name):self.public_name = nameself.private_name = '_' + namedef __get__(self, instance, owner):value = getattr(instance, self.private_name)logging.info('Xiaoxu Accessing %r giving %r', self.public_name, value)return valuedef __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 instancename = LoggedAccess()# Second descriptor instanceage = LoggedAccess()def __init__(self, name, age):self.name = name # Calls the first descriptorself.age = age # Calls the second descriptordef birthday(self):self.age += 1x = People("xiaoxu", 99)
# INFO:root:Xiaoxu Access Updating 'name' to 'xiaoxu'
# INFO:root:Xiaoxu Access Updating 'age' to 99print(x.name)
# INFO:root:Xiaoxu Accessing 'name' giving 'xiaoxu'
# xiaoxuprint(x.age)
# INFO:root:Xiaoxu Accessing 'age' giving 99
# 99l = 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 = "_" + nameself.original_name = namedef __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)@abstractmethoddef validate(self, original_name, value):pass
自定義驗證器需要從 Validator 繼承,并且必須提供 validate() 方法以根據需要測試各種約束。
自定義驗證器
這是三個實用的數據驗證工具:
- OneOf 驗證值是一組受約束的選項之一。
- Number 驗證值是否為 int 或 float。根據可選參數,它還可以驗證值在給定的最小值或最大值之間。
- String 驗證值是否為 str。根據可選參數,它可以驗證給定的最小或最大長度。它還可以驗證用戶定義的 predicate。
class OneOf(XValidator):def __init__(self, *options):# 將tuple元組形式的options轉換為setself.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 = minvalueself.maxvalue = maxvaluedef 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 = minsizeself.maxsize = maxsizeself.predicate = predicatedef 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 = nameself.kind = kindself.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, Anyclass Validation:def __init__(self, validation_function: Callable[[Any], bool], error_msg: str) -> None:print("Validation初始化被執行")self.validation_function = validation_function # 傳進來的是匿名函數self.error_msg = error_msgdef __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 = Noneself.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 selfreturn 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實例化對象)賦值為42client.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 descriptorif hasattr(obj, '__dict__') and name in vars(obj):return vars(obj)[name] # instance variableif descr_get is not null:return descr_get(cls_var, obj, objtype) # non-data descriptorif cls_var is not null:return cls_var # class variableraise 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__'):raisereturn 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 pymysqlclass 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 = keyself.value = valuep = 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_propertyclass Xiaoxu:@cached_propertydef 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 = funcself.attrname = Noneself.__doc__ = func.__doc__self.lock = RLock()def __set_name__(self, owner, name):if self.attrname is None:self.attrname = nameelif 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 selfif 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 Noneval = cache.get(self.attrname, _NOT_FOUND)if val is _NOT_FOUND:with self.lock:# check if another thread filled cache while we awaited lockval = cache.get(self.attrname, _NOT_FOUND)if val is _NOT_FOUND:val = self.func(instance)try:cache[self.attrname] = valexcept 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 Nonereturn 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 lockval = cache.get(self.attrname, _NOT_FOUND)if val is _NOT_FOUND:val = self.func(instance)try:cache[self.attrname] = valexcept 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_propertyclass Xiaoxu:@cached_propertydef 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 functoolsfrom 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 retdef _partialHttp(httpFunc, __self, head, /, **kwargs):# data_func:獲取test單測返回的接口請求數據data_func = __self.funcmethod = __self.attrNameargsHttp = (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 retreturn partclass BindSelf(AbsHttpBase):def __init__(self, func):self.func = funcself.attrName = Noneself.url = Nonedef __set_name__(self, owner, name):if not self.attrName:self.attrName = nameif self.attrName not in ["get", "post", "file"]:raise ValueError("request name must be get or post.")def __call__(self, url, /, *args, **kwargs):self.url = urlself_ = selfprint(f"\n【url】:{url}\n【method】:{self_.attrName}")def __call__(__self, real_test_func, /, ):__self.func = real_test_funcheader = 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 MethodTypereturn 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 BindSelfclass AbstractReqSupport:"""自定義的裝飾器,用于修飾pytest該方法是get方法請求,還是post方法請求或者文件上傳等請求"""@BindSelfdef get(self):pass@BindSelfdef post(self):# self沒有使用到pass@BindSelfdef file(self):pass
AbstHttpReq.py:
import json
from abc import ABC, ABCMetafrom 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 = Noneif 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 resdef 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 = Falsefor i in lists:if i.strip().startswith("token"):flag = Trueif not flag:lists[0] = lists[0] + ";"lists.append("token=" + token)cookie_new = "".join(lists)header = dict()header["Cookie"] = cookie_newheader["User-Agent"] = user_agentreturn header
Requests.py:
import jsonpath
import requestsdef 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 responseif 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 warningsimport pytestfrom requestsCase.AbsReqSupport import AbstractReqSupportwarnings.filterwarnings("ignore")py = pytestclass TestCase:@AbstractReqSupport.get("http://localhost:8800/fruit/queryFruits")def test_request(self):params = {"fruitId": "1006"}data = dict()data["params"] = paramsreturn data
執行前,說明下這個接口是我本地Java項目實現的get請求接口,部分源碼如下:
@Controller
@RequestMapping(value = "/fruit")
@Slf4j
public class FruitController {@AutowiredQueryFruitService queryFruitService;@AutowiredProcessorImpl<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>() {@Overridepublic void validate(QueryFruitRequest params1) throws CommExp {}@Overridepublic 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的接口請求是一致的,且編碼的形式更為靈活強大。