Following system colour scheme - Python 增強提案 Selected dark colour scheme - Python 增強提案 Selected light colour scheme - Python 增強提案

Python 增強提案

PEP 447 – 向元類新增 __getdescriptor__ 方法

作者:
Ronald Oussoren <ronaldoussoren at mac.com>
狀態:
推遲
型別:
標準跟蹤
建立日期:
2013年6月12日
釋出歷史:
2013年7月2日, 2013年7月15日, 2013年7月29日, 2015年7月22日

目錄

摘要

目前 object.__getattribute__super.__getattribute__ 在查詢屬性時,會檢視類 MRO 中類的 __dict__。此 PEP 向元類添加了一個可選的 __getdescriptor__ 方法,該方法取代了此行為,並提供了對屬性查詢的更多控制,尤其是在使用 super 物件時。

也就是說,_PyType_Lookupsuper.__getattribute__ 中的 MRO 遍歷迴圈從以下更改為

def lookup(mro_list, name):
    for cls in mro_list:
        if name in cls.__dict__:
            return cls.__dict__

    return NotFound

def lookup(mro_list, name):
    for cls in mro_list:
        try:
            return cls.__getdescriptor__(name)
        except AttributeError:
            pass

    return NotFound

__getdescriptor__ 的預設實現會查詢類字典

class type:
   def __getdescriptor__(cls, name):
       try:
           return cls.__dict__[name]
       except KeyError:
           raise AttributeError(name) from None

PEP 狀態

此 PEP 被推遲,直到有人有時間更新此 PEP 並推動它。

基本原理

目前無法影響 超類 如何查詢屬性(即 super.__getattribute__ 無條件地檢視類 __dict__),這對於可以按需增長新方法的動態類(例如動態代理類)可能會有問題。

__getdescriptor__ 方法使得即使使用 超類 查詢屬性,也可以動態新增屬性。

新方法也會影響 object.__getattribute__(以及 PyObject_GenericGetAttr),以保持一致性,並在一個地方實現類的動態屬性解析。

背景

super.__getattribute__ 的當前行為給作為其他(非 Python)類或型別的動態代理的類帶來了問題,其中一個例子是 PyObjC。PyObjC 為 Objective-C 執行時中的每個類建立一個 Python 類,並在使用時在 Objective-C 執行時中查詢方法。這對於正常訪問是正常的,但對於使用 super 物件的訪問則不行。因此,PyObjC 目前包含一個必須與其類一起使用的自定義 super,並且還完全重新實現了 PyObject_GenericGetAttr 以進行正常屬性訪問。

此 PEP 中的 API 使得可以刪除自定義 super 並簡化實現,因為自定義查詢行為可以新增到中心位置。

注意

PyObjC 無法預先計算類 __dict__ 的內容,因為 Objective-C 類可以在執行時增長新方法。此外,Objective-C 類往往包含大量方法,而大多數 Python 程式碼只會使用其中的一小部分,這使得預先計算不必要地昂貴。

超類屬性查詢鉤子

super.__getattribute__object.__getattribute__ (或 PyObject_GenericGetAttr,特別是 C 程式碼中的 _PyType_Lookup) 都遍歷物件的 MRO,並目前檢視類的 __dict__ 來查詢屬性。

透過此提案,兩種查詢方法不再檢視類 __dict__,而是呼叫特殊方法 __getdescriptor__,該方法是元類上定義的一個槽。該方法的預設實現會在類 __dict__ 中查詢名稱,這意味著除非元型別實際定義了新的特殊方法,否則屬性查詢不會改變。

旁註:Python中的屬性解析演算法

object.__getattribute__(或 CPython 實現中的 PyObject_GenericGetAttr)實現的屬性解析過程相當直接,但如果不閱讀 C 程式碼則不完全如此。

object.__getattribute__ 的當前 CPython 實現基本上等同於以下(偽)Python 程式碼(不包括一些內務管理和提速技巧)

def _PyType_Lookup(tp, name):
    mro = tp.mro()
    assert isinstance(mro, tuple)

    for base in mro:
       assert isinstance(base, type)

       # PEP 447 will change these lines:
       try:
           return base.__dict__[name]
       except KeyError:
           pass

    return None


class object:
    def __getattribute__(self, name):
        assert isinstance(name, str)

        tp = type(self)
        descr = _PyType_Lookup(tp, name)

        f = None
        if descr is not None:
            f = descr.__get__
            if f is not None and descr.__set__ is not None:
                # Data descriptor
                return f(descr, self, type(self))

        dict = self.__dict__
        if dict is not None:
            try:
                return self.__dict__[name]
            except KeyError:
                pass

        if f is not None:
            # Non-data descriptor
            return f(descr, self, type(self))

        if descr is not None:
            # Regular class attribute
            return descr

        raise AttributeError(name)


class super:
    def __getattribute__(self, name):
       assert isinstance(name, unicode)

       if name != '__class__':
           starttype = self.__self_type__
           mro = startype.mro()

           try:
               idx = mro.index(self.__thisclass__)

           except ValueError:
               pass

           else:
               for base in mro[idx+1:]:
                   # PEP 447 will change these lines:
                   try:
                       descr = base.__dict__[name]
                   except KeyError:
                       continue

                   f = descr.__get__
                   if f is not None:
                       return f(descr,
                           None if (self.__self__ is self.__self_type__) else self.__self__,
                           starttype)

                   else:
                       return descr

       return object.__getattribute__(self, name)

此 PEP 應該將從“# PEP 447”開始的行處的字典查詢更改為方法呼叫以執行實際查詢,從而使對正常屬性訪問和透過 super 代理 的訪問都能夠影響該查詢。

請注意,特定類已經可以透過實現自己的 __getattribute__ 槽(無論是否呼叫超類實現)來完全覆蓋預設行為。

在 Python 程式碼中

元型別可以定義一個方法 __getdescriptor__,該方法在屬性解析期間由 super.__getattribute__object.__getattribute 呼叫。

class MetaType(type):
    def __getdescriptor__(cls, name):
        try:
            return cls.__dict__[name]
        except KeyError:
            raise AttributeError(name) from None

__getdescriptor__ 方法的引數是類(即元型別的例項)和要查詢的屬性名稱。它應該返回屬性的值而不呼叫描述符,並且在找不到名稱時應該引發 AttributeError

type 類為 __getdescriptor__ 提供了預設實現,該實現會在類字典中查詢名稱。

使用示例

下面的程式碼實現了一個愚蠢的元類,它將屬性查詢重定向到名稱的大寫版本。

class UpperCaseAccess (type):
    def __getdescriptor__(cls, name):
        try:
            return cls.__dict__[name.upper()]
        except KeyError:
            raise AttributeError(name) from None

class SillyObject (metaclass=UpperCaseAccess):
    def m(self):
        return 42

    def M(self):
        return "fortytwo"

obj = SillyObject()
assert obj.m() == "fortytwo"

如本 PEP 前面所述,此功能更實際的用例是 __getdescriptor__ 方法,該方法根據屬性訪問動態填充類 __dict__,主要是在無法可靠地使類字典與其源保持同步時,例如因為用於填充 __dict__ 的源也是動態的,並且沒有可用於檢測該源更改的觸發器。

一個例子是 PyObjC 中的類橋:類橋是一個 Python 物件(類),它表示一個 Objective-C 類,並且概念上在 Objective-C 類中的每個 Objective-C 方法都有一個 Python 方法。與 Python 一樣,可以向 Objective-C 類新增新方法或替換現有方法,並且沒有可用於檢測此情況的回撥。

在 C 程式碼中

一個新的型別標誌 Py_TPFLAGS_GETDESCRIPTOR,其值為 (1UL << 11),表示新的槽已存在並要使用。

一個新的槽 tp_getdescriptor 被新增到 PyTypeObject 結構中,該槽對應於 type 上的 __getdescriptor__ 方法。

槽的原始定義如下

PyObject* (*getdescriptorfunc)(PyTypeObject* cls, PyObject* name);

此方法應在 _cls_ 的名稱空間中查詢 _name_,不檢視超類,並且不應呼叫描述符。如果找不到 _name_,該方法返回 NULL 而不設定異常,否則返回一個新的引用(而不是借用的引用)。

具有 tp_getdescriptor 槽的類必須將 Py_TPFLAGS_GETDESCRIPTOR 新增到 tp_flags,以指示必須使用新的槽。

直譯器對該鉤子的使用

元型別需要新方法,因此它在 type_ 上定義。 super.__getattribute__object.__getattribute__/PyObject_GenericGetAttr(透過 _PyType_Lookup)都在遍歷 MRO 時使用此 __getdescriptor__ 方法。

實現的其他更改

PyObject_GenericGetAttr 的更改將透過更改私有函式 _PyType_Lookup 來完成。它目前返回借用的引用,但當 __getdescriptor__ 方法存在時,它必須返回一個新的引用。因此,_PyType_Lookup 將被重新命名為 _PyType_LookupName,這將導致此私有 API 的所有樹外使用者出現編譯時錯誤。

出於同樣的原因,_PyType_LookupId 被重新命名為 _PyType_LookupId2。typeobject.c 中具有相同問題的其他一些函式沒有更新名稱,因為它們是該檔案私有的。

當類具有重寫 __getdescriptor__ 的元類時,Objects/typeobject.c 中的屬性查詢快取將被停用,因為對於此類類,使用快取可能無效。

此 PEP 對自省的影響

使用此 PEP 中引入的方法可能會影響具有使用自定義 __getdescriptor__ 方法的元類的類的自省。本節列出了這些更改。

以下列出的專案僅受自定義 __getdescriptor__ 方法的影響,object 的預設實現不會導致問題,因為它仍然只使用類 __dict__,並且不會導致 object.__getattribute__ 的可見行為發生可見更改。

  • dir 可能不會顯示所有屬性

    與自定義 __getattribute__ 方法一樣,當使用 __getdescriptor__() 方法動態解析屬性時,dir() 可能無法看到所有(例項)屬性。

    解決方案很簡單:如果類想全面支援內建的 dir() 函式,則使用 __getdescriptor__ 的類也應實現 __dir__()

  • inspect.getattr_static 可能不會顯示所有屬性

    函式 inspect.getattr_static 特意不呼叫 __getattribute__ 和描述符,以避免在使用此函式進行內省時呼叫使用者程式碼。__getdescriptor__ 方法也將被忽略,這是 inspect.getattr_static 的結果可能與 builtin.getattr 的結果不同的另一種方式。

  • inspect.getmembersinspect.classify_class_attrs

    這兩個函式都直接訪問 MRO 沿線的類的 __dict__,因此可能受自定義 __getdescriptor__ 方法的影響。

    具有自定義 __getdescriptor__ 方法的程式碼,如果希望與這些方法很好地配合,還需要確保當 Python 程式碼直接訪問 __dict__ 時,其設定是正確的。

    請注意,inspect.getmemberspydoc 使用,因此這可能會影響執行時文件自省。

  • 直接自省類 __dict__

    任何直接訪問類 __dict__ 進行自省的程式碼都可能受到自定義 __getdescriptor__ 方法的影響,請參閱上一項。

效能影響

警告:本節中的基準測試結果已過時,待我將補丁移植到當前主幹後將更新。我預計本節的結果不會發生重大變化。

微基準測試

Issue 18181 的附件之一(pep447-micro-bench.py)中包含一個微基準測試,專門測試屬性查詢的速度,包括直接查詢和透過 super 查詢。

請注意,當使用自定義 __getdescriptor__ 方法時,具有深層類層次結構的屬性查詢明顯變慢。這是因為當存在此方法時,CPython 的屬性查詢快取無法使用。

Pybench

下面的 pybench 輸出將此 PEP 的實現與常規原始碼樹進行了比較,兩者都基於變更集 a5681f50bae2,在空閒機器和執行 Centos 6.4 的 Core i7 處理器上執行。

儘管機器處於空閒狀態,但執行之間存在明顯差異,我看到“最小時間”差異在 -0.1% 到 +1.5% 之間變化,而“平均時間”差異也類似(但略小)。

-------------------------------------------------------------------------------
PYBENCH 2.1
-------------------------------------------------------------------------------
* using CPython 3.4.0a0 (default, Jul 29 2013, 13:01:34) [GCC 4.4.7 20120313 (Red Hat 4.4.7-3)]
* disabled garbage collection
* system check interval set to maximum: 2147483647
* using timer: time.perf_counter
* timer: resolution=1e-09, implementation=clock_gettime(CLOCK_MONOTONIC)

-------------------------------------------------------------------------------
Benchmark: pep447.pybench
-------------------------------------------------------------------------------

    Rounds: 10
    Warp:   10
    Timer:  time.perf_counter

    Machine Details:
       Platform ID:    Linux-2.6.32-358.114.1.openstack.el6.x86_64-x86_64-with-centos-6.4-Final
       Processor:      x86_64

    Python:
       Implementation: CPython
       Executable:     /tmp/default-pep447/bin/python3
       Version:        3.4.0a0
       Compiler:       GCC 4.4.7 20120313 (Red Hat 4.4.7-3)
       Bits:           64bit
       Build:          Jul 29 2013 14:09:12 (#default)
       Unicode:        UCS4


-------------------------------------------------------------------------------
Comparing with: default.pybench
-------------------------------------------------------------------------------

    Rounds: 10
    Warp:   10
    Timer:  time.perf_counter

    Machine Details:
       Platform ID:    Linux-2.6.32-358.114.1.openstack.el6.x86_64-x86_64-with-centos-6.4-Final
       Processor:      x86_64

    Python:
       Implementation: CPython
       Executable:     /tmp/default/bin/python3
       Version:        3.4.0a0
       Compiler:       GCC 4.4.7 20120313 (Red Hat 4.4.7-3)
       Bits:           64bit
       Build:          Jul 29 2013 13:01:34 (#default)
       Unicode:        UCS4


Test                             minimum run-time        average  run-time
                                 this    other   diff    this    other   diff
-------------------------------------------------------------------------------
          BuiltinFunctionCalls:    45ms    44ms   +1.3%    45ms    44ms   +1.3%
           BuiltinMethodLookup:    26ms    27ms   -2.4%    27ms    27ms   -2.2%
                 CompareFloats:    33ms    34ms   -0.7%    33ms    34ms   -1.1%
         CompareFloatsIntegers:    66ms    67ms   -0.9%    66ms    67ms   -0.8%
               CompareIntegers:    51ms    50ms   +0.9%    51ms    50ms   +0.8%
        CompareInternedStrings:    34ms    33ms   +0.4%    34ms    34ms   -0.4%
                  CompareLongs:    29ms    29ms   -0.1%    29ms    29ms   -0.0%
                CompareStrings:    43ms    44ms   -1.8%    44ms    44ms   -1.8%
    ComplexPythonFunctionCalls:    44ms    42ms   +3.9%    44ms    42ms   +4.1%
                 ConcatStrings:    33ms    33ms   -0.4%    33ms    33ms   -1.0%
               CreateInstances:    47ms    48ms   -2.9%    47ms    49ms   -3.4%
            CreateNewInstances:    35ms    36ms   -2.5%    36ms    36ms   -2.5%
       CreateStringsWithConcat:    69ms    70ms   -0.7%    69ms    70ms   -0.9%
                  DictCreation:    52ms    50ms   +3.1%    52ms    50ms   +3.0%
             DictWithFloatKeys:    40ms    44ms  -10.1%    43ms    45ms   -5.8%
           DictWithIntegerKeys:    32ms    36ms  -11.2%    35ms    37ms   -4.6%
            DictWithStringKeys:    29ms    34ms  -15.7%    35ms    40ms  -11.0%
                      ForLoops:    30ms    29ms   +2.2%    30ms    29ms   +2.2%
                    IfThenElse:    38ms    41ms   -6.7%    38ms    41ms   -6.9%
                   ListSlicing:    36ms    36ms   -0.7%    36ms    37ms   -1.3%
                NestedForLoops:    43ms    45ms   -3.1%    43ms    45ms   -3.2%
      NestedListComprehensions:    39ms    40ms   -1.7%    39ms    40ms   -2.1%
          NormalClassAttribute:    86ms    82ms   +5.1%    86ms    82ms   +5.0%
       NormalInstanceAttribute:    42ms    42ms   +0.3%    42ms    42ms   +0.0%
           PythonFunctionCalls:    39ms    38ms   +3.5%    39ms    38ms   +2.8%
             PythonMethodCalls:    51ms    49ms   +3.0%    51ms    50ms   +2.8%
                     Recursion:    67ms    68ms   -1.4%    67ms    68ms   -1.4%
                  SecondImport:    41ms    36ms  +12.5%    41ms    36ms  +12.6%
           SecondPackageImport:    45ms    40ms  +13.1%    45ms    40ms  +13.2%
         SecondSubmoduleImport:    92ms    95ms   -2.4%    95ms    98ms   -3.6%
       SimpleComplexArithmetic:    28ms    28ms   -0.1%    28ms    28ms   -0.2%
        SimpleDictManipulation:    57ms    57ms   -1.0%    57ms    58ms   -1.0%
         SimpleFloatArithmetic:    29ms    28ms   +4.7%    29ms    28ms   +4.9%
      SimpleIntFloatArithmetic:    37ms    41ms   -8.5%    37ms    41ms   -8.7%
       SimpleIntegerArithmetic:    37ms    41ms   -9.4%    37ms    42ms  -10.2%
      SimpleListComprehensions:    33ms    33ms   -1.9%    33ms    34ms   -2.9%
        SimpleListManipulation:    28ms    30ms   -4.3%    29ms    30ms   -4.1%
          SimpleLongArithmetic:    26ms    26ms   +0.5%    26ms    26ms   +0.5%
                    SmallLists:    40ms    40ms   +0.1%    40ms    40ms   +0.1%
                   SmallTuples:    46ms    47ms   -2.4%    46ms    48ms   -3.0%
         SpecialClassAttribute:   126ms   120ms   +4.7%   126ms   121ms   +4.4%
      SpecialInstanceAttribute:    42ms    42ms   +0.6%    42ms    42ms   +0.8%
                StringMappings:    94ms    91ms   +3.9%    94ms    91ms   +3.8%
              StringPredicates:    48ms    49ms   -1.7%    48ms    49ms   -2.1%
                 StringSlicing:    45ms    45ms   +1.4%    46ms    45ms   +1.5%
                     TryExcept:    23ms    22ms   +4.9%    23ms    22ms   +4.8%
                    TryFinally:    32ms    32ms   -0.1%    32ms    32ms   +0.1%
                TryRaiseExcept:    17ms    17ms   +0.9%    17ms    17ms   +0.5%
                  TupleSlicing:    49ms    48ms   +1.1%    49ms    49ms   +1.0%
                   WithFinally:    48ms    47ms   +2.3%    48ms    47ms   +2.4%
               WithRaiseExcept:    45ms    44ms   +0.8%    45ms    45ms   +0.5%
-------------------------------------------------------------------------------
Totals:                          2284ms  2287ms   -0.1%  2306ms  2308ms   -0.1%

(this=pep447.pybench, other=default.pybench)

基準測試套件(使用選項“-b 2n3”)的執行似乎也表明效能影響很小

Report on Linux fangorn.local 2.6.32-358.114.1.openstack.el6.x86_64 #1 SMP Wed Jul 3 02:11:25 EDT 2013 x86_64 x86_64
Total CPU cores: 8

### call_method_slots ###
Min: 0.304120 -> 0.282791: 1.08x faster
Avg: 0.304394 -> 0.282906: 1.08x faster
Significant (t=2329.92)
Stddev: 0.00016 -> 0.00004: 4.1814x smaller

### call_simple ###
Min: 0.249268 -> 0.221175: 1.13x faster
Avg: 0.249789 -> 0.221387: 1.13x faster
Significant (t=2770.11)
Stddev: 0.00012 -> 0.00013: 1.1101x larger

### django_v2 ###
Min: 0.632590 -> 0.601519: 1.05x faster
Avg: 0.635085 -> 0.602653: 1.05x faster
Significant (t=321.32)
Stddev: 0.00087 -> 0.00051: 1.6933x smaller

### fannkuch ###
Min: 1.033181 -> 0.999779: 1.03x faster
Avg: 1.036457 -> 1.001840: 1.03x faster
Significant (t=260.31)
Stddev: 0.00113 -> 0.00070: 1.6112x smaller

### go ###
Min: 0.526714 -> 0.544428: 1.03x slower
Avg: 0.529649 -> 0.547626: 1.03x slower
Significant (t=-93.32)
Stddev: 0.00136 -> 0.00136: 1.0028x smaller

### iterative_count ###
Min: 0.109748 -> 0.116513: 1.06x slower
Avg: 0.109816 -> 0.117202: 1.07x slower
Significant (t=-357.08)
Stddev: 0.00008 -> 0.00019: 2.3664x larger

### json_dump_v2 ###
Min: 2.554462 -> 2.609141: 1.02x slower
Avg: 2.564472 -> 2.620013: 1.02x slower
Significant (t=-76.93)
Stddev: 0.00538 -> 0.00481: 1.1194x smaller

### meteor_contest ###
Min: 0.196336 -> 0.191925: 1.02x faster
Avg: 0.196878 -> 0.192698: 1.02x faster
Significant (t=61.86)
Stddev: 0.00053 -> 0.00041: 1.2925x smaller

### nbody ###
Min: 0.228039 -> 0.235551: 1.03x slower
Avg: 0.228857 -> 0.236052: 1.03x slower
Significant (t=-54.15)
Stddev: 0.00130 -> 0.00029: 4.4810x smaller

### pathlib ###
Min: 0.108501 -> 0.105339: 1.03x faster
Avg: 0.109084 -> 0.105619: 1.03x faster
Significant (t=311.08)
Stddev: 0.00022 -> 0.00011: 1.9314x smaller

### regex_effbot ###
Min: 0.057905 -> 0.056447: 1.03x faster
Avg: 0.058055 -> 0.056760: 1.02x faster
Significant (t=79.22)
Stddev: 0.00006 -> 0.00015: 2.7741x larger

### silent_logging ###
Min: 0.070810 -> 0.072436: 1.02x slower
Avg: 0.070899 -> 0.072609: 1.02x slower
Significant (t=-191.59)
Stddev: 0.00004 -> 0.00008: 2.2640x larger

### spectral_norm ###
Min: 0.290255 -> 0.299286: 1.03x slower
Avg: 0.290335 -> 0.299541: 1.03x slower
Significant (t=-572.10)
Stddev: 0.00005 -> 0.00015: 2.8547x larger

### threaded_count ###
Min: 0.107215 -> 0.115206: 1.07x slower
Avg: 0.107488 -> 0.115996: 1.08x slower
Significant (t=-109.39)
Stddev: 0.00016 -> 0.00076: 4.8665x larger

The following not significant results are hidden, use -v to show them:
call_method, call_method_unknown, chaos, fastpickle, fastunpickle, float, formatted_logging, hexiom2, json_load, normal_startup, nqueens, pidigits, raytrace, regex_compile, regex_v8, richards, simple_logging, startup_nosite, telco, unpack_sequence.

替代提案

__getattribute_super__

此 PEP 的早期版本使用了類上的以下靜態方法

def __getattribute_super__(cls, name, object, owner): pass

此方法執行名稱查詢並呼叫描述符,並且必然僅限於與 super.__getattribute__ 一起使用。

複用 tp_getattro

最好避免新增新的槽,從而保持 API 更簡單易懂。Issue 18181 上的評論詢問是否可以重用 tp_getattro 槽,也就是說 super 可以呼叫 MRO 沿線所有方法的 tp_getattro 槽。

那行不通,因為 tp_getattro 會在嘗試使用 MRO 中的類解析屬性之前,先檢視例項 __dict__。這意味著使用 tp_getattro 而不是窺視類字典會改變 超類 的語義。

新方法的替代放置位置

此 PEP 提議將 __getdescriptor__ 作為元類上的方法新增。另一種選擇是將其作為類上的類方法新增(類似於 __new__ 是類的 靜態方法 而不是元類的方法)。

使用元類上的方法的優點是,當 MRO 上的兩個類具有不同的元類時,如果這些元類對 __getdescriptor__ 具有不同的行為,則會引發錯誤。如果使用普通的類方法,這個問題將不會被檢測到,但在執行程式碼時可能會導致細微的錯誤。

歷史

  • 2015年7月23日:與 Guido 討論後添加了型別標誌 Py_TPFLAGS_GETDESCRIPTOR

    新標誌主要用於避免在載入舊版 CPython 擴充套件時崩潰,也可能對速度有積極影響。

  • 2014年7月:將槽重新命名為 __getdescriptor__,舊名稱與其他槽的命名風格不匹配,且描述性不足。

討論串

參考資料


來源: https://github.com/python/peps/blob/main/peps/pep-0447.rst

最後修改: 2025-02-01 08:55:40 GMT