Python的静态类型之旅

5,078 阅读5分钟

本文系掘金Python月专题文章,转载请注明来源

江湖有句话:“动态类型一时爽,代码重构火葬场”被广为流传,这话一般出自静态语言拥护者口中,听起来有点耸人听闻,但也没有想象中的那么严重,Python在大型项目的应用太多了,Instagram就是最好的例子。

Python作为动态语言,在定义变量、函数返回值、方法参数都不需要指定数据类型,某种程度上让代码变得无比简洁、灵活,抛开程序运行效率,但动态语言也存在不足,例如:

1、IDE的智能提示比较鸡肋,因为无法判断变量类型,所以IDE就不知道变量有那些属性和方法,没有智能提示对老鸟来说是非常痛苦的,举个简单例子,印象中str有个startwith方法,但正确的写法是 startswith,有个s,我不得不去查个文档。(不过个人建议初学者还是老老实实用编辑器手敲代码,以此加深记忆)

2、编译过程中,只能发现语法错误,类型不匹配的问题只有等程序真正运行了才知道。虽然可以通过单元测试来规避,但是如果在代码编写的过程中有IDE给我们指出来不是更好吗。

3、接口调用全靠文档注释说明,调用某个方法或函数,返回值和参数类型说明只能根据文档来确定。虽然我们可以要求程序员使用docstring或者注释来说明函数的参数类型以及返回值类型,有个问题是即使一开始你规规矩矩的写了docstring,但是代码更新之后,你的docstring可能就没有同步更新。

这些问题在大型项目,特别是多人合作的项目上显得尤为突出。代码规范、Code Review 就变得更重要了。正因为这些问题,社区对静态类型特性的引进呼声越来越强烈,所以在 Python3.5,也就是 PEP484 中有了类型提示(Type Hints)。定义函数时,可以指定函数的返回值类型、参数的类型。

以前写一个函数是这样的:

def greeting(name):
    return "Hello" + name

>>> greeting("bob")
'Hellobob'
>>> greeting(1)
TypeError: must be str, not int

当你不去看文档或者源代码的时候,你根本不知道你可以传递什么类型的值进去,而当你传入整数1时,只有等到程序运行的时候才能发现错误,如果有一种数据类型检查工具在程序启动前事先查一遍就可以避免程序出错。

在Python3.5中,用 Type Hint 的写法是这样的:

def greeting(name: str) -> str:
    return 'Hello ' + name

上面就是静态类型的写法,多了 「: str」与 「-> str」,前者用来说明 name 的类型,后者指函数返回值的类型。这样一来,IDE像PyCharm这样的工具也能即时的发现代码的问题。

当然,除了IDE之外,我们还有更强大的静态类型检查工具,叫 mypy,这个工具也是由Python之父GUido亲自操刀实现的静态类型检查工具。

pip install mypy

$ mypy test.py
test.py:4: error: Argument 1 to "greeting" has incompatible type "int"; expected "str"

有了类型提示,Python在代码调用、重构、甚至是静态分析等方面有了更好的效果,不但减轻了开发时自行进行型态检查的负担,更重要的是,由于有了型态上的提示,让过去Python整合开发工具上做不好的各种智能提示、重构等功能有了统一的参考标准。

某种意义上类型提示只是一种辅助功能,虽然我们加了数据类型提示,但是对于Python解释器来说,它会直接忽略掉类型提示信息,即时类型有误也不会阻止程序的运行。

而对于变量的类型,在PEP484中可以通过类型注释来说明,就是以注释的方式来说明变量的类型,例如:

from typing import List

x = []                # type: List[Employee]
y = [1, 2]            # type: List[int]
y.append("a")

上面类型注释表示x必须是 Employee 对象组成的列表,y必须是 int 构成的列表,整数列表y追加一个字符串后,我们用 mypy 来检查代码有什么问题:

mypy test.py
xx.py:3: error: Name 'Employee' is not defined
xx.py:5: error: Argument 1 to "append" of "list" has incompatible type "str"; expected "int"

在 Python3.6,也就是 PEP526 的提案中,针对变量注解做了进一步优化,将类型的声明作为了语法的一部分,这样比起注释可读性更强,例如:

my_var: int  # 声明为整数类型的变量
my_var = 5  # 通过类型检查
other_var: int = 'a'  # 给整数类型变量赋值字符串,检查器会报错,但是解释器运行是不会有任何问题
print(other_var)
mypy xx.py  # 运行类型检查器
xx.py:3: error: Incompatible types in assignment (expression has type "str", variable has type "int")

python test.py # 运行解释器
a

类型提示虽然给了IDE智能提示、重构带来了很大的便利,而恰恰因为这些类型信息的声明,使得动态语言变得臃肿起来,例如:

T = TypeVar('T')
S = TypeVar('S')
class Foo(Generic[T]):
    def method(self, x: T, y: S) -> S:
    # Body

这是一段有泛型的注解,看起来跟Java代码没什么区别了。而讽刺的是,Java也开始加入了动态语言的特性,例如在Java10中,就有本地变量类型推断特性,可以使用关键字 var 来定义变量,而不需要指定数据类型,意味着,静态语言也开始往动态语言特性方面发展。

public class VarTest {

    public static void main(String[] args) {
        var name = "java";
        System.out.println(name);
    }
}

那么到底是静态语言好还是动态语言好,Java和Python各自作为静态语言和动态语言的代表,一个明显的特点就是都在互相借鉴彼此的优点,所谓天下大势,分久必合,合久必分。没有一种语言是完美的,Python灵活但可控性没那么强,更像是一位开放的家长,在语言的处理上给开发者最大的自由。毕竟 We are all consenting adults! 而反观Java,就像一位严苛的家长一样,小心翼翼地对待每个开发者,生怕你闯祸。