簡體   English   中英

Python 何時從實例 __dict__ 退回到類 __dict__?

[英]When does Python fall back onto class __dict__ from instance __dict__?

請看下面的片段:

class Foo:
    class_var = "hi"

foo = Foo()
assert foo.class_var is Foo.class_var
assert "class_var" in Foo.__dict__
assert "class_var" not in foo.__dict__

這里的所有斷言都通過了,盡管我不確定身份斷言通過是否令人驚訝。

Python 何時以及如何從實例__dict__ __dict__

據我所知,何時調用 foo.class_var 會發生以下步驟:

  • Python 開始在 foo 對象的命名空間中尋找 class_var。
  • 如果它找到它,它會返回它。
  • 但是在這種情況下,它沒有找到它,因此它查找 foo 的類型,即 Foo。
  • 它在 Foo 中找到它並返回它。

根據(已經提到) [Python.Docs]:數據模型重點是我的):

自定義類

自定義類類型通常由類定義創建(請參閱類定義部分)。 一個類有一個由字典對象實現的命名空間。 類屬性引用被翻譯成這個字典中的查找,例如, Cx被翻譯成C.__dict__["x"] (盡管有許多鈎子允許其他定位屬性的方法)。 如果在那里找不到屬性名稱,則在基類中繼續進行屬性搜索。

...

類實例

類實例是通過調用類對象創建的(見上文)。 類實例具有作為字典實現的命名空間,這是搜索屬性引用的第一個位置。 如果在那里找不到屬性,並且實例的類具有該名稱的屬性,則使用類 attributes 繼續搜索

...

調用描述符

...

屬性訪問的默認行為是從對象的字典中獲取、設置或刪除屬性。 例如, ax有一個查找鏈,從a.__dict__['x']開始,然后type(a).__dict__['x'] ,並繼續通過type(a)的基類,不包括元類。

但是,如果查找的值是定義描述符方法之一的對象,則 Python 可能會覆蓋默認行為並調用描述符方法 這在優先鏈中發生的位置取決於定義了哪些描述符方法以及如何調用它們。

在類定義內(但在初始化程序(或其他方法)之外)定義的屬性稱為類屬性,並且綁定到類本身而不是其實例 這就像來自C++Java靜態成員。 [Python.Docs]:復合語句 - 類定義狀態(重點仍然是我的):

程序員注:類定義中定義的變量是類屬性; 它們由實例共享。 實例屬性可以在self.name = value的方法中設置。 類和實例屬性都可以通過符號“ self.name ”訪問,並且當以這種方式訪問​​時,實例屬性會隱藏同名的類屬性 類屬性可以用作實例屬性的默認值,但在那里使用可變值可能會導致意外結果。描述符可用於創建具有不同實現細節的實例變量。

因此,屬性查找順序可以總結如下(按升序遍歷,找到屬性名稱時只需返回其值(因此忽略其余條目))。 (內置) __getattribute__方法執行的第一步:

  1. 描述符(如果有的話 - 請注意它們的存在也可以間接觸發(通過其他功能))

  2. 實例命名空間foo.__dict__

  3. 實例類命名空間Foo.__dict__

  4. 實例類基類命名空間( e.__dict__ for e in Foo.__mro__

  5. 自定義 __getattr__ 方法可能返回的任何內容

以上是通常發生的情況,因為Python具有高度可定制性,可以更改(例如__slots__ )。

對於確切的行為,您可以檢查源代碼( [GitHub]:python/cpython - (main) cpython/Objects ):

  • typeobject.c : type_getattro (可選: super_getattro , slot_tp_getattro )

  • object.c : _PyObject_GenericGetAttrWithDict

這是一個可以解決問題的示例(希望如此)。

代碼00.py

#!/usr/bin/env python

import sys
from pprint import pformat as pf


def print_dict(obj, header="", indent=0, filterfunc=lambda x, y: not x.startswith("__")):
    if not header:
        header = getattr(obj, "__name__", None)
    if header:
        print("{:}{:}.__dict__:".format("  " * indent, header))
    lines = pf({k: v for k, v in getattr(obj, "__dict__", {}).items() if filterfunc(k, v)}, sort_dicts=False).split("\n")
    for line in lines:
        print("{:}{:}".format("  " * (indent + 1), line))
    print()


class Descriptor:
    def __init__(self, name):
        self.name = name

    def __get__(self, instance, cls):
        print("{:s}.__get__".format(self.name))

    def __set__(self, instance, value):
        print("{:s}.__set__ - {:}".format(self.name, value))

    def __delete__(self, instance):
        print("{:s}.__delete__".format(self.name))


class Demo:
    cls_attr0 = 3.141593
    cls_attr1 = Descriptor("cls_attr1")

    '''
    def __getattribute__(self, name):
        print("__getattribute__:", self, name)
        return super().__getattribute__(name)
    '''

    '''
    def __getattr__(self, name):
        print("__getattr__:", self, name)
        return "something dummy"
    '''

    def __init__(self):
        self.inst_attr0 = 2.718282


def main(*argv):
    print("ORIGINAL")
    demos = [Demo() for _ in range(2)]
    demo0 = demos[0]
    demo1 = demos[1]
    print_dict(Demo)
    print_dict(demo0, header="demo0")
    print("\ndemo0 attrs:", demo0.cls_attr0, demo0.cls_attr1, demo0.inst_attr0)
    print_dict(demo1, header="\ndemo1")
    print("\ndemo1 attrs:", demo1.cls_attr0, demo1.cls_attr1, demo1.inst_attr0)

    print("\nALTER 1ST INSTANCE OBJECT")
    demo0.inst_attr0 = -3
    demo0.cls_attr0 = -5

    print_dict(Demo)
    print_dict(demo0, header="demo0")
    print("\ndemo0 attrs:", demo0.cls_attr0, demo0.cls_attr1, demo0.inst_attr0)
    print_dict(demo1, header="\ndemo1")
    print("\ndemo1 attrs:", demo1.cls_attr0, demo1.cls_attr1, demo1.inst_attr0)

    print("\nALTER CLASS")
    Demo.cls_attr0 = -7
    Demo.cls_attr1 = -9
    print_dict(Demo, header="Demo")
    print_dict(demo1, header="demo0")
    print("\ndemo0 attrs:", demo0.cls_attr0, demo0.cls_attr1, demo0.inst_attr0)
    print_dict(demo1, header="\ndemo1")
    print("\ndemo1 attrs:", demo1.cls_attr0, demo1.cls_attr1, demo1.inst_attr0)


if __name__ == "__main__":
    print("Python {:s} {:03d}bit on {:s}\n".format(" ".join(elem.strip() for elem in sys.version.split("\n")),
                                                   64 if sys.maxsize > 0x100000000 else 32, sys.platform))
    rc = main(*sys.argv[1:])
    print("\nDone.")
    sys.exit(rc)

輸出

 [cfati@CFATI-5510-0:e:\Work\Dev\StackOverflow\q072399556]> "e:\Work\Dev\VEnvs\py_pc064_03.09_test0\Scripts\python.exe" code00.py Python 3.9.9 (tags/v3.9.9:ccb0e6a, Nov 15 2021, 18:08:50) [MSC v.1929 64 bit (AMD64)] 064bit on win32 ORIGINAL Demo.__dict__: {'cls_attr0': 3.141593, 'cls_attr1': <__main__.Descriptor object at 0x00000171B0B24FD0>} demo0.__dict__: {'inst_attr0': 2.718282} cls_attr1.__get__ demo0 attrs: 3.141593 None 2.718282 demo1.__dict__: {'inst_attr0': 2.718282} cls_attr1.__get__ demo1 attrs: 3.141593 None 2.718282 ALTER 1ST INSTANCE OBJECT Demo.__dict__: {'cls_attr0': 3.141593, 'cls_attr1': <__main__.Descriptor object at 0x00000171B0B24FD0>} demo0.__dict__: {'inst_attr0': -3, 'cls_attr0': -5} cls_attr1.__get__ demo0 attrs: -5 None -3 demo1.__dict__: {'inst_attr0': 2.718282} cls_attr1.__get__ demo1 attrs: 3.141593 None 2.718282 ALTER CLASS Demo.__dict__: {'cls_attr0': -7, 'cls_attr1': -9} demo0.__dict__: {'inst_attr0': 2.718282} demo0 attrs: -5 -9 -3 demo1.__dict__: {'inst_attr0': 2.718282} demo1 attrs: -7 -9 2.718282 Done.

您應該知道“類屬性”和“實例屬性”之間的區別,在您的示例中,您有class_var這是類屬性,在第一個斷言語句中,它檢查class_var是否在Foo的實例中,它通過,因為fooFoo類型,讓我們在這里嘗試另一個更清晰的例子:

class Foo:
    var1 = "Hello"
    def __init__(self,var2):
        self.var2 = var2
foo = Foo("Hello")
assert foo.var1 is Foo.var1
assert "var1" in Foo.__dict__
assert "var2" in foo.__dict__
assert "var1" in foo.__dict__

注意,最后一個assert語句會引發錯誤,因為這里的var1是類屬性,而不是實例屬性。

我喜歡用一個例子來解釋它,因為人們喜歡他們自己的經歷。 帶有子類的示例

class A:
    x = 1
    w = -1


class B(A):
    y = 2
    w = -2

    def __init__(self):
        self.z = 3
        self.w = -3


def get_dict(obj):
    """Get a __dict__ only with normal user defined keys"""
    return {k: v for k, v in obj.__dict__.items() if not k.startswith('__')}


b = B()

您可以想象getattr(b, name)的實現簡化如下:

if name in b.__dict__:
    return b.__dict__[name]
elif name in B.__dict__:
    return B.__dict__[name]
elif name in A.__dict__:
    return A.__dict__[name]
else:
    raise AttributeError(f"'B' object has no attribute '{name}'")

探索實例的所有__dict__和所有類:

>>> get_dict(A)
{'x': 1, 'w': -1}
>>> get_dict(B)
{'y': 2, 'w': -2}
>>> get_dict(b)
{'z': 3, 'w': -3}

# change the values
>>> A.w = 10
>>> B.w = 20
>>> b.w = 30

>>> A.x, B.y, b.z
(1, 2, 3)
>>> A.w, B.w, b.w
(10, 20, 30)

>>> get_dict(A)
{'x': 1, 'w': 10}
>>> get_dict(B)
{'y': 2, 'w': 20}
>>> get_dict(b)
{'z': 3, 'w': 30}

CristiFat 的答案很准確,但對每個人來說可能都不夠簡單。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM