这将是一个冗长的答案,可能只会起到补充说明的作用...但你的问题让我深入研究了一下,所以我想分享我的发现(和痛苦)。
最终你可能会发现这个答案对你的实际问题没有帮助。事实上,我的结论是 - 我不会这样做。话虽如此,这个结论的背景可能会让你感到有趣,因为你正在寻找更多细节。
纠正一些误解
第一个答案,在大多数情况下是正确的,但并非总是如此。例如,考虑这个类:
class Foo:
def __init__(self):
self.name = 'Foo!'
@property
def inst_prop():
return f'Retrieving {self.name}'
self.inst_prop = inst_prop
inst_prop
是一个属性,但不可撤销地成为实例属性:
>>> Foo.inst_prop
Traceback (most recent call last):
File "<pyshell#60>", line 1, in <module>
Foo.inst_prop
AttributeError: type object 'Foo' has no attribute 'inst_prop'
>>> Foo().inst_prop
<property object at 0x032B93F0>
>>> Foo().inst_prop.fget()
'Retrieving Foo!'
这完全取决于您的
property
首先在哪里定义。如果您的
@property
在类“scope”(或真正的
namespace
)内定义,则它变成了类属性。在我的示例中,类本身在实例化之前不知道任何
inst_prop
。当然,在这里作为属性它并不是非常有用。
首先,让我们谈谈您关于继承解析的评论...
那么继承与这个问题有什么关系呢?以下文章稍微深入地探讨了这个主题,方法解析顺序与此有些相关,尽管它主要讨论的是继承的广度而不是深度。
结合我们的发现,考虑到以下设置:
@property
def some_prop(self):
return "Family property"
class Grandparent:
culture = some_prop
world_view = some_prop
class Parent(Grandparent):
world_view = "Parent's new world_view"
class Child(Parent):
def __init__(self):
try:
self.world_view = "Child's new world_view"
self.culture = "Child's new culture"
except AttributeError as exc:
print(exc)
self.__dict__['culture'] = "Child's desired new culture"
想象一下执行这些代码时会发生什么:
print("Instantiating Child class...")
c = Child()
print(f'c.__dict__ is: {c.__dict__}')
print(f'Child.__dict__ is: {Child.__dict__}')
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')
结果如下:
Instantiating Child class...
can't set attribute
c.__dict__ is: {'world_view': "Child's new world_view", 'culture': "Child's desired new culture"}
Child.__dict__ is: {'__module__': '__main__', '__init__': <function Child.__init__ at 0x0068ECD8>, '__doc__': None}
c.world_view is: Child's new world_view
Child.world_view is: Parent's new world_view
c.culture is: Family property
Child.culture is: <property object at 0x00694C00>
注意以下几点:
- 可以应用
self.world_view
,但无法应用self.culture
culture
不存在于Child.__dict__
中(即类的mappingproxy
,不要与实例__dict__
混淆)
- 尽管
culture
存在于c.__dict__
中,但没有被引用。
你可能已经猜到原因了——world_view
被Parent
类作为非属性覆盖,因此Child
也能覆盖它。而culture
是继承的,所以它只存在于Grandparent
的mappingproxy
中:
Grandparent.__dict__ is: {
'__module__': '__main__',
'culture': <property object at 0x00694C00>,
'world_view': <property object at 0x00694C00>,
...
}
实际上,如果您尝试删除
Parent.culture
:
>>> del Parent.culture
Traceback (most recent call last):
File "<pyshell#67>", line 1, in <module>
del Parent.culture
AttributeError: culture
你会注意到,即使是对于{{Parent}},它也不存在。因为该对象直接引用了{{Grandparent.culture}}。
那么,分辨率顺序怎么样?
我们有兴趣观察实际的分辨率顺序,让我们尝试移除Parent.world_view
:
del Parent.world_view
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
想知道结果是什么吗?
c.world_view is: Family property
Child.world_view is: <property object at 0x00694C00>
Great! Please let me know what text you would like me to translate and into which language.
Child.world_view = "Child's independent world_view"
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
del c.world_view
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
Child.world_view = property(lambda self: "Child's own property")
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
结果是:
c.world_view is: Child's new world_view
Child.world_view is: Child's independent world_view
c.world_view is: Child's independent world_view
Child.world_view is: Child's independent world_view
c.world_view is: Child's own property
Child.world_view is: <property object at 0x020071B0>
这很有趣,因为
c.world_view
被恢复为其实例属性,而
Child.world_view
是我们分配的属性。删除实例属性后,它将恢复为类属性。重新分配
Child.world_view
到属性后,我们立即失去对实例属性的访问。
因此,我们可以推断出以下解析顺序:
1.如果存在一个类属性,并且它是
property
,通过
getter
或
fget
检索其值(稍后会详细介绍)。从当前类到基类。
2.否则,如果存在实例属性,则检索实例属性值。
3.否则,检索非
property
类属性。从当前类到基类。
在这种情况下,让我们删除根
property
:
del Grandparent.culture
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')
这将会产生:
c.culture is: Child's desired new culture
Traceback (most recent call last):
File "<pyshell#74>", line 1, in <module>
print(f'Child.culture is: {Child.culture}')
AttributeError: type object 'Child' has no attribute 'culture'
Ta-dah!现在Child
拥有了自己的culture
,这是通过强制插入到c.__dict__
中实现的。Child.culture
当然不存在,因为它从未在Parent
或Child
类属性中定义,并且Grandparent
的已被删除。
这是我的问题的根本原因吗?
实际上,不是。你遇到的错误与我们在分配self.culture
时观察到的错误完全不同。但继承顺序为答案设置了背景 - 即property
本身。
除了先前提到的getter
方法外,property
还有一些巧妙的技巧。在这种情况下最相关的是setter
或fset
方法,它由self.culture = ...
行触发。由于您的property
没有实现任何setter
或fget
函数,Python不知道该怎么做,并抛出一个AttributeError
(即can't set attribute
)。
如果您实现了setter
方法:
@property
def some_prop(self):
return "Family property"
@some_prop.setter
def some_prop(self, val):
print(f"property setter is called!")
实例化Child
类时,您将得到:
Instantiating Child class...
property setter is called!
现在你不会再收到AttributeError
错误,而是实际调用了some_prop.setter
方法。这使你对对象有更多的控制...根据我们之前的发现,我们知道在属性到达之前需要重写类属性。这可以在基类中作为触发器实现。以下是一个新的示例:
class Grandparent:
@property
def culture(self):
return "Family property"
@culture.setter
def culture(self, val):
print('Fine, have your own culture')
type(self).culture = None
self.culture = val
class Parent(Grandparent):
pass
class Child(Parent):
def __init__(self):
self.culture = "I'm a millennial!"
c = Child()
print(c.culture)
这导致:
Fine, have your own culture
I'm a millennial!
TA-DAH!现在您可以覆盖继承属性的自己实例属性了!
那么,问题解决了吗?
...并没有。这种方法的问题是,现在你无法拥有一个适当的setter
方法。有些情况下,你确实想要在property
上设置值。但现在,每当你设置self.culture = ...
时,它将始终覆盖你在getter
中定义的任何函数(在这种情况下,真正的部分只是被@property
包装的部分)。你可以添加更细致的措施,但不管怎样,它都将涉及到更多的东西,而不仅仅是self.culture = ...
。例如:
class Grandparent:
@culture.setter
def culture(self, val):
if isinstance(val, tuple):
if val[1]:
print('Fine, have your own culture')
type(self).culture = None
self.culture = val[0]
else:
raise AttributeError("Oh no you don't")
class Child(Parent):
def __init__(self):
try:
self.culture = "I'm a Gen X!"
except AttributeError:
self.culture = "I'm a Boomer!", True
这比其他答案所述的类级别的
size = None
要复杂得多。
你也可以考虑编写自己的
描述器,以处理
__get__
和
__set__
或其他方法。 但是归根结底,当引用
self.culture
时,
__get__
将始终首先触发;而当引用
self.culture = ...
时,
__set__
将始终首先触发。 我尝试过了,无法避免这一点。
问题的核心,在我看来
我在这里看到的问题是 - 你不能两全其美。 property
的作用就像是一个带有方便访问方法(如getattr
或setattr
)的descriptor。如果你还想让这些方法实现其他目的,那么你只会自找麻烦。我可能会重新考虑一下方法:
- 我真的需要这个
property
吗?
- 一个方法能为我提供不同的服务吗?
- 如果我需要一个
property
,是否有任何理由需要覆盖它?
- 如果这些
property
不适用于子类,那么子类是否真的属于同一族群?
- 如果我确实需要覆盖任何/所有
property
,是否有一个单独的方法比简单地重新分配更好,因为重新分配可能会意外地使 property
失效?
对于第5点,我的方法是在基类中添加一个 overwrite_prop()
方法,覆盖当前类属性,以便 property
将不再被触发:
class Grandparent:
def overwrite_props(self):
type(self).size = None
type(self).len = None
class Child(Parent):
def __init__(self):
self.overwrite_props()
self.size = 5
self.len = 10
正如你所看到的,虽然还有点勉强,但至少比神秘的size = None
更明确。 话虽如此,最终我不会覆盖该属性,并会重新考虑我的设计。
如果你已经走到这一步了 - 感谢你与我一起走过这段旅程。 这是一个有趣的小练习。
len(self.elements)
的实现例子。这个非常具体的实现为该类的所有实例强加了一个约定,即它们都提供elements
属性,它是一个 可计数容器。然而,你的Square_Integers_Below
类似乎不会像这样行为(也许它会动态生成其成员),因此它必须定义自己的行为方式。 - a_guestlen(self.elements)
不适合数学集合的默认实现,因为存在“许多”甚至没有有限基数的集合。一般来说,如果子类不想使用其基类的行为,则需要在类级别上覆盖它。实例属性,顾名思义,是在实例级别上工作的,因此受类行为的控制。 - a_guest