簡體   English   中英

python 屬性 class 命名空間混淆

[英]python property class namespace confusion

關於對 fset/fget/fdel 函數的引用以及它們所在的名稱空間,我對屬性 class 的使用感到困惑。 行為會有所不同,具體取決於我是將屬性用作裝飾器還是助手 function。為什么 class 和實例名稱空間中的重復變量會影響一個示例而不影響另一個示例?

當使用屬性作為此處顯示的裝飾器時,我必須使用前導下划線隱藏__dict__中的 var 名稱,以防止搶占屬性函數。 如果不是,我會看到一個遞歸循環。

class setget():
    """Play with setters and getters"""
    @property
    def x(self):
        print('getting x')
        return self._x
    @x.setter
    def x(self, x):
        print('setting x')
        self._x = x
    @x.deleter
    def x(self):
        print('deleting x')
        del self._x

我可以將 _x 視為實例屬性,將 x 視為 class 屬性:

>>> sg = setget()
>>> sg.x = 1
setting x
>>> sg.__dict__
{'_x': 1}
pprint(setget.__dict__)
mappingproxy({'__dict__': <attribute '__dict__' of 'setget' objects>,
              '__doc__': 'Play with setters and getters',
              '__module__': '__main__',
              '__weakref__': <attribute '__weakref__' of 'setget' objects>,
              'x': <property object at 0x000001BF3A0C37C8>})
>>> 

下面是一個遞歸示例,如果省略了實例變量名稱下划線。 (此處未顯示代碼)這對我來說很有意義,因為實例屬性 x 不存在,因此我們進一步查看 class 屬性。

>>> sg = setget()
>>> sg.x = 1
setting x
setting x
setting x
setting x
...

但是,如果我使用屬性作為助手 function,如此處的一個答案中所述: python class 屬性與實例屬性,則不需要隱藏下划線的名稱,並且沒有沖突。

示例代碼的副本:

class PropertyHelperDemo:
    '''Demonstrates a property definition helper function'''
    def prop_helper(k: str, doc: str):
        print(f'Creating property instance {k}')
        def _get(self):
            print(f'getting {k}')
            return self.__dict__.__getitem__(k) # might use '_'+k, etc.
        def _set(self, v):
            print(f'setting {k}')
            self.__dict__.__setitem__(k, v)
        def _del(self):
            print(f'deleting {k}')
            self.__dict__.__delitem__(k)
        return property(_get, _set, _del, doc)

    X: float = prop_helper('X', doc="X is the best!")
    Y: float = prop_helper('Y', doc="Y do you ask?")
    Z: float = prop_helper('Z', doc="Z plane!")
    # etc...

    def __init__(self, X: float, Y: float, Z: float):
        #super(PropertyHelperDemo, self).__init__()  # not sure why this was here
        (self.X, self.Y, self.Z) = (X, Y, Z)

    # for read-only properties, the built-in technique remains sleek enough already
    @property
    def Total(self) -> float:
        return self.X + self.Y + self.Z

在這里,我驗證屬性 fset function 正在后續調用中執行。

>>> p = PropertyHelperDemo(1, 2, 3)
setting X
setting Y
setting Z
>>> p.X = 11
setting X
>>> p.X = 111
setting X
>>> p.__dict__
{'X': 111, 'Y': 2, 'Z': 3}
>>> pprint(PropertyHelperDemo.__dict__)
mappingproxy({'Total': <property object at 0x000002333A093F98>,
              'X': <property object at 0x000002333A088EF8>,
              'Y': <property object at 0x000002333A093408>,
              'Z': <property object at 0x000002333A093D18>,
              '__annotations__': {'X': <class 'float'>,
                                  'Y': <class 'float'>,
                                  'Z': <class 'float'>},
              '__dict__': <attribute '__dict__' of 'PropertyHelperDemo' objects>,
              '__doc__': 'Demonstrates a property definition helper function',
              '__init__': <function PropertyHelperDemo.__init__ at 0x000002333A0B3AF8>,
              '__module__': '__main__',
              '__weakref__': <attribute '__weakref__' of 'PropertyHelperDemo' objects>,
              'prop_helper': <function PropertyHelperDemo.prop_helper at 0x000002333A052F78>})
>>> 

我可以在兩個命名空間中看到 class 和具有重疊名稱 X、Y、Z 的實例屬性。 我的理解是命名空間搜索順序是從局部變量開始的,所以我不明白為什么要在這里執行屬性 fset function。

非常感謝任何指導。

我認為你在將_x解釋為“實例屬性”並將x解釋為“類屬性”時有點誤入歧途 - 事實上,兩者都只綁定到實例,除了任意定義的行為之外,兩者都沒有綁定到另一個由@property裝飾的方法。

它們都占用相同的名稱空間,這就是為什么盡管它們可能代表相同的數量,但由於擔心隱藏/混淆名稱空間,它們不能共享名稱。

名稱空間的問題與@property裝飾器的使用沒有直接關系。 您不必“隱藏”屬性名稱 - 您只需要確保屬性名稱與方法名稱不同,因為一旦應用了@property裝飾器,就可以像訪問@property裝飾的方法一樣訪問沒有典型方法調用簽名的任何其他屬性,包括()

這是一個與您提供的示例相鄰的示例,可能有助於澄清。 我在下面定義了一個 class, PositionVector ,它保存空間中一個點的xyz坐標。

在初始化 class 的實例時,我還創建了一個屬性length ,它根據 x、y 和 z 值計算向量的長度。 試試這個:

import numpy as np

class PositionVector:
    
    def __init__(self, x: float, y: float, z: float) -> None:
        self.x = x
        self.y = y
        self.z = z
        self.length = np.sqrt(x**2 + y**2 + z**2)
        
        
p1 = PositionVector(x = 10, y = 0, z = 0)

print (p1.length)
# Result -> 10.0

只是現在我想更改實例的y屬性。 我這樣做:

p1.y = 10.0
print (f"p1's 'y' value is {p1.y}")
# Result -> p1's 'y' value is 10.0

除了現在,如果我再次訪問向量的長度,我會得到錯誤的答案:

print (f"p1's length is {p1.length}")
# Result -> p1's length is 10.0

出現這種情況是因為length在任何給定時刻都取決於xyz的當前值,它永遠不會更新並保持一致。 我們可以通過重新定義我們的 class 來解決這個問題,所以 length 是一種每次用戶想要訪問它時都會不斷重新計算的方法,如下所示:

class PositionVector:
    
    def __init__(self, x: float, y: float, z: float) -> None:
        self.x = x
        self.y = y
        self.z = z
        
    def length(self):
        return np.sqrt(self.x**2 + self.y**2 + self.z**2)

現在,我有辦法通過調用實例的length()方法隨時獲取此 class 實例的正確長度:

p1 = PositionVector(x = 10, y = 0, z = 0)

print (f"p1's length is {p1.length()}")
# Result -> p1's length is 10.0

p1.y = 10.0
print (f"p1's 'y' value is {p1.y}")
# Result -> p1's 'y' value is 10.0

print (f"p1's length is {p1.length()}")
# Result -> p1's length is 14.142135623730951

這很好,除了兩個問題:

  1. 如果此 class 已被使用,返回並將length從屬性更改為方法將破壞向后兼容性,迫使使用此 class 的任何其他代碼需要修改才能像以前一樣工作。

  2. 盡管我確實希望每次調用它時都重新計算length ,但我希望能夠將其拾取並“處理”,就像它是實例的“屬性”,而不是實例的“行為”一樣。 因此,使用p1.length()而不是簡單地p1.length來獲取實例的長度感覺很不合常理。

我可以恢復向后兼容性,並允許通過將@property裝飾器應用於方法來像訪問任何其他屬性一樣訪問length 只需將@property添加到length()方法定義中,即可將其調用簽名 go 恢復為原始形式:

    @property
    def length(self):
        return np.sqrt(self.x**2 + self.y**2 + self.z**2)

p1 = PositionVector(x=10, y=0, z=0)

print(f"p1's length is {p1.length}")
# Result -> p1's length is 10.0

p1.y = 10.0
print(f"p1's 'y' value is {p1.y}")
# Result -> p1's 'y' value is 10.0

print(f"p1's length is {p1.length}")
# Result -> p1's length is 14.142135623730951

此時,沒有陰影或“帶下划線”的屬性名稱,我不需要它們 - 我可以正常訪問xyz ,並像訪問任何其他屬性一樣訪問length ,但我確信我隨時調用它,我得到最新的值,正確反映了xyz的當前值。 在此 state 中對 p1 調用dict會產生:

print(p1.__dict__)
# Result -> {'x': 10, 'y': 10.0, 'z': 0}

在某些用例中,您不僅要計算length ,還要將其值保存為實例的 static 屬性。 這是您可能想要創建一個屬性並讓它在每次計算時都保存length值的地方。 你會像這樣完成這個:

class PositionVector:
    def __init__(self, x: float, y: float, z: float) -> None:
        self.x = x
        self.y = y
        self.z = z
        self.placeholder_attribute_name = None

    @property
    def length(self):
        self.placeholder_attribute_name = np.sqrt(self.x**2 + self.y**2 + self.z**2)
        return self.placeholder_attribute_name

這樣做對 class 的先前功能沒有任何影響。它只是創建了一種靜態保存長度值的方法,與創建它的行為無關。

您不必特別為該屬性命名。 除了已經使用的任何其他名稱外,您可以隨意命名。 在上面的例子中,您不能將其命名為xyzlength ,因為所有這些都有其他含義。

然而,為了可讀性,做以下兩件事確實有意義,而且這是常見的做法:

  1. 明確表示不能直接使用此屬性。 在上面的例子中——你不希望有人通過調用p1.placeholder_attribute_name來獲取向量的長度,因為這不能保證產生正確的當前長度——他們應該改用p1.length 您使用普遍采用的 Python 約定表明此屬性不供公眾使用 - 前導下划線:
    class PositionVector:
        def __init__(self, x: float, y: float, z: float) -> None:
            self.x = x
            self.y = y
            self.z = z
            self._placeholder_attribute_name = None
    
        @property
        def length(self):
            self._placeholder_attribute_name = np.sqrt(self.x**2 + self.y**2 + self.z**2)
            return self._placeholder_attribute_name

  1. 使用屬性名稱向任何閱讀您的代碼的人傳達該屬性的實際含義。 如果該屬性旨在隱藏“ length ”屬性 - 將length放在某處而不是不太有用的placeholder_attribute_name將提高可讀性。 您可以通過將其命名為_length來指示此陰影length

總之:

  • 使用@property裝飾器不會強制您使用“公共”和“私有”屬性名稱——只有在除了使用@property裝飾方法計算您的屬性值之外,您還想保存該方法的值時,您才會這樣做返回綁定到 class 的每個實例的持久屬性。
  • 即使您確實選擇在公共中使用propertyname而在私有中使用_propertyname ,這也不是絕對規則,它只是為了提高可讀性而采用的約定。

感謝 Vin 對property進行了很好的詳細描述,但它並沒有真正回答我的問題 - 本來可以更清楚地措辭。 它顯示了我的困惑。

setget遞歸而不是PropertyHelperDemo的根本原因是setget中的屬性方法調用自身,而PropertyHelperDemo中的方法直接訪問實例__dict__ ,如下所示:

def _get(self):
        print(f'getting {k}')
        return self.__dict__.__getitem__(k)

現在這似乎很明顯。 很明顯,不會阻止沖突的property__dict__屬性名稱,並且解決順序是在__dict__條目之前查找屬性。

在其他實驗中,我發現可以通過在__dict__中創建同名條目來替換實例方法。 所以整體的解決順序仍然不太清楚(對我來說)。

另一個讓我感到困惑的來源是dir返回一個方法名稱列表加上__dict__條目和其他屬性,並且顯然消除了重復項。 來自文檔

如果 object 沒有提供dir (),function 會盡力從對象的dict屬性(如果已定義)和它的類型 object 中收集信息。結果列表不一定完整,並且當 object 有一個自定義getattr ()。

...如果 object 是一個類型或 class object,則該列表包含其屬性的名稱,並遞歸地包含其基屬性的名稱。

... 結果列表按字母順序排序。

有趣的是,屬性出現在 class __dict__中,但沒有出現在實例__dict__中。

另一種探索方法是使用inspect ,它不會消除重復項。

>>> p = PropertyHelperDemo(1, 2, 3)
setting X
setting Y
setting Z
>>> 
>>> import inspect
>>> pprint(inspect.getmembers(p))
getting X
getting Y
getting Z
getting X
getting Y
getting Z
[('Total', 6),
 ('X', 1),
 ('Y', 2),
 ('Z', 3),
 ('__annotations__',
  {'X': <class 'float'>, 'Y': <class 'float'>, 'Z': <class 'float'>}),
 ('__class__', <class '__main__.PropertyHelperDemo'>),
 ('__delattr__',
  <method-wrapper '__delattr__' of PropertyHelperDemo object at 0x00000181D14C6608>),
 ('__dict__', {'X': 1, 'Y': 2, 'Z': 3}),
 ('__dir__',
  <built-in method __dir__ of PropertyHelperDemo object at 0x00000181D14C6608>),
...
...
...
>>> pprint(inspect.getmembers(p, predicate=inspect.ismethod))
getting X
getting Y
getting Z
getting X
getting Y
getting Z
[('__init__',
  <bound method PropertyHelperDemo.__init__ of <__main__.PropertyHelperDemo object at 0x00000181D14C6608>>),
 ('prop_helper',
  <bound method PropertyHelperDemo.prop_helper of <__main__.PropertyHelperDemo object at 0x00000181D14C6608>>)]
>>> 

在第一個清單中,我們可以看到property方法以及__dict__屬性。 有趣的是(對我來說) property方法是由inspect執行的。 我們看到方法X, Y, Z執行了兩次,因為Total也調用了它們。 當我們篩選方法時,屬性X, Y, ZTotal未列出。

當然,只有當你想讓自己和其他人都發瘋時,才重復使用這樣的名字是個好主意。

足夠的 omphaloskepsis,是時候繼續前進了。

暫無
暫無

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

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