子类化:是否可以使用常规属性覆盖属性



假设我们要创建一个类族,这些类是总体概念的不同实现或特化。假设某些派生属性有一个合理的默认实现。我们想把它放到一个基类中

class Math_Set_Base:
@property
def size(self):
return len(self.elements)

因此,在这个相当愚蠢的例子中,子类将自动计算其元素

class Concrete_Math_Set(Math_Set_Base):
def __init__(self,*elements):
self.elements = elements
Concrete_Math_Set(1,2,3).size
# 3

但是,如果子类不想使用此默认值怎么办?这不起作用:

import math
class Square_Integers_Below(Math_Set_Base):
def __init__(self,cap):
self.size = int(math.sqrt(cap))
Square_Integers_Below(7)
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
#   File "<stdin>", line 3, in __init__
# AttributeError: can't set attribute

我意识到有一些方法可以用属性覆盖属性,但我想避免这种情况。因为基类的目的是使其用户的生活尽可能轻松,而不是通过强加(从子类的狭隘角度)复杂和多余的访问方法来增加膨胀。

能做到吗?如果不是,下一个最佳解决方案是什么?

这将是一个冗长的答案,可能只是恭维......但是你的问题让我陷入了兔子洞,所以我也想分享我的发现(和痛苦)。

您最终可能会发现此答案对您的实际问题没有帮助。 事实上,我的结论是——我根本不会这样做。 话虽如此,这个结论的背景可能会让你有点高兴,因为你正在寻找更多细节。


解决一些误解

第一个答案虽然在大多数情况下是正确的,但并非总是如此。 例如,考虑以下类:

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>

注意如何:

  1. self.world_view能够应用,而self.culture失败
  2. cultureChild.__dict__中不存在(类的mappingproxy,不要与实例混淆__dict__)
  3. 即使culture存在于c.__dict__中,它也没有被引用。

你也许能猜到为什么 -world_view被类Parent覆盖为非属性,所以Child也能够覆盖它。 同时,由于culture是继承的,它只存在于Grandparentmappingproxy

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_viewproperty,尽管我们之前已经成功地分配了self.world_view!但是,如果我们像另一个答案一样,在班级层面强行改变world_view呢?如果我们删除它怎么办?如果我们将当前类属性分配为属性怎么办?

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重新分配给属性后,我们会立即失去对实例属性的访问权限。

因此,我们可以推测以下解析顺序

  1. 如果类属性存在并且它是一个property,则通过getterfget检索其值(稍后会详细介绍)。 当前类首先到基类最后一个。
  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'

哒!Child现在有了自己的culture,基于强行插入c.__dict__。 当然,Child.culture不存在,因为它从未在类属性中定义过ParentChild,并且Grandparent被删除了。


这是我问题的根本原因吗?

其实不然。 您得到的错误,我们在分配self.culture时仍在观察到,是完全不同的。 但是继承顺序为答案设定了背景——这就是property本身。

除了前面提到的getter方法外,property还有一些巧妙的技巧。 在这种情况下最相关的是setterfset方法,它由self.culture = ...行触发。 由于您的property没有实现任何setterfget函数,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!")
# 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!

哒!您现在可以在继承的属性上覆盖您自己的实例属性!


那么,问题解决了?

。没有。 这种方法的问题是,现在你不能有一个合适的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

它比其他答案更复杂,size = None在班级层面。

你也可以考虑编写自己的描述符来处理__get____set__,或者其他方法。 但归根结底,当引用self.culture时,__get__总是首先触发,当引用self.culture = ...时,__set__总是先触发。 就我所尝试的那样,无法绕过它。


问题的症结,IMO

我在这里看到的问题是 - 你不能吃你的蛋糕和它。property的意思就像一个描述符,可以从getattrsetattr等方法方便地访问。 如果你也希望这些方法达到不同的目的,你只是在自找麻烦。 我可能会重新考虑这种方法:

  1. 我真的需要为此property吗?
  2. 一种方法可以为我服务吗?
  3. 如果我需要一个property,有什么理由需要覆盖它吗?
  4. 如果这些property不适用,子类真的属于同一个家庭吗?
  5. 如果我确实需要覆盖任何/所有property,单独的方法是否比简单地重新分配更好,因为重新分配可能会意外地使property无效?

对于第 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更明确。 也就是说,最终,我根本不会覆盖该属性,并且会从根本上重新考虑我的设计。

如果你已经走到了这一步 - 谢谢你和我一起走过这段旅程。 这是一个有趣的小练习。

属性是一个数据描述符,它优先于具有相同名称的实例属性。您可以使用唯一的__get__()方法定义非数据描述符:实例属性优先于同名的非数据描述符,请参阅文档。这里的问题是下面定义的non_data_property仅用于计算目的(您无法定义 setter 或删除程序),但在您的示例中似乎是这种情况。

import math
class non_data_property:
def __init__(self, fget):
self.__doc__ = fget.__doc__
self.fget = fget
def __get__(self, obj, cls):
if obj is None:
return self
return self.fget(obj)
class Math_Set_Base:
@non_data_property
def size(self, *elements):
return len(self.elements)
class Concrete_Math_Set(Math_Set_Base):
def __init__(self, *elements):
self.elements = elements

class Square_Integers_Below(Math_Set_Base):
def __init__(self, cap):
self.size = int(math.sqrt(cap))
print(Concrete_Math_Set(1, 2, 3).size) # 3
print(Square_Integers_Below(1).size) # 1
print(Square_Integers_Below(4).size) # 2
print(Square_Integers_Below(9).size) # 3

但是,这假定您有权访问基类以进行此更改。

@property

是在类级别定义的。该文档详细介绍了它的工作原理,但只要说设置属性解析为调用特定方法就足够了。但是,管理此过程的property对象是使用类自己的定义定义的。也就是说,它被定义为类变量,但行为类似于实例变量。

这样做的一个结果是,您可以在类级别自由地重新分配它:

print(Math_Set_Base.size)
# <property object at 0x10776d6d0>
Math_Set_Base.size = 4
print(Math_Set_Base.size)
# 4

就像任何其他类级名称(例如方法)一样,您可以通过以不同的方式显式定义它来覆盖子类中它:

class Square_Integers_Below(Math_Set_Base):
# explicitly define size at the class level to be literally anything other than a @property
size = None
def __init__(self,cap):
self.size = int(math.sqrt(cap))
print(Square_Integers_Below(4).size)  # 2
print(Square_Integers_Below.size)     # None

当我们创建一个实际实例时,实例变量只是隐藏同名的类变量。property对象通常使用一些恶作剧来操作此过程(即应用 getter 和 setter),但是当类级名称未定义为属性时,不会发生任何特殊情况,因此它的行为就像您对任何其他变量的期望一样。

你根本不需要分配(size)。size是基类中的一个属性,因此可以在子类中重写该属性:

class Math_Set_Base:
@property
def size(self):
return len(self.elements)
# size = property(lambda self: self.elements)

class Square_Integers_Below(Math_Set_Base):
def __init__(self, cap):
self._cap = cap
@property
def size(self):
return int(math.sqrt(self._cap))
# size = property(lambda self: int(math.sqrt(self._cap)))

您可以通过预先计算平方根来(微)优化这一点:

class Square_Integers_Below(Math_Set_Base):
def __init__(self, cap):
self._size = int(math.sqrt(self._cap))
@property
def size(self):
return self._size

看起来您想在类中定义size

class Square_Integers_Below(Math_Set_Base):
size = None
def __init__(self, cap):
self.size = int(math.sqrt(cap))

另一种选择是将cap存储在类中,并使用定义为属性的size进行计算(这将覆盖基类的属性size)。

我建议添加一个这样的二传手:

class Math_Set_Base:
@property
def size(self):
try:
return self._size
except:
return len(self.elements)
@size.setter
def size(self, value):
self._size = value

这样,您可以覆盖默认的.size属性,如下所示:

class Concrete_Math_Set(Math_Set_Base):
def __init__(self,*elements):
self.elements = elements
class Square_Integers_Below(Math_Set_Base):
def __init__(self,cap):
self.size = int(math.sqrt(cap))
print(Concrete_Math_Set(1,2,3).size) # 3
print(Square_Integers_Below(7).size) # 2

你也可以做下一件事

class Math_Set_Base:
_size = None
def _size_call(self):
return len(self.elements)
@property
def size(self):
return  self._size if self._size is not None else self._size_call()
class Concrete_Math_Set(Math_Set_Base):
def __init__(self, *elements):
self.elements = elements

class Square_Integers_Below(Math_Set_Base):
def __init__(self, cap):
self._size = int(math.sqrt(cap))

我认为可以将基类的属性设置为派生类中派生类的另一个属性,然后在派生类中使用具有新值的基类的属性。

在初始代码中,基类中的名称size与派生类中的属性self.size之间存在某种冲突。如果我们将派生类中的名称self.size替换为self.length.这将输出:

3
<__main__.Square_Integers_Below object at 0x000001BCD56B6080>

然后,如果我们在整个程序的所有出现中将方法size的名称替换为length,这将导致相同的异常:

Traceback (most recent call last):
File "C:/Users/Maria/Downloads/so_1.2.py", line 24, in <module>
Square_Integers_Below(7)
File "C:/Users/Maria/Downloads/so_1.2.py", line 21, in __init__
self.length = int(math.sqrt(cap))
AttributeError: can't set attribute

固定代码,或者无论如何,以某种方式工作的版本,是保持代码完全相同,除了类Square_Integers_Below,它将方法size从基类设置为另一个值。

class Square_Integers_Below(Math_Set_Base):
def __init__(self,cap):
#Math_Set_Base.__init__(self)
self.length = int(math.sqrt(cap))
Math_Set_Base.size = self.length
def __repr__(self):
return str(self.size)

然后,当我们运行所有程序时,输出为:

3
2

我希望这在某种程度上有所帮助。

相关内容

  • 没有找到相关文章

最新更新