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


14

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

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

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

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

Answers:


6

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

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

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


这非常接近完美。因此,这意味着即使只定义getter也会隐式添加一个setter,而这个setter除了阻止分配之外什么也没做?有趣。我可能会接受这个答案。
Paul Panzer

是的,根据文档,属性总是定义所有三个描述的方法(__get__()__set__()__delete__()),他们抛出一个AttributeError,如果你不为他们提供任何功能。请参阅等效于属性实现的Python
Arkelis

只要你不关心setter__set__财产,这将工作。但是,我要提醒您,这也意味着您不仅可以在类级别(self.size = ...)甚至在实例级别(例如Concrete_Math_Set(1, 2, 3).size = 10,同样有效)也可以轻松覆盖该属性。思想的食物:)
r.ook

并且 Square_Integers_Below(9).size = 1也是有效的,因为它是一个简单的属性。在这种特殊的“大小”用例中,这似乎很笨拙(改为使用属性),但是在一般情况下“在子类中轻松覆盖计算的道具”,在某些情况下可能会很好。您也可以使用来控制属性访问,__setattr__()但可能会让人不知所措。
Arkelis,

1
我并不是不同意这些可能存在有效的用例,我只是想提一下警告,因为它可能会使将来的调试工作变得复杂。只要您和OP都知道其中的含义,那么一切就很好了。
r.ook

10

这将是一个漫长的回答,可能只会起到补充作用……但是您的问题让我骑着小兔子去了,所以我也想分享我的发现(和痛苦)。

您可能最终会发现此答案对您的实际问题没有帮助。实际上,我的结论是-我完全不会这样做。话虽如此,由于您正在寻找更多细节,因此得出此结论的背景可能会给您带来一些乐趣。


解决一些误解

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

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. culture不存在在Child.__dict__(的mappingproxy类的,不与该实例相混淆__dict__
  3. 即使culture存在于中c.__dict__,也未引用。

你也许能猜到为什么- world_view被覆盖Parent类作为非财产,所以Child能够覆盖它。同时,由于culture是继承,它仅在存在mappingproxyGrandparent

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属性的访问权限。

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

  1. 如果存在class属性,并且它是一个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当然不存在,因为它从未在ParentChildclass属性中定义,并且Grandparent已将删除了。


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

其实没有。分配时仍会观察到的您得到的错误self.culture完全不同的。但是继承顺序为答案提供了背景- property本身就是答案。

除了前面提到的getter方法外,property袖子上还有一些巧妙的技巧。在这种情况下,最相关的是由行触发的setterfset方法self.culture = ...。由于您property没有实现任何setterfget功能,蟒蛇不知道做什么,并抛出一个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。如果您还希望这些方法达到不同的目的,那么您就在自找麻烦。我也许会重新考虑这种方法:

  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

如您所见,尽管仍然有些作弊,但它至少比隐喻更明确size = None。就是说,最终,我根本不会覆盖该属性,而会从根本上重新考虑我的设计。

如果您已经走了这么远-谢谢您与我同行。这是一个有趣的小运动。


2
哇,非常感谢!我需要一些时间来消化它,但是我想我要了。
Paul Panzer

6

@property在类级别定义A。该文档详细介绍了它的工作方式,但足以说明设置获取属性可以解决调用特定方法的问题。但是,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),但是当类级名称未定义为属性时,则不会发生任何特殊情况,因此它会像您期望的其他任何变量一样起作用。


谢谢,这非常简单。这是否意味着即使我的类及其祖先中从没有任何属性,它仍然会首先在整个继承树中搜索一个类级别的名称,然后再去研究__dict__?另外,有什么方法可以自动化吗?是的,它只是一条线,但它的那种东西,这是非常神秘的,如果你不熟悉的性质研究等血淋淋的细节阅读
保罗装甲

@PaulPanzer我尚未研究其背后的过程,因此我自己无法给您满意的答复。如果需要,您可以尝试从cpython源代码中解开它。至于自动化过程,除了首先不要将其设置为属性,或者只是添加注释/文档字符串以使阅读代码的人知道您在做什么,我认为没有什么好办法。像# declare size to not be a @property
Green Cloak Guy

4

您根本不需要分配(至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

很好,只需将父属性替换为子属性即可!+1
r.ook

这在某种程度上是直截了当的事情,但是我很感兴趣并且确实询问了tio允许非属性重写的方式,以免给必须编写一个简单的赋值属性的备忘录属性带来不便。工作。
Paul Panzer

我不确定为什么您要使代码复杂化。size只需花很少的钱就可以识别出它是一个属性而不是一个实例属性,那么您就不必做任何花哨的事情了
chepner

我的用例是具有许多属性的子类。IMO不必写5行而不是全部写5行。
Paul Panzer

2

看来您想size在课程中定义:

class Square_Integers_Below(Math_Set_Base):
    size = None

    def __init__(self, cap):
        self.size = int(math.sqrt(cap))

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


2

我建议像这样添加一个setter:

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

1

你也可以做下一件事

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还是_size_call有。您可能已经将函数调用size作为条件放入其中,并用于try... except... 进行测试,_size而不是获取不会被使用的额外类引用。无论如何,我觉得比起size = None最初的简单覆盖来说,这甚至更加神秘。
r.ook
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.