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

Pau*_*zer 22 python inheritance properties

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

class Math_Set_Base:
    @property
    def size(self):
        return len(self.elements)
Run Code Online (Sandbox Code Playgroud)

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

class Concrete_Math_Set(Math_Set_Base):
    def __init__(self,*elements):
        self.elements = elements

Concrete_Math_Set(1,2,3).size
# 3
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

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

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

r.o*_*ook 12

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

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


纠正一些误解

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

class Foo:
    def __init__(self):
        self.name = 'Foo!'
        @property
        def inst_prop():
            return f'Retrieving {self.name}'
        self.inst_prop = inst_prop
Run Code Online (Sandbox Code Playgroud)

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!'
Run Code Online (Sandbox Code Playgroud)

这完全取决于最初property的定义位置。如果 your@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"
Run Code Online (Sandbox Code Playgroud)

想象一下执行这些行时会发生什么:

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}')
Run Code Online (Sandbox Code Playgroud)

结果是:

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>
Run Code Online (Sandbox Code Playgroud)

注意如何:

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

您可能能够猜到原因 -world_viewParent类作为非属性覆盖,因此Child也能够覆盖它。同时,由于culture是继承的,因此它只存在于mappingproxyofGrandparent中:

Grandparent.__dict__ is: {
    '__module__': '__main__', 
    'culture': <property object at 0x00694C00>, 
    'world_view': <property object at 0x00694C00>, 
    ...
}
Run Code Online (Sandbox Code Playgroud)

事实上,如果您尝试删除Parent.culture

>>> del Parent.culture
Traceback (most recent call last):
  File "<pyshell#67>", line 1, in <module>
    del Parent.culture
AttributeError: culture
Run Code Online (Sandbox Code Playgroud)

您会注意到它甚至不存在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}')
Run Code Online (Sandbox Code Playgroud)

想知道结果是什么?

c.world_view is: Family property
Child.world_view is: <property object at 0x00694C00>
Run Code Online (Sandbox Code Playgroud)

它恢复到祖父母的world_view property,尽管我们已经成功地分配了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}')
Run Code Online (Sandbox Code Playgroud)

结果是:

# 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>
Run Code Online (Sandbox Code Playgroud)

这很有趣,因为c.world_view恢复到其实例属性,而Child.world_view是我们分配的属性。删除实例属性后,它恢复为类属性。将 重新分配Child.world_view给属性后,我们立即失去了对实例属性的访问权限。

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

  1. 如果类属性存在并且它是 a ,则通过orproperty检索其值(稍后会详细介绍)。当前班级排在第一位,基础班级排在最后。getterfget
  2. 否则,如果实例属性存在,则检索实例属性值。
  3. 否则,检索非property类属性。当前班级排在第一位,基础班级排在最后。

在这种情况下,让我们删除根property

del Grandparent.culture
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')
Run Code Online (Sandbox Code Playgroud)

这使:

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'
Run Code Online (Sandbox Code Playgroud)

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


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

事实上,没有。您遇到的错误(我们在分配时仍在观察)self.culture完全不同的。但继承顺序为答案设置了背景——答案就是答案property本身。

除了前面提到的getter方法之外,property还有一些巧妙的技巧。在这种情况下最相关的是setter, orfset方法,它由self.culture = ...line 触发。由于您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...
Run Code Online (Sandbox Code Playgroud)

实例化该类时,Child您将得到:

Instantiating Child class...
property setter is called!
Run Code Online (Sandbox Code Playgroud)

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)
Run Code Online (Sandbox Code Playgroud)

结果是:

Fine, have your own culture
I'm a millennial!
Run Code Online (Sandbox Code Playgroud)

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


那么,问题解决了吗?

... 并不真地。这种方法的问题是,现在你无法找到合适的setter方法。在某些情况下,您确实希望在property. 但现在,每当您设置时,它总是self.culture = ...会覆盖您在 中定义的任何函数(在本例中,实际上只是包装的部分。您可以添加更细微的措施,但无论怎样,它总是会涉及到的不仅仅是.例如:getter@propertyself.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
Run Code Online (Sandbox Code Playgroud)

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

您还可以考虑编写自己的描述符来处理__get__and__set__或其他方法。但归根结底,当self.culture被引用时,__get__总是会先被触发,当self.culture = ...被引用时,__set__总是会被先触发。据我所知,没有办法解决这个问题。


问题的关键,国际海事组织

我在这里看到的问题是——鱼与熊掌不可兼得。 property意思就像一个描述符getattr,可以通过或 等方法方便地访问setattr。如果你还希望这些方法达到不同的目的,那你就是自找麻烦。我也许会重新考虑这种方法:

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

对于第 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
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,虽然仍然有点做作,但它至少比神秘的size = None. 也就是说,最终我根本不会覆盖该属性,并且会从根本上重新考虑我的设计。

如果您已经走到这一步,感谢您与我一起走过这段旅程。这是一个有趣的小练习。

  • 哇,非常感谢!我需要一些时间来消化这一点,但我想这是我自找的。 (2认同)

Ark*_*lis 11

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

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
Run Code Online (Sandbox Code Playgroud)

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

  • 我并不是不同意这些可能有有效的用例,我只是想提一下警告,因为它会使将来的调试工作变得复杂。只要您和 OP 都意识到其中的含义,那么一切都很好。 (2认同)

Gre*_*Guy 8

A@property在类级别定义。该文档详细介绍了它的工作原理,但足以说明设置获取属性解析为调用特定方法。然而,property管理这个过程的对象是用类自己的定义来定义的。也就是说,它被定义为一个类变量,但表现得像一个实例变量。

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

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

Math_Set_Base.size = 4
print(Math_Set_Base.size)
# 4
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

当我们创建一个实际的实例时,实例变量只是掩盖了同名的类变量。该property对象通常使用一些诡计来操纵这个过程(即应用​​ getter 和 setter),但是当类级别名称未定义为属性时,不会发生任何特殊情况,因此它的行为与您对任何其他变量的期望一样。


che*_*ner 6

您根本不需要赋值 (to 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)))
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)