Python:在子类中删除类属性

61
我有一个子类,我希望它包含在基类中存在的类属性。
我尝试了这个,但它不起作用:
>>> class A(object):
...     x = 5
>>> class B(A):
...     del x
Traceback (most recent call last):
  File "<pyshell#1>", line 1, in <module>
    class B(A):
  File "<pyshell#1>", line 2, in B
    del x
NameError: name 'x' is not defined

我该如何做到这一点?


17
这违反了LSP原则。你为什么要这样做? - Marcelo Cantos
2
你想要删除 B 类的 x 类变量,但实际上它是存储在 A.__dict__ 中的。如果你成功删除了它,那么 A.x 也会被删除。因此,最接近的方法是通过给 B 添加一个 x 类变量来 隐藏 A.x,就像 @Keith 在他的回答中建议的那样。 - Lauritz V. Thaulow
1
@JBernardo:LSP和静态类型之间没有太多联系。LSP是一个可靠的原则,因为它使类层次结构表现出可预测和一致的行为。这同样适用于静态和动态类型的语言,无论它们是否提供违反LSP的手段。 - Marcelo Cantos
2
它不一定违反LSP。例如,如果类属性仅在内部使用,并且所有这些内部方法都被子类覆盖。 - DylanYoung
7个回答

69
您可以使用 delattr(class, field_name) 从类定义中删除它。

4
我不是在创建子类,而是在我的测试中从设置类中删除一个设置项以检查正确的错误处理 - 所以这正是我所需要的! - sage
2
有一个完整的编码示例会很有帮助。这个能填写吗?我不确定这应该放在代码的哪里。 - broccoli2000
@broccoli2000 只要在你需要使用这个类之前的任何地方都可以。 - DylanYoung

22

你不需要删除它,只需要覆盖它。

class B(A):
   x = None

或者干脆不引用它。

或者考虑一种不同的设计(实例属性?)。


27
这并没有回答问题。将属性设置为 None 并不会删除该属性,这应该是非常明显的。删除属性是由 deldelattr() 完成的。Bhushan 简洁的 回答 似乎是这个问题唯一的真正答案。 - Cecil Curry
@CecilCurry 更深层次的问题是,为什么原帖作者认为他需要删除它?无论如何,这是一个糟糕的设计。 - Keith
10
也许基类设计得不太好,用户想要在不复制整个源代码的情况下进行修复。比如,Django的AuthenticationForm应该是一个“用于验证用户”的基类,但它定义了用户名和密码表单字段,这使得它对于非密码验证无用。删除这些属性可以保留所有基类的方法以供重用,同时去除那些本来就不应该在基类中出现的垃圾代码。 - Dave

19

对我来说,所有的答案都没有起作用。

例如,delattr(SubClass, "attrname")(或其确切等效项del SubClass.attrname)不会“隐藏”父方法,因为这不是方法解析的工作方式。相反,它会失败并引发AttributeError('attrname',),因为子类没有attrname。当然,用None替换属性实际上并没有将其删除。

让我们考虑这个基类:

class Spam(object):
    # Also try with `expect = True` and with a `@property` decorator
    def expect(self):
        return "This is pretty much expected"

我知道只有两种方法可以对其进行子类化,隐藏expect属性:
  1. Using a descriptor class that raises AttributeError from __get__. On attribute lookup, there will be an exception, generally indistinguishable from a lookup failure.

    The simplest way is just declaring a property that raises AttributeError. This is essentially what @JBernardo had suggested.

    class SpanishInquisition(Spam):
        @property
        def expect(self):
            raise AttributeError("Nobody expects the Spanish Inquisition!")
    
    assert hasattr(Spam, "expect") == True
    # assert hasattr(SpanishInquisition, "expect") == False  # Fails!
    assert hasattr(SpanishInquisition(), "expect") == False
    

    However, this only works for instances, and not for the classes (the hasattr(SpanishInquisition, "expect") == True assertion would be broken).

    If you want all the assertions above to hold true, use this:

    class AttributeHider(object):
        def __get__(self, instance, owner):
            raise AttributeError("This is not the attribute you're looking for")
    
    class SpanishInquisition(Spam):
        expect = AttributeHider()
    
    assert hasattr(Spam, "expect") == True
    assert hasattr(SpanishInquisition, "expect") == False  # Works!
    assert hasattr(SpanishInquisition(), "expect") == False
    

    I believe this is the most elegant method, as the code is clear, generic and compact. Of course, one should really think twice if removing the attribute is what they really want.

  2. Overriding attribute lookup with __getattribute__ magic method. You can do this either in a subclass (or a mixin, like in the example below, as I wanted to write it just once), and that would hide attribute on the subclass instances. If you want to hide the method from the subclass as well, you need to use metaclasses.

    class ExpectMethodHider(object):
        def __getattribute__(self, name):
            if name == "expect":
                raise AttributeError("Nobody expects the Spanish Inquisition!")
            return super().__getattribute__(name)
    
    class ExpectMethodHidingMetaclass(ExpectMethodHider, type):
        pass
    
    # I've used Python 3.x here, thus the syntax.
    # For Python 2.x use __metaclass__ = ExpectMethodHidingMetaclass
    class SpanishInquisition(ExpectMethodHider, Spam,
                             metaclass=ExpectMethodHidingMetaclass):
        pass
    
    assert hasattr(Spam, "expect") == True
    assert hasattr(SpanishInquisition, "expect") == False
    assert hasattr(SpanishInquisition(), "expect") == False
    

    This looks worse (more verbose and less generic) than the method above, but one may consider this approach as well.

    Note, this does not work on special ("magic") methods (e.g. __len__), because those bypass __getproperty__. Check out Special Method Lookup section of the Python documentation for more details. If this is what you need to undo, just override it and call object's implementation, skipping the parent.

毋庸置疑,这只适用于“新风格类”(即从object继承的类),因为魔术方法和描述符协议在那里不受支持。希望这些已经过去了。

12
也许你可以将 x 设置为一个属性,并且在有人尝试访问它时触发 AttributeError。
>>> class C:
        x = 5

>>> class D(C):
        def foo(self):
             raise AttributeError
        x = property(foo)

>>> d = D()
>>> print(d.x)
File "<pyshell#17>", line 3, in foo
raise AttributeError
AttributeError

2
这是目前唯一实际可行的解决方案。当然,它仅在子类实例上“删除”属性,而不是在子类本身上。 - drdaeman

8

仔细考虑为什么你想这样做,你可能不需要。考虑不让B继承A。

子类化的思想是为了特殊化一个对象。特别地,一个类的子类应该是父类的有效实例:

>>> class foo(dict): pass
>>> isinstance(foo(), dict)
... True

如果你实现了这个行为(例如 x = property(lambda: AttributeError)),你就破坏了子类概念,这是不好的。

74
通常我讨厌没有意义的回答,例如“不要做这个很棒很有趣的事情,因为作为一个负责任的成年人...(手舞足蹈)!”但这并非例外。尽管保持Liskov替换原则对于理智的开发至关重要,但每条规则都是可以被打破的。毕竟我们都是成年人。 谢谢。 - Cecil Curry
2
我一般同意@CecilCurry的看法,因为之前我遇到过类似的答案,并且很讨厌它们。但是在这种情况下,我认为删除父属性本身并不令人感到棒极了或有趣,所以温馨提示LSP并不是胡说八道。最终,我给这个答案和CecilCurry的评论都点了赞。 - RayLuo
5
我发现指向编程普遍接受的原则的答案非常有益,因为我正在学习。我也知道每个规则都有某种灵活性,所以把它们当作指导方针。 - mehmet
有时候,为了得到正确的设计,你需要做很多“错误”的事情。所以我支持@CecilCurry的观点。 - darda

5
我也遇到了同样的问题,而且我认为在子类中删除类属性是有合理理由的:我的超类(称为A)具有只读属性,提供属性的值,但在我的子类(称为B)中,该属性是一个可读/写实例变量。我发现虽然我认为实例变量应该覆盖它,但Python仍在调用属性函数。我本可以创建一个单独的getter函数用于访问底层属性,但那似乎是不必要和不优雅的接口命名空间混乱(好像那真的很重要一样)。
事实证明,答案是创建一个新的抽象超类(称为S),具有A的原始公共属性,并让A和B派生自S。由于Python具有鸭子类型,因此B并非扩展自A并不重要,我仍然可以在相同的地方使用它们,因为它们隐式实现了相同的接口。

4
尝试这样做可能不是一个好主意,但是...
似乎无法通过“正确”的继承方式来实现这一点,因为默认情况下查找B.x的方式。当获取B.x时,首先在B中查找x,如果未找到,则在A中查找x;但是当设置或删除B.x时,只会搜索B。例如:
>>> class A:
>>>     x = 5

>>> class B(A):
>>>    pass

>>> B.x
5

>>> del B.x
Traceback (most recent call last):
File "<stdin>", line 1, in <module>  
AttributeError: class B has no attribute 'x'

>>> B.x = 6
>>> B.x
6

>>> del B.x
>>> B.x
5

在这里,我们看到首先似乎无法删除B.x,因为它不存在(A.x存在并且是在评估B.x时返回的)。但是通过将B.x设置为6,B.x将存在,可以通过B.x检索并通过del B.x删除,之后再次将A.x作为响应返回给B.x

另一方面,您可以使用元类使B.x引发AttributeError

class NoX(type):
    @property
    def x(self):
        raise AttributeError("We don't like X")

class A(object):
    x = [42]

class B(A, metaclass=NoX):
    pass

print(A.x)
print(B.x)

现在当然,纯粹主义者可能会喊叫这违反了LSP,但事情并不简单。这一切归结于您是否认为通过这样做创建了一个子类型。issubclass和isinstance方法说是,但LSP说不行(许多程序员会认为“是”,因为您继承自A)。
LSP意味着如果B是A的子类型,则我们可以在任何可以使用A的地方使用B,但由于在执行此构造时无法执行此操作,因此我们可以得出结论B实际上不是A的子类型,因此没有违反LSP。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接