如何处理Python中的静态类型?
我来自Java的世界,想知道除了在编译代码时不会出现错误之外,Python的动态类型有什么特别好的地方?
你喜欢Python的类型系统吗?有没有什么例子能说明它在大型项目中帮了大忙?这样是不是有点容易出错呢?
7 个回答
你喜欢在Python中使用它吗?
这就是Python的一部分。在Python中喜欢它其实有点傻。
你有没有一个例子,说明它在大型项目中帮助过你?
有的。每天我都很高兴,因为我可以进行修改,而由于鸭子类型(Duck typing),这些修改都是相对局部的,所有单元测试和集成测试都能通过,并且不会影响到其他地方。
如果这是在Java中,修改就需要无休止地重构,把接口从类中提取出来,这样我才能引入在Java的静态类型检查下仍然被允许的变化。
这样不容易出错吗?
其实并没有比静态类型更容易出错。一个简单的单元测试就能确认这些对象符合预期的特性。
在Java中,写一个类(a)能通过编译时检查,且(b)在运行时却崩溃是很简单的。类型转换就是一个常见的原因。未能满足类的意图是很常见的情况——一个类可能编译通过,但仍然无法正常工作。
我怀疑大多数复杂的Java程序中都有动态类型的特性。
每次在Java中把一个对象从Object类型转换成其他具体类型时,你都在进行动态类型检查。这包括在1.5版本之前使用集合类的每一次操作。实际上,Java的泛型在某些情况下仍然会在运行时进行类型检查。
每次使用Java反射时,你也在进行动态类型检查。这包括从文本文件中的类名或方法名映射到真实的类或方法,比如每次使用Spring的XML配置文件时。
这是否让Java程序变得脆弱且容易出错?Java程序员是否花费大量时间去追踪和修复动态类型错误的问题?可能不是,Python程序员也是如此。
动态类型的一些优点:
- 大大减少了对继承的依赖。我见过一些Java程序有庞大的继承树,而Python程序通常几乎不使用继承,更倾向于使用鸭子类型(即只关心对象的行为,而不是对象的类型)。
- 编写真正通用的代码变得简单。例如,min()和max()函数可以接受任何可比较类型的序列——整数、字符串、浮点数、具有适当比较方法的类、列表、元组等等。
- 代码量更少。很多Java代码并没有真正解决问题,反而只是为了让类型系统正常工作。如果一个Python程序的大小只有相应Java程序的五分之一,那么你需要编写、维护、阅读和理解的代码量也只有五分之一。换句话说,Python的有效信息与无效信息的比例要高得多。
- 开发周期更快。这与代码量少是相辅相成的——你花更少的时间去考虑类型和类,而更多的时间去思考如何解决你正在处理的问题。
- 对面向切面编程(AOP)的需求很小。我知道Python有一些面向切面编程的库,但我不认识使用它们的人,因为在99%的情况下,你可以通过装饰器和动态对象修改来实现AOP所需的功能。在Java世界中广泛使用AspectJ让我觉得Java语言本身存在一些不足,需要借助外部工具来弥补。
静态类型检查在一般情况下是无法决定的。这意味着有些程序在静态类型上是安全的,但类型检查器却无法证明它们是安全的,因此类型检查器必须拒绝这些程序。
换句话说:有些类型安全的程序,类型检查器不会让你写。更简单地说:静态类型限制了你写某些程序的能力。
这适用于所有的静态类型,不仅仅是Java。
具体到Java来说,它的类型系统相当糟糕。它的类型系统连非常简单的属性都无法表达。例如:在static void java.util.Arrays.sort(Object[] a)
的类型中,哪里说明结果必须是排序好的呢?或者说数组元素必须是部分有序的呢?
Java的另一个问题是,它的类型系统有一些大漏洞,简直可以开卡车通过:
String[] a = new String[1];
Object[] b = a;
b[0] = 1; // ArrayStoreException
在这个特定的例子中,问题出在协变数组上。数组不可能既是协变的又是类型安全的。
Java把静态类型的所有麻烦都结合在一起,却没有任何好处。所以,你不如直接去掉这些麻烦。
不过,值得注意的是,这并不是普遍适用的。还有其他语言拥有更好的类型系统,权衡的利弊也不那么明显。
例如,这里是用Python写的最简单的语言基准(斐波那契):
def fib(n):
if n < 2: return n
return fib(n-2) + fib(n-1)
还有Java的版本:
int fib(int n) {
if (n < 2) return n;
return fib(n-2) + fib(n-1);
}
注意,这里有更多的杂乱,完全是因为静态类型的原因。为了让比较更公平,我们假设有一种语言,它的语法像Python,但语义像Java:
def fib(n: int) -> int:
if n < 2: return n
return fib(n-2) + fib(n-1)
[有趣的旁注:在Python 3.x中加入了可选的静态类型注解,这实际上也是有效的Python代码,尽管显然它仍然不是静态类型安全的,因为这些注解只是注解。它们从来没有被实际检查过。]
确实,这里有一些杂乱。然而,在Haskell中,它看起来是这样的:
fib n
| n < 2 = n
| otherwise = fib (n-2) + fib (n-1)
与Python版本不同,这个是完全静态类型安全的,但没有任何与类型相关的杂乱。
在这个特定的情况下,静态类型和动态类型的好处之间的区别就不那么明显了。
顺便说一下,更符合Haskell风格的版本可能是这样的:
fib 0 = 0
fib 1 = 1
fib n = fib (n-2) + fib (n-1)
或者这样:
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
实际上,Java和Python之间更重要的区别并不是Java是静态类型而Python是动态类型,而是Java根本不是一个好的编程语言,而Python是。所以,Java总是会输,不是因为它是静态类型,而是因为它很糟糕。比较BASIC和Haskell,Haskell显然胜出,但同样,不是因为它是静态类型,而是因为BASIC很糟糕。
一个更有趣的比较是Java与BASIC,或者Python与Haskell。