简体   繁体   English

Python 相当于 .Net 的密封类

[英]Python's equivalent of .Net's sealed class

Does python have anything similar to a sealed class? python有什么类似于密封类的东西吗? I believe it's also known as final class, in java.我相信它在 java 中也被称为 final 类。

In other words, in python, can we mark a class so it can never be inherited or expanded upon?换句话说,在python中,我们可以标记一个类,使其永远不能被继承或扩展吗? Did python ever considered having such a feature? python有没有考虑过有这样的功能? Why?为什么?

Disclaimers免责声明

Actually trying to understand why sealed classes even exist.实际上试图理解为什么密封类甚至存在。 Answer here (and in many , many , many , many , many , really many other places) did not satisfy me at all, so I'm trying to look from a different angle. 在这里回答(以及在许多许多许多许多许多真的许多其他地方)根本不能满足我,所以我试图从不同的角度来看。 Please, avoid theoretical answers to this question, and focus on the title!请避免对这个问题的理论答案,并专注于标题! Or, if you'd insist, at least please give one very good and practical example of a sealed class in csharp, pointing what would break big time if it was unsealed.或者,如果你坚持,至少请给出一个非常好的和实用的 csharp 密封类的例子,指出如果它被解封会破坏什么。

I'm no expert in either language, but I do know a bit of both.我不是这两种语言的专家,但我对这两种语言都有一点了解。 Just yesterday while coding on csharp I got to know about the existence of sealed classes.就在昨天在 csharp 上编码时,我知道了密封类的存在。 And now I'm wondering if python has anything equivalent to that.现在我想知道python是否有与此等价的东西。 I believe there is a very good reason for its existence, but I'm really not getting it.我相信它的存在是有充分理由的,但我真的不明白。

You can use a metaclass to prevent subclassing:您可以使用元类来防止子类化:

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

gives:给出:

>>> 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

The __metaclass__ = Final line makes the Foo class 'sealed'. __metaclass__ = Final Foo类“密封”。

Note that you'd use a sealed class in .NET as a performance measure;请注意,您将在 .NET 中使用密封类作为性能度量; since there won't be any subclassing methods can be addressed directly.因为不会有任何子类化方法可以直接解决。 Python method lookups work very differently, and there is no advantage or disadvantage, when it comes to method lookups, to using a metaclass like the above example. Python 方法查找的工作方式非常不同,在方法查找方面,使用像上面示例这样的元类没有优势或劣势。

Before we talk Python, let's talk "sealed":在我们谈论 Python 之前,让我们先谈谈“密封”:

I, too, have heard that the advantage of .Net sealed / Java final / C++ entirely-nonvirtual classes is performance.我也听说过 .Net 密封/Java final/C++ 完全非虚拟类的优势在于性能。 I heard it from a .Net dev at Microsoft, so maybe it's true.我是从 Microsoft 的 .Net 开发人员那里听说的,所以也许是真的。 If you're building a heavy-use, highly-performance-sensitive app or framework, you may want to seal a handful of classes at or near the real, profiled bottleneck.如果您正在构建一个重度使用、高性能敏感的应用程序或框架,您可能希望将少数类密封在或接近真实的、已分析的瓶颈处。 Particularly classes that you are using within your own code.特别是您在自己的代码中使用的类。

For most applications of software, sealing a class that other teams consume as part of a framework/library/API is kinda...weird.对于大多数软件应用程序,将其他团队作为框架/库/API 的一部分使用的类密封起来有点……奇怪。

Mostly because there's a simple work-around for any sealed class, anyway.主要是因为无论如何,任何密封类都有一个简单的解决方法。

I teach "Essential Test-Driven Development" courses, and in those three languages, I suggest consumers of such a sealed class wrap it in a delegating proxy that has the exact same method signatures, but they're override-able (virtual), so devs can create test-doubles for these slow, nondeterministic, or side-effect-inducing external dependencies.我教授“基本测试驱动开发”课程,在这三种语言中,我建议这种密封类的使用者将其包装在具有完全相同方法签名的委托代理中,但它们是可覆盖的(虚拟),因此开发人员可以为这些缓慢的、不确定的或引起副作用的外部依赖项创建测试替身。

[Warning: below snark intended as humor. [警告:下面的蛇皮意为幽默。 Please read with your sense of humor subroutines activated.激活您的幽默感子程序阅读。 I do realize that there are cases where sealed/final are necessary.]我确实意识到有些情况下需要密封/最终。]

The proxy (which is not test code) effectively unseals (re-virtualizes) the class, resulting in v-table look-ups and possibly less efficient code (unless the compiler optimizer is competent enough to in-line the delegation).代理(不是测试代码)有效地解封(重新虚拟化)类,导致 v-table 查找和可能效率较低的代码(除非编译器优化器足以内联委托)。 The advantages are that you can test your own code efficiently, saving living, breathing humans weeks of debugging time (in contrast to saving your app a few million microseconds) per month... [Disclaimer: that's just a WAG.优点是您可以有效地测试自己的代码,每月为生活、呼吸人类节省数周的调试时间(与为您的应用程序节省几百万微秒相比)...... [免责声明:这只是一个 WAG。 Yeah, I know, your app is special.是的,我知道,您的应用程序很特别。 ;-] ;-]

So, my recommendations: (1) trust your compiler's optimizer, (2) stop creating unnecessary sealed/final/non-virtual classes that you built in order to either (a) eke out every microsecond of performance at a place that is likely not your bottleneck anyway (the keyboard, the Internet...), or (b) create some sort of misguided compile-time constraint on the "junior developers" on your team (yeah...I've seen that, too).所以,我的建议:(1) 相信你的编译器的优化器,(2) 停止创建你构建的不必要的密封/最终/非虚拟类,以便 (a) 在一个不太可能的地方维持每一微秒的性能无论如何你的瓶颈(键盘,互联网......),或者(b)对你团队的“初级开发人员”产生某种误导的编译时间约束(是的......我也看到了)。

Oh, and (3) write the test first.哦,(3)先写测试。 ;-) ;-)

Okay, yes, there's always link-time mocking, too (eg TypeMock).好的,是的,也总是存在链接时模拟(例如 TypeMock)。 You got me.你得到了我。 Go ahead, seal your class.来吧,封你的课。 Whatevs.什么。

Back to Python: The fact that there's a hack rather than a keyword is probably a reflection of the pure-virtual nature of Python.回到 Python:存在 hack 而不是关键字这一事实可能反映了 Python 的纯虚拟特性。 It's just not "natural."这不是“自然的”。

By the way, I came to this question because I had the exact same question.顺便说一句,我来这个问题是因为我有完全相同的问题。 Working on the Python port of my ever-so-challenging and realistic legacy-code lab, and I wanted to know if Python had such an abominable keyword as sealed or final (I use them in the Java, C#, and C++ courses as a challenge to unit testing).在我极具挑战性和现实性的遗留代码实验室的 Python 端口上工作,我想知道 Python 是否有像 seal 或 final 这样可恶的关键字(我在 Java、C# 和 C++ 课程中将它们用作单元测试的挑战)。 Apparently it doesn't.显然它没有。 Now I have to find something equally challenging about untested Python code.现在我必须为未经测试的 Python 代码找到同样具有挑战性的东西。 Hmmm...嗯……

Python does have classes that can't be extended, such as bool or NoneType : 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

However, such classes cannot be created from Python code.但是,不能从 Python 代码创建此类类。 (In the CPython C API, they are created by not setting the Py_TPFLAGS_BASETYPE flag.) (在 CPython C API 中,它们是通过不设置Py_TPFLAGS_BASETYPE标志来创建的。)

Python 3.6 will introduce the __init_subclass__ special method; Python 3.6 将引入__init_subclass__特殊方法; raising an error from it will prevent creating subclasses.从中引发错误将阻止创建子类。 For older versions, a metaclass can be used.对于旧版本,可以使用元类。

Still, the most “Pythonic” way to limit usage of a class is to document how it should not be used.尽管如此,限制类使用的最“Pythonic”方式是记录不应该如何使用它。

Similar in purpose to a sealed class and useful to reduce memory usage ( Usage of __slots__? ) is the __slots__ attribute which prevents monkey patching a class. __slots__属性与密封类的目的相似并且有助于减少内存使用( 使用 __slots__? ),它可以防止猴子修补类。 Because when the metaclass __new__ is called, it is too late to put a __slots__ into the class, we have to put it into the namespace at the first possible timepoint, ie during __prepare__ .因为当元类__new__被调用时,将__slots__放入该类已经太晚了,我们必须在第一个可能的时间点将其放入命名空间,即在__prepare__期间。 Additionally, this throws the TypeError a little bit earlier.此外,这会提前一点抛出 TypeError。 Using mcs for the isinstance comparison removes the necessity to hardcode the metaclass name in itself.使用 mcs 进行 isinstance 比较消除了对元类名称本身进行硬编码的必要性。 The disadvantage is that all unslotted attributes are read-only.缺点是所有未开槽的属性都是只读的。 Therefore, if we want to set specific attributes during initialization or later, they have to slotted specifically.因此,如果我们想在初始化期间或之后设置特定的属性,就必须专门对它们进行开槽。 This is feasible eg by using a dynamic metaclass taking slots as an argument.这是可行的,例如通过使用将槽作为参数的动态元类。

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")

where the attempt of overwriting foo.foo will throw AttributeError: 'Foo' object attribute 'foo' is read-only and attempting to add foo.x will throw AttributeError: 'Foo' object has no attribute 'x' .覆盖 foo.foo 的尝试将抛出AttributeError: 'Foo' object attribute 'foo' is read-only并且尝试添加 foo.x 将抛出AttributeError: 'Foo' object has no attribute 'x' The limiting power of __slots__ would be broken when inheriting, but because Foo has the metaclass Final, you can't inherit from it. __slots__的限制能力在继承时会被打破,但因为 Foo 有元类 Final,你不能从它继承。 It would also be broken when dict is in slots , so we throw a ValueError in case.dictslot中时它也会被破坏,所以我们抛出一个 ValueError 以防万一。 To conclude, defining setters and getters for slotted properties allows to limit how the user can overwrite them.总而言之,为开槽属性定义 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

In that regard, Foo could not be inherited or expanded upon.在这方面, Foo 不能被继承或扩展。 The disadvantage is that all attributes have to be explicitly slotted, or are limited to a read-only class variable.缺点是所有属性都必须显式插入,或者仅限于只读类变量。

Python 3.8 has that feature in the form of the typing.final decorator: 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

See https://docs.python.org/3/library/typing.html#typing.finalhttps://docs.python.org/3/library/typing.html#typing.final

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM