健壮的Python
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.2 类型系统

正如本章前面所述,类型系统旨在为用户提供一种对语言中的行为和约束进行建模的方法。无论是在代码编写的过程中还是在代码执行的阶段,编程语言对其类型系统的工作方式都设定了期望。

2.2.1 强类型与弱类型

类型系统可以从弱和强的角度进行分类。强类型语言倾向于使用类型来限制它们的操作。换句话说,如果你破坏了类型的语义表达,程序会通过编译器错误或运行时错误来告知开发者(有时非常醒目)。Haskell、TypeScript和Rust等语言都被认为是强类型的。支持者推崇强类型语言的主要原因是在构建或运行代码时错误会更明显。

相比之下,弱类型语言不会使用类型来限制开发者对编程元素的操作。为了理解操作,类型通常会被强制转换为不同的类型。JavaScript、Perl和旧版本的C等语言都是弱类型的。它们的支持者提倡开发人员可以快速迭代代码而无须在开发过程中陷入语法的纠缠。

Python属于强类型语言。类型之间发生的隐式转换很少。它会非常明显地指出你的非法操作:

将其与弱类型语言(例如JavaScript)进行对比:

在健壮性方面,像Python这样的强类型语言肯定更具优势。虽然错误会在运行时而不是开发时出现,但它们会显式地出现在TypeError异常中。这会大大减少调试问题的时间,使开发人员能够更快地进行增量价值交付。

弱类型语言本质上就是不健壮的吗?

弱类型语言的代码毫无疑问也可以是健壮的,我不是在贬低这些弱类型语言。世界上运行的大量生产级JavaScript就可以说明这一点。但是,弱类型语言需要格外小心才能健壮。使用它们的过程中很容易弄错变量类型并做出错误的假设。开发人员会非常依赖代码检查工具、测试以及其他工具来提高代码的可维护性。

2.2.2 动态类型与静态类型

我要讨论的关于类型的另一个区别是:静态类型与动态类型。这是计算机处理类型机器表达的根本区别。

静态类型语言在构建或者打包代码时将其类型信息嵌入变量中。开发人员可以显式地向变量添加类型信息,或者某些工具(如编译器)可以为开发人员推断类型。变量在运行时不会改变其类型(因此被称为“静态”)。静态类型语言的支持者吹捧它们具备编写安全代码的能力,可以从强大的安全网中获益。

另一方面,动态类型语言在运行时将类型信息嵌入变量的值或者变量本身中。因为没有与该变量相关的类型约束信息,变量在运行时可以轻易地更改类型。动态类型语言的支持者声称它们具备开发所需的灵活性和速度,并且使用动态类型语言编写代码不会总是被编译器的报错打断。

Python是一种动态类型语言。正如在有关机器表达的讨论中所说,变量值中嵌入了类型信息。Python对在运行时更改变量的类型没有任何限制:

不幸的是,很多时候在运行时更改类型是构建健壮代码的障碍。因为这使开发者无法在变量的生命周期内做出确定性的假设。当假设具备不确定性时,很容易基于它们编写出其他不稳定的假设,进而在代码中留下逻辑炸弹。

动态类型语言从根本上就是不健壮的吗?

就像弱类型语言一样,用动态类型语言编写健壮的代码毫无疑问也是可能的。只是需要我们付出更大的努力。开发者必须做出更深思熟虑的决定,才能使代码库更易于维护。另外,静态类型也不能完全保证程序的健壮性,也可能限制了开发者对类型的操作,但其实并没有因此获得多大的收益。

更糟糕的是,我之前展示的类型注解在运行时对变量行为没有影响:

没有错误,没有警告,没有任何东西。但是希望并没有消失,你有很多策略可以使代码更健壮(否则,这将是一本很短的书)。作为健壮代码的开发者,我们将讨论最后一件事,然后开始深入改进我们的代码库。

2.2.3 鸭子类型

每当提到鸭子类型时,一定会有人这样回复:

如果它走路像鸭子,叫起来像鸭子,那它一定是鸭子。

我对这句话的观点是,它对于解释什么是鸭子类型实际上没有任何帮助。它读起来朗朗上口、简洁明了,但是关键在于,只有那些已经了解了鸭子类型的人才能理解这句话。我年轻的时候,只是礼貌地点点头,生怕在这简单的一句话中漏掉了什么深刻的东西。直到后来我才真正领会鸭子类型的力量。

鸭子类型是指在编程语言中只要遵守某些接口就可以使用某个对象或者实体的能力。它在Python语言中是一个很棒的东西,大多数人可能在不知情的情况下使用它。让我们通过一个简单的例子来说明:

在print_items的3个调用中,我们循环遍历集合并打印集合中的每个元素。想想看它是如何工作的。print_items完全不知道它将收到什么类型的参数,只需要在运行时接收一个参数并对其进行操作。它不需要根据参数类型来决定做不同的事情,而是检查传入的参数是否可以迭代(通过调用__iter__方法检测)。如果参数具备__iter__属性,则在循环中调用和返回。

我们可以通过一个简单的代码示例来验证这一点:

这就是鸭子类型赋予我们的能力,只要某个类型支持被调用时函数使用的变量和方法,就可以在该函数中自由使用该类型。

另一个例子如下:

这里不管传入的参数是整数还是字符串都是合法的,因为这两种类型都支持加法操作,所以两者都可以正常工作。所有支持加法运算符的对象都可以被当作合法参数传入,甚至是列表类型:

那么如何利用这个特性为代码的健壮性服务呢?事实证明,鸭子类型是一把双刃剑。它可以提高健壮性的原因在于提高了代码的可组合性(我们将在第17章中了解有关可组合性的更多信息)。建立一个能够处理多种类型的可靠抽象库可以减少处理复杂特殊情况的需要。但是,如果过度使用了鸭子类型,就会开始打破开发人员的支撑性假设。代码更新就不再是仅仅进行更改那么简单,必须查看代码所有的调用点并确保传给函数的参数类型也适用于更新后的代码。

考虑到所有这些,最好将本节前面对鸭子类型的惯用解释改写为:

如果它走路像鸭子,叫起来像鸭子,而你正在寻找像鸭子一样走路和叫的东西,那么你可以把它当作一只鸭子对待。

这不是也非常朗朗上口吗?

讨论

你在你的代码库中使用过鸭子类型吗?是否有一些地方可以传入与代码想要的类型不一致的类型,但代码仍然可以正常工作?你认为这些会增加还是减少你的代码逻辑健壮性?