Python中的子类化:Redux

2021-06-23 06:49:40

子类和组成之间的冲突与面向对象的编程一样古老。最新的语言(如Go或Rust)证明您不需要子类别以成功编写代码。但具体怎样?

任何跟随我的人都足以让我坚持在构成过度继承营地。但是,Python的设计是以某种方式设计的,无需子类别就无法编写惯例代码。当时有时候是,在第1主题的情况下,我对这篇文章的目标是思考问题。

我意识到这个博文很长。事实上,它是我2006年论文自上文以来的最长的散文。客观地,我应该将其分成至少三个部分。这将是更好的参与(SEO!社交媒体!点击!),它会使人们实际读到尽头。

但我希望它能成为自己。我希望它成为多年来学到的东西的蒸馏精华。当然,你可以随意服用尽可能多的休息 - 这篇文章不会在任何地方!

但请给我礼貌地拿回愤怒的评论,直到整体读它。无论你的忠诚是什么,我希望你能用开放的思想阅读它。

让我们从细微差别开始。许多关于子类化讨论的原因之一是如此令人沮丧的果蝇是不仅仅是一种类型的继承。作为奇妙的文章,为什么继承从未做出任何意义解释2,有三种类型应该永远不会混合 - 无论你如何对统一的对子。

这使得三个人可能互相争论,每个人都以自己的方式正确,从不找到共同点。我们都看到这些讨论展开了。

在我的经验中 - 如果严格使用 - 一个是好的,一个是可选的,但有用,一个是坏的。子类化子类的大多数问题源于我们尝试一次使用多种类型的子类或将物体设计聚焦在坏类型上。

在所有情况下,你牺牲了阅读方便的便利。这不一定是糟糕的,因为软件设计都是关于权衡的,你可以得出结论,在某些情况下它是完全值得的。这就是为什么我不希望你同意所遵循的一切。但我希望挑起一些有助于你将来做出这种决定的想法。

但现在,没有进一步的ADO,让我们看看三种类型。从坏的开始。

对子类的大多数批评来自代码共享,所以就是这样。我不觉得我有很多东西要加入它,所以我将通过远远聪明地聪明地与特殊的现有技术联系起来,比我更雄辩:

在多个轴上的变化。这是布兰登帖子的主要实用外卖和桑迪谈的下半场。它不容易解释,所以我会引用他们的作品,但本质是:如果要自定义一个类的一个行为方面,则通过子类化的代码共享将无法正常运行。它导致子类爆炸。

class和实例命名空间变得混乱。如果您在从一个或多个基本类继承的类中有属性self.x,则需要研究和精神能量,以了解x来自的位置。读取代码时,这是正确的,并且在调试时也是如此。

它还意味着在同一层次结构中始终存在两个类的危险 - 这对彼此不了解 - 尝试具有相同名称的属性。 Python具有双重下划线前缀(__X)的概念来处理此方案,但这一直被吓到并认为更喜欢同意成人的原则。

问题是,如果没有通知所有各方,那么知情同意是不可能的。这一问题与多种继承及其极端形式的混合形式呈呈指重较差。你依靠类 - 你可能没有控制,这对彼此一无所知 - 相交在共享命名空间中。

另一个问题是您无法控制您从基本类别向用户曝光的方法和属性。它们就在那里,玷污了你的API表面。随着时间的推移,随着基本类的发展和添加或重命名方法和属性,可能会随着时间的推移而变化。这是attrs(和最终DataClasses)选择使用类装饰器而不是子类化的原因之一:您必须在附加到课程的内容中进行故意。不可能意外地泄露给所有子类。

令人困惑的间接。这是前一个问题的特例,奥维尔和纳撒尼尔谈话的要点。如果每种方法都在自我上,那就不清楚它来自观看呼叫时的到来。除非你非常小心,否则每次尝试控制流量都以野鹅追逐结束。一旦多次继承发挥作用,最好在MRO和SUPER()上阅读。我认为这是公平的,说如果一个问题归结为“超级()即使是什么?”在stackoverflow上获得近3,000个升值和超过1,000个书签。

如果您构建了用于实现或覆盖从其他地方调用的现有方法,则构建需要子类化的API,则此此会产生额外的问题。扭曲和异步都曾分别致力于他们的协议3课程,它永远伤痕累累。最常见的问题是,找出存在哪些方法(特别是在扭曲的深层层次中)和通常沉默的故障,如果你命名你的方法,那么基本班没有找到它。 4.

“也是基于子类的设计是一个巨大的错误”可能是编程中最常见的句子。

- 2017年4月6日Cory Benfield(@Lukasaoz)

只有当我需要弯曲我不控制的类的行为时,我只使用子类别进行代码共享。我认为这是一种令人难以置恶的猴子补丁。通常最好编写适配器,门面,代理或装饰员,但有些情况下,您需要委派的方法的数量会使您想要仅更改一个小细节5。

摘要数据类型(ADTS)主要用于收紧界面合同。您希望能够说您希望具有某些属性(属性,方法)的对象,并不关心其余部分。在许多语言中,它们被称为接口,这听起来不那么自命不凡,这就是为什么我从现在开始使用这个术语。

由于Python是动态键入的,并且键入注释是严格的可选,因此您不需要正式接口。但是,有一种方法可以明确定义一个代码函数所需的接口非常有用。自从像Mypy这样的类型跳棋的出现以来,他们已经成为验证的API文档,我找到了精彩。

例如,如果要编写带有read()方法的对象的函数,则以某种方式定义具有该方法的接口读取器(如何在一分钟内解释)并将其使用如下:

只要它返回它可以打印的字符串,您的打印机()函数不关心读取()正在做的事情。它可以返回预定义的字符串,读取文件,或制作Web API调用。打印机()不关心,如果您尝试调用任何其他方法,则将您的类型检查器对其喊叫。

摘要基本类(ABCS)是Zope.Interface的强大版本,使用名义亚型。自从Python 2.6和标准图书馆充满了它们,他们已经存在。

请注意,并非每个抽象基类也是一种抽象数据类型。有时它只是一个不完整的课程,你应该通过对它进行对它进行组织并实现其抽象方法来完成 - 而不是接口。虽然,但区别并不总是100%。

协议通过使用结构亚型来避免子类化。它们已在Python 3.8中添加,但键入 - 扩展使它们可用作Python 3.5。

标称亚型和结构亚型是大词,但幸运的是他们很简单地解释。

标称子类型意味着您必须告诉类型系统,您的类是接口定义的子类型。 ABC通常通过子类化,但您也可以使用寄存器()方法。

这就是您如何将读者界面定义从介绍和标记Fooreader和Barreader的方式:

导入ABC类阅读器(Metaclass = ABC。ABCMETA):@ABC。 AbstractMethod Def Read(Self) - > str:... class fooreader(读者):def读(self) - > str:返回" foo"课程禁令:Def读(Self) - > str:返回"酒吧"读者。注册(Barreader)断言(Fooreader(),读者)断言isinstance(Barreader(),读者)

如果Fooreader没有称为读取的方法,则实例化将在运行时失败。如果您使用寄存器()路由与禁令者一样,则在运行时未验证接口,并且它将成为一个(作为文档调用它)“虚拟子类”。这为您提供了使用更多动态或神奇方式来提供所需接口的自由。由于register()将实现对象作为其参数,您可以将其用作类装饰器并保存两个空行。

在标称亚型中,多重继承不仅被接受但鼓励,因为理想情况下没有任何方法,没有行为,并且无法遗传和绝望地混合 - 只有类别才会复制。一个类可以实现许多不同的接口和较小的界面,更好。

使用ABC来定义接口的一个“Upids”是通过对它们进行对它们进行对,可以通过将常规方法添加到抽象基类来分割代码共享中。但正如开始的那样:混合子类类型是一个坏主意。代码共享是一个坏主意。多重继承使它成为一个额外的坏主意。要公平,我已经看到了这种模式的良好用途,但你必须与你的方法非常明智。

结构亚型是鸭键入类型:如果您的类满足协议的约束,则会自动被认为是它的子类型。因此,一个类可以在不知道它们的情况下从各种包中实现许多协议!

默认情况下,这仅适用于类型检查器,但如果应用键入键入.Runtime_Checkable(),您也可以执行isInstance()检查。

从键入导入协议,runtime_checkable @runtime_checkable类读者(协议):def读取(self) - > str:... class fooreader:def读(self) - > str:返回" foo"断言isinstance(Fooreader(),读者)

正如您所看到的那样:Fooreader不知道读者协议存在!

我真正喜欢协议是如何定义我完全非侵入性的接口,并且该定义可以与接口的消费者一起定义。当您在同一代码库中具有不同的接口的不同实现时,这很好。例如,您可以在生产中发送电子邮件,但只需将其打印到开发6中的控制台。

或者,如果您只使用第三方类的小子集,并且希望明确哪个子集。这是伟大的(验证!)文档,并在为测试实施假货时帮助。

有关协议和结构亚型的更多详细信息,请查看雕文,我想要一只新鸭。

虽然这种类型的子类化大多是无害的,但由于键入,因此您不需要对Python中的抽象数据类型进行子类化.Ptotocol和ABCS的register()方法。

所以我们有一个对余额的子类型,这是一个不必要的余额。现在我们已经达到了好类型。事实上,即使你想要,你就无法绕过Python的这种继承。除非您想停止使用异常。

有趣的是,专业化往往误解了。直观很容易:如果我们说B类专门为基础A类,我们说B类是具有额外属性的。一只狗是一种动物。 A350是乘客飞机。它们具有其基类的所有属性,并添加属性,方法或在层次结构中的一个位置。

尽管这种诱人的简单性,但它经常使用不正确。最臭名昭着的错误说明,广场是矩形的专业化,因为几何上,这是一个特殊的情况。但是,一个正方形不是矩形加上更多。

除非代码知道它必须期望一个正方形,否则您无法使用矩形的广场。如果您无法与对象进行交互,就像它是其基类的实例一样,那么您重新侵犯LISKOV替换原则9,您无法编写多态代码。

如果您仔细观察,您会意识到前一节的接口是专业化的特殊情况。您始终将通用API合同专门为混凝土!关键差异是抽象数据类型是......嗯......摘要。

当我尝试代表严格分层的数据时,我发现专业化非常有用。

例如,想象一下,您希望将电子邮件帐户表示为类。它们都在数据库和地址中与其ID一起分享一些数据,但是 - 根据帐户的类型 - 它们(CAN)具有额外的属性。重要的是,这些添加的属性和方法几乎没有于现有的属性。例如,存储服务器上电子邮件的邮箱需要密码哈希的形式登录信息。接受电子邮件的帐户,只将它们转发到另一个电子邮件地址不10。

类邮箱:ID:UUID ADDR:STR PWD:STR类转发器:ID:UUID ADDR:STR目标:列表[str]

地址类型在类中编码,每个类仅具有它使用的字段。如果你的模型很简单,这绝对是去的方式。如果您有更多的字段和更多类型,则在重复重复的任何尝试都只有意义。

您添加到任一类的任何方法都将完全独立于另一个 - 留下困惑的空间。您还可以使用union类型的类型检查程序使用这些类:邮箱|转发器。

通常在任何情况下以这种方法开始这种方法是一个好主意,因为复制比错误的抽象更便宜。看到您面前的所有可能的字段更容易更轻松地进行更轻松的设计决策。

这是一种设计,当您尝试避免所有成本的遗产时,您可能最终会结束,但仍然避免重复自己:

class addrtype(枚举。枚举):邮箱="邮箱"转发器="转发器" Class EmailAddr:类型:addrtype ID:uuid addr:str#只有在type == addrtype.mailbox pwd:str | none#只有在incy == addrtype.forwarder target:list [str] |没有任何

从技术上讲,这更干燥,但它使课堂的情况使用更加尴尬。大多数字段的类型/存在完全取决于类型字段的值,它只存在,因为所有地址类型都共享相同的类类型。

它与我最喜欢的设计原则相矛盾,使非法状态不可思议,并且不可能使用类型检查器明智地检查,这将抱怨一直访问不可能的字段。

所有在此类上工作的行为都将被列入大量条件(IF-ELIF - else语句),这会显着提高代码的复杂性。全态的整点是避免这种情况。

具有可选属性11可能是一个红旗。拥有需要评论的字段来解释何时使用它们是五月一天的集会。由于争议类型的注释是,在这种情况下,他们显然指出了您的模型存在问题。如果没有它们,您必须注意到您的代码比应该的代码更加复杂,这不是直截了当的。

您可以使情况更少疼痛,将邮箱特定数据移动到类中,并使该字段可选。它更好但仍然不必要的笨重。

这种方法将最后一个反转并与我们过于简单的数据模型看起来很傻,但是让我们假装EmailAddr有更多的领域,使得值得被包装为自己的课程:

Class EmailAddr:ID:UUID ADDR:STR类邮箱:电子邮件:EmailAddr PWD:Str类转发器:电子邮件:EmailAddr目标:列表[str]

这种方法并不那么糟糕!我们没有任何可选字段,所有数据关系都很清楚。随着可读性和清晰度,没有什么可以抱怨的。

除了它也是非常笨重的,你不需要咨询Guido来意识到这是一切而是蟒蛇。那么为什么它看起来如此思考,虽然构成应该比遗传更好? EmailAddr和邮箱/转发器太密切相关 - 命名字段甚至尴尬地存储它。作文并没有失败我们,但在这种情况下强迫一个人的关系感觉就像违反谷物。

但向我们展示我们的模型很有用:它们都有共同的基础信息并密切相关。因此,让我们走出最后一步,并使用Python的方式共享公共基础并通过子类化专业。当我在本文的后续部分中提高基于子类设计的设计时,我们将回到作文。

最后,在我看来的方法最符合人体工程学,干燥,明显,可行的类型检查:

每当您有一个邮箱时,您就知道您有一个PWD字段 - 等等。该类型在类中编码,因此您不必在字段中重复它。邮箱严格是ModeAddr加上更多。

至于代码,您现在必须了解对上述LISKOV替代原则的负责任子类的规则。这是额外的复杂性和精神开销,但边界和责任都更加清晰。

阅读清晰度与各种子类一样受到各种子类,因为您必须组装最终类以了解存在的字段。但有效地在第一种方法中获得了相同的课程。只要你没有过度,理想地保持身体彼此的定义,这是在这样的情况下最好的权衡。

我在我的解析库中为PEM文件中使用它是如此有用,但尚未后悔。

从本节中派生的一般建议是首先始终关注数据的形状,只有那么与之有关。

一旦你的形状钉下了,行为就会变得更加自然。一个很好的例子,这是一个不确定数据的SAN I / O运动首先,因为该行为应该是通过设计替代的。

只要你避免在专业化时方法之间的交叉层次结构,你应该没问题。但总是问自己,如果一个函数是不够的 - 特别是如果你正在协调两个或更多课程之间的工作,并且没有多态性来利用。如果您无法确定方法所属的类,答案通常都不是。

最后一定要了解@singledispatch;如果你还没有,它会觉得魔法。

最后一个方法是如此有用,即它潜入我们的勿忘映射下的emiddation:

邮箱实例现在具有属性addr,就好像它在其中定义了:https://play.golang.org/p/wsjja6myudb。但初始化时,您仍然必须明确,并且没有实际层次结构。有n

......