Python的等价于.Net的sealed类

13

Python有类似于封闭类的东西吗?在Java中,我相信它也被称为最终类。

换句话说,在Python中,我们可以将一个类标记为永远不能被继承或扩展吗?Python是否曾考虑过具有此功能?为什么?

免责声明

实际上我想了解为什么存在密封类这里的答案(以及很多, 很多, 很多, 很多, 很多, 真的很多其他地方)并没有完全满足我的需求,所以我试图从不同的角度来看待这个问题。请避免对这个问题提供理论性的答案,而是专注于标题!或者,如果您坚持的话,请至少给出一个在csharp中使用密封类的非常好的实际例子,并指出如果它没有被密封会导致什么问题。

我虽然不是专家,但对这两种语言都有一些了解。就在昨天,当我在编写C#代码时,我了解到了密封类的存在。现在我在想Python是否有任何相当的东西。我相信它存在的原因非常好,但我真的不明白。

密封类在某些情况下非常有用。其中一种情况可能是您只有静态和/或常量成员,因此继承不会有任何帮助。另一个替代用途可能是在类是这样的情况下,任何错误都应通过更改实现并提交修复来解决,而不是通过子类化来解决。sealing类可以鼓励这样做...虽然这可能不是它预期的使用方式 ;) - Chris Pfohl
1
@ChristopherPfohl 只有静态/常量成员听起来是封闭它的一个很好的理由。但是,再次问一下,谁会想要扩展这样的类呢?仅将其用于强制进行调试,只与您相关,对我来说听起来像是一种可怕的中心化导向的理由,但也许是迄今为止我听到的最好的理由。 - cregox
1
看起来在Python 3.8中我们有了Final装饰器。https://www.python.org/dev/peps/pep-0591/ - KFL
5个回答

15

你可以使用元类来防止子类化:

class Final(type):
    def __new__(cls, name, bases, classdict):
        for b in bases:
            if isinstance(b, Final):
                raise TypeError("type '{0}' is not an acceptable base type".format(b.__name__))
        return type.__new__(cls, name, bases, dict(classdict))

class Foo:
    __metaclass__ = Final

class Bar(Foo):
    pass

提供:

>>> class Bar(Foo):
...     pass
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in __new__
TypeError: type 'Foo' is not an acceptable base type

__metaclass__ = Final 这一行使得 Foo 类成为了“密封”(sealed)类。

需要注意的是,在 .NET 中,使用密封类是为了提高性能;由于没有子类化,方法可以直接调用。Python 方法查找的工作方式与此大不相同,并且在涉及方法查找时,使用上述示例中的元类既没有优势也没有劣势。


这很棒,我相信它回答了问题。不幸的是,对我来说并没有真正帮助。这告诉我答案实际上应该是“不,Python没有实现封闭类,但你仍然可以自己编写”。但是,您是否知道这是否是一个干净的hack?您知道,它不会破坏东西或高度不鼓励或只是使代码运行变慢...我相信最初,封闭类也意味着更快。 - cregox
@Cawas:这不会使任何东西变慢;元类仅适用于定义子类时。它也不会使任何东西变快,使用此“hack”无法获得查找优化。 - Martijn Pieters
性能只是人们为封装类提供的次要但更简单的理由之一。你告诉过我如何在Python中封装一个类,现在我想知道为什么要这样做。由于它不是本地的,我想人们从来没有这样做过。可能还被建议不要这样做。为什么? - cregox
1
@Cawas:这显然不是Pythonic的;阻止他人创建子类是非常专制的事情;Python精神的一部分是让其他人自己决定是否要自食其果,这不是原始类作者的问题。 - Martijn Pieters
@PaulMcGuire:这是一个很好的比喻。就像私有名称混淆(以双下划线开头的名称)一样,Final元类可以被规避,但它发出了一个强有力的信号,即作者希望不必处理与子类的冲突。 :-) - Martijn Pieters
显示剩余2条评论

2
Python 3.8通过typing.final装饰器实现了该功能:
class Base:
    @final
    def done(self) -> None:
        ...
class Sub(Base):
    def done(self) -> None:  # Error reported by type checker
        ...

@final
class Leaf:
    ...
class Other(Leaf):  # Error reported by type checker

请参见 https://docs.python.org/zh-cn/3/library/typing.html#typing.final

这应该是被接受的答案 - 它是 Python 最接近封闭类的方式,应该能满足大多数对它们的使用情况。 - undefined

2
在我们讨论Python之前,让我们先谈谈“sealed”:
我也听说过.Net sealed / Java final / C++ entirely-nonvirtual类的优点是性能。我从微软的一位.Net开发者那里听说的,所以可能是真的。如果您正在构建一个使用频率高、性能敏感度高的应用程序或框架,您可能希望在实际的性能瓶颈处或附近封装一些类。特别是那些在自己的代码中使用的类。
对于大多数软件应用程序来说,封装其他团队作为框架/库/API的类有点...奇怪。
主要是因为任何封装类都有一个简单的解决方法。
我教授“Essential Test-Driven Development”课程,在这三种语言中,我建议这样一个封装类的消费者将其包装在一个委托代理中,该代理具有完全相同的方法签名,但它们是可重写(虚拟的),因此开发人员可以为这些缓慢的、不确定的或引起副作用的外部依赖项创建测试替身。
[警告:以下嘲讽旨在幽默。请带着您的幽默子程序阅读。我确实意识到有些情况下需要封装/final。]
代理(不是测试代码)有效地解封(重新虚拟化)类,导致v表查找和可能不那么高效的代码(除非编译器优化器足够聪明以内联委托)。优点是您可以高效地测试自己的代码,每个月节省生活、呼吸的人数数周的调试时间(与每月节省您的应用程序几百万微秒相比)... [免责声明:这只是一个猜测。是的,我知道,你的应用程序很特别。;-)]
所以,我的建议是:(1)相信您的编译器优化器,(2)停止创建不必要的封装/final/non-virtual类,这些类是为了在可能不是瓶颈的地方(键盘、互联网...)挤出每一微秒的性能,或者在您的团队中创建某种误导性的编译时约束(是的...我也见过)。
哦,还有(3)先写测试。;-)
好吧,是的,还有链接时模拟,例如TypeMock。你抓住我了。去吧,封装你的类。无论怎样。
回到Python:事实上,存在一个hack而不是关键字,这可能反映了Python的纯虚拟本质。它就是不“自然”。
顺便说一下,我来到这个问题是因为我有完全相同的问题。正在处理我的极具挑战性和现实的遗留代码实验室的Python端口,并且我想知道Python是否有类似封装或终极关键字(我在Java、C#和C++课程中使用它们作为单元测试的挑战)。显然没有。现在我必须找到一些关于未经测试的Python代码同样具有挑战性的东西。嗯...

非常冗长且有趣的观点。我实际上完全同意Python类永远不应该被封闭,即使我仍然不知道背后的原因 - 这可能是沿着“从未考虑过任何这种专制主义”的思路。无论如何,如果您还添加了实际的方法来自毁(以回答标题中的问题),同时解释为什么我们不应该这样做以及最佳替代方案,也许您可以将其转化为一个很棒的答案。;P - cregox

1
Python确实有一些无法扩展的类,比如boolNoneType
>>> class ExtendedBool(bool):
...     pass
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: type 'bool' is not an acceptable base type

然而,这样的类无法从Python代码中创建。(在CPython C API中,它们是通过不设置Py_TPFLAGS_BASETYPE标志来创建的。)
Python 3.6将引入__init_subclass__特殊方法;从中引发错误将防止创建子类。对于旧版本,可以使用元类。
尽管如此,限制类的使用最“Pythonic”的方式是记录如何不应使用它。

0
类似于sealed class的作用,并且有助于减少内存使用(__slots__的用途?)是__slots__属性,该属性可以防止monkey patching一个class。因为当metaclass __new__被调用时,把__slots__放入class已经太晚了,我们必须在第一个可能的时间点即__prepare__期间将其放入namespace中。此外,这会更早地引发TypeError异常。使用mcs进行isinstance比较可以消除在元类本身中硬编码元类名称的必要性。缺点是所有未slotted的属性都是只读的。因此,如果我们想在初始化或以后设置特定的属性,则必须明确指定它们为slotted。这可以通过使用将slots作为参数的动态元类来实现。
def Final(slots=[]):
    if "__dict__" in slots:
        raise ValueError("Having __dict__ in __slots__ breaks the purpose")
    class _Final(type):
        @classmethod
        def __prepare__(mcs, name, bases, **kwargs):   
            for b in bases:
                if isinstance(b, mcs):
                    msg = "type '{0}' is not an acceptable base type"
                    raise TypeError(msg.format(b.__name__))

            namespace = {"__slots__":slots}
            return namespace
    return _Final

class Foo(metaclass=Final(slots=["_z"])):
    y = 1    
    def __init__(self, z=1):       
        self.z = 1

    @property
    def z(self):
        return self._z

    @z.setter
    def z(self, val:int):
        if not isinstance(val, int):
            raise TypeError("Value must be an integer")
        else:
            self._z = val                

    def foo(self):
        print("I am sealed against monkey patching")

试图覆盖foo.foo将会抛出AttributeError: 'Foo' object attribute 'foo' is read-only,而尝试添加foo.x将会抛出AttributeError: 'Foo' object has no attribute 'x'。当继承时,__slots__的限制力量会被打破,但由于Foo具有元类Final,因此无法从中继承。如果dictslots中,则也会被打破,因此我们会抛出ValueError。总之,为slotted属性定义setter和getter可以限制用户如何覆盖它们。

foo = Foo()
# attributes are accessible
foo.foo()
print(foo.y)
# changing slotted attributes is possible
foo.z = 2

# %%
# overwriting unslotted attributes won't work
foo.foo = lambda:print("Guerilla patching attempt")
# overwriting a accordingly defined property won't work
foo.z = foo.foo
# expanding won't work
foo.x = 1
# %% inheriting won't work
class Bar(Foo):
    pass

在这方面,Foo 无法被继承或扩展。缺点是所有属性都必须显式地插入槽中,或者仅限于只读类变量。

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