[英]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
,它保存空間中一個點的x
、 y
和z
坐標。
在初始化 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
在任何給定時刻都取決於x
、 y
和z
的當前值,它永遠不會更新並保持一致。 我們可以通過重新定義我們的 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
這很好,除了兩個問題:
如果此 class 已被使用,返回並將length
從屬性更改為方法將破壞向后兼容性,迫使使用此 class 的任何其他代碼需要修改才能像以前一樣工作。
盡管我確實希望每次調用它時都重新計算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
此時,沒有陰影或“帶下划線”的屬性名稱,我不需要它們 - 我可以正常訪問x
、 y
和z
,並像訪問任何其他屬性一樣訪問length
,但我確信我隨時調用它,我得到最新的值,正確反映了x
、 y
和z
的當前值。 在此 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 的先前功能沒有任何影響。它只是創建了一種靜態保存長度值的方法,與創建它的行為無關。
您不必特別為該屬性命名。 除了已經使用的任何其他名稱外,您可以隨意命名。 在上面的例子中,您不能將其命名為x
、 y
、 z
或length
,因為所有這些都有其他含義。
然而,為了可讀性,做以下兩件事確實有意義,而且這是常見的做法:
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
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, Z
和Total
未列出。
當然,只有當你想讓自己和其他人都發瘋時,才重復使用這樣的名字是個好主意。
足夠的 omphaloskepsis,是時候繼續前進了。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.