这将是一个漫长的回答,可能只会起到补充作用……但是您的问题让我骑着小兔子去了,所以我也想分享我的发现(和痛苦)。
您可能最终会发现此答案对您的实际问题没有帮助。实际上,我的结论是-我完全不会这样做。话虽如此,由于您正在寻找更多细节,因此得出此结论的背景可能会给您带来一些乐趣。
解决一些误解
第一个答案虽然在大多数情况下是正确的,但并非总是如此。例如,考虑此类:
class Foo:
def __init__(self):
self.name = 'Foo!'
@property
def inst_prop():
return f'Retrieving {self.name}'
self.inst_prop = inst_prop
inst_prop
,虽然是property
,但是实例属性的不可撤销的:
>>> 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
在“作用域”类(或者实际上是在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
是继承,它仅在存在mappingproxy
的Grandparent
:
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>
world_view
property
即使我们成功地成功分配了前者,它也恢复了祖父母的身份self.world_view
!但是,如果我们world_view
像其他答案一样在课堂上强行改变,该怎么办?如果我们删除它怎么办?如果我们将当前的class属性指定为属性怎么办?
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}')
结果是:
# Creating Child's own world view
c.world_view is: Child's new world_view
Child.world_view is: Child's independent world_view
# Deleting Child instance's world view
c.world_view is: Child's independent world_view
Child.world_view is: Child's independent world_view
# Changing Child's world view to the property
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
属性重新分配给属性后,我们立即失去了对instance属性的访问权限。
因此,我们可以推测以下解决顺序:
- 如果存在class属性,并且它是一个
property
,则通过getter
或fget
(稍后会对此进行详细介绍)来检索其值。当前课程第一至基础课程最后。
- 否则,如果存在实例属性,则检索实例属性值。
- 否则,检索非
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'
塔达! Child
现在有自己的culture
基于强行插入c.__dict__
。 Child.culture
当然不存在,因为它从未在Parent
或Child
class属性中定义,并且Grandparent
已将删除了。
这是我的问题的根本原因吗?
其实没有。分配时仍会观察到的您得到的错误self.culture
是完全不同的。但是继承顺序为答案提供了背景- property
本身就是答案。
除了前面提到的getter
方法外,property
袖子上还有一些巧妙的技巧。在这种情况下,最相关的是由行触发的setter
或fset
方法self.culture = ...
。由于您property
没有实现任何setter
或fget
功能,蟒蛇不知道做什么,并抛出一个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!")
# do something else...
实例化Child
该类时,您将获得:
Instantiating Child class...
property setter is called!
AttributeError
现在,您实际上是在调用some_prop.setter
方法,而不是接收。这使您可以更好地控制对象...根据我们先前的发现,我们知道在到达属性之前需要重写一个类属性。这可以在基类中作为触发器实现。这是一个新鲜的例子:
class Grandparent:
@property
def culture(self):
return "Family property"
# add a setter method
@culture.setter
def culture(self, val):
print('Fine, have your own culture')
# overwrite the child class attribute
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:
# Usual setter
self.culture = "I'm a Gen X!"
except AttributeError:
# Trigger the overwrite condition
self.culture = "I'm a Boomer!", True
它waaaaay超过对方的回答复杂,size = None
在一流水平。
您还可以考虑编写自己的描述符来代替处理__get__
and __set__
和其他方法。但是,归根结底,何时self.culture
引用,__get__
总是先被触发,而self.culture = ...
引用何时,__set__
总是先被触发。就我所尝试的而言,没有解决方法。
问题的症结,海事组织
我在这里看到的问题是-您不能也不能吃蛋糕。 property
就像描述符,可以方便地从getattr
或方法访问setattr
。如果您还希望这些方法达到不同的目的,那么您就在自找麻烦。我也许会重新考虑这种方法:
- 我真的需要
property
这个吗?
- 方法可以为我提供不同的服务吗?
- 如果需要
property
,是否有任何理由需要覆盖它?
- 如果这些子类
property
不适用,那么该子类真的属于同一家族吗?
- 如果我确实需要覆盖任何/所有
property
s,那么单独的方法比简单地重新分配对我有更好的帮助,因为重新分配可能会意外使property
s 无效?
对于第5点,我的overwrite_prop()
方法是在基类中有一个覆盖当前类属性的方法,以便property
不再触发该方法:
class Grandparent:
# ...
def overwrite_props(self):
# reassign class attributes
type(self).size = None
type(self).len = None
# other properties, if necessary
# ...
# Usage
class Child(Parent):
def __init__(self):
self.overwrite_props()
self.size = 5
self.len = 10
如您所见,尽管仍然有些作弊,但它至少比隐喻更明确size = None
。就是说,最终,我根本不会覆盖该属性,而会从根本上重新考虑我的设计。
如果您已经走了这么远-谢谢您与我同行。这是一个有趣的小运动。