python的“缺点”

类型系统

python是一个动态类型的语言,它只能进行动态类型检查,所谓动态类型检查,实际就
是说,python只有在运行时会进行类型检查,比如说这样的代码:

1
2
3
4
5
def add1(x):
return x + 1

a = "hello"
add1(a)

运行时python会报错,因为integer类型与string类型是不能相加的,但是这种类型错误
只能在运行到这条代码时才能检查出来,事实上还有一种静态类型检查,它可以不运行
程序就检查出程序的类型错误,这种语言大概可以分为两类:

  1. C/C++,java等,它们的静态检查是在编译源码时完成的,在这种语言中你要对每一
    个变量的类型进行标识,非常的繁琐,这也是为什么很多人觉得python比C/C++这类
    语言用起来更舒服的原因之一。
  2. haskell,ML等,在这种语言中,每一次你运行一个程序时,它会先将程序送入一个
    typechecker中,只有程序通过了typechecker的检查,才能让它运行。这类语言一般
    内部会有一个推导系统,比如说著名的HM类型系统,所以在这种语言中你不需要做特
    别多的类型标识,类型系统本身可以推导出绝大多数类型,所以你可以直接用
    x=1, 而不是 int x=1,因为类型系统可以根据1来推导出x的类型。

静态类型系统的优点主要有以下几点:

  1. 更好的发现错误:静态类型系统可以帮程序员检测出绝大多数低级错误,所以一些
    Haskell程序员说,只要我的代码通过了类型检查,那么它90%的可能是正确的。
  2. 类型本身可以作为文档的一部分,比如说一个函数如果你知道了它的参数类型以及返 回值类型,那么你很可能就可以猜出它的功能。
  3. IDE更好用,一般来说能够进行静态类型检查的语言,它的IDE都更好用,代码跳转,
    重构等功能都非常准确,而python做不到这一点,即便是pyhon所谓最好的IDE
    pycharm我也可以很轻松的让它出错。

静态类型系统配合类型推是个好东西,既能享受静态类型检查带来的好处,又不用做过
多的类型标注,很可惜python中是无法实现这样的类型系统,也就是说无法进行完备的
静态类型检查,因为它太“灵活”, 实际上它的很多灵活性是多余的,因为好的编程风格,
是不推荐你使用这部分特性的。这部分灵活性也使得python的很多语义都与类型系统是
矛盾的。比如说列表,类型系统要求列表必须同质,也就说列表的元素必须是同一类型,
这是符合逻辑的,可是“灵活”的python却允许不同类型的元素放在同一个列表。在比如
说python允许一个变量绑定到不同类型的值,比如下面的代码:

1
2
aa = 1
aa = 'hello'

但是这种做法静态类型系统是不允许的(当然这是所有动态语言的问题 )。还有很多这
样的例子。静态类型系统实际上是限制了程序语言的灵活性以及表达力,但这是值得的,
因为它去掉的基本都是一些不必要的灵活性,而且去掉这些灵活性之后可以让程序更严
谨,实际上现在已经有人在着手向python中添加静态类型,各位可以看看这篇文章
。 有意了解更多类型知识的童鞋可以看看types and programming languages这本书。

缩进

python强制缩进是一个明智的选择,它使得代码的可读性有了质的飞跃,但是它使用空
格这样的不可见字符来标示一个块的结束,我认为不是什么很好的选择。ruby社区有一
种说法:ruby使用end终结一个块是比python使用空格缩进更高级的做法。过去有一段时 间我对这种说法也是嗤之以鼻。但是我现在改变了看法,
使用空格这样的不可见字符来 标示一个块的结束有以下几个问题:

  1. 更容易出错

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    def sum(ls):
    ret = 0
    for i in ls:
    ret += i
    return ret

    # incorrect
    def sum(ls):
    ret = 0
    for i in ls:
    ret += 1
    return ret

    第二个函数仅仅因为多按了一个tab就产生了一个致命的bug,这种简单的例子可能还
    好检查,可是对于复杂的函数这种bug很不好排查。如果有明确的块界定符,比如说
    end,},那么就根本不会有这种低级的问题.

  2. 无法使用编辑器来自动格式化代码, 这是一个很大的问题,当你从网上拷贝一段别人 的代码后,你必须小心翼翼的来确保代码的缩进正确,
    编辑器是无法自动格式化像 python这类语言的代码的, 这不是因为编辑器不够强大,而是本质上不可能实现,所
    以python的代码我们必须手动来格式化,这是很机械的工作,可是因为python的设计
    失误,我们无法把它交给机器来做,曾经我因为写其他语言代码的习惯, 所以在emacs
    中写python代码时总是喜欢运行代码自动格式化的命令,可是每一次都弄得一团糟,
    最后不得不在配置中把python的格式化命令去掉了,
    所以对于python的这种语法虽然 看上去很简洁,但是我不认为是一个好的设计。

  3. 无法实现一个完备的匿名函数,python的缩进方式除了导致第三方工具无法自动格
    式化代码这个问题外,还有一个很大的问题,那就是python无法实现一个功能完备的
    匿名函数。如果你有函数式编程经验,应该能体会匿名函数的重要性,举一个很简单 的例子,比如python的列表解析:

    1
    2
    [i+1 for i in range(10)]
    map(lambda i: i+1, range(10))

    二者实际是等价的,很多人对列表解析推崇有加,可是实际上它不过是个可有可无的
    语法糖,而且还是一个表达能力并不强大的语法糖(我个人并不排斥语法糖,但是你
    要明白语法糖背后隐藏的东西),这应该也是从haskell学来的,缩进语法应该也是,
    我的印象中,python似乎从haskell学了很多蹩脚的东西。如果python能把匿名函数
    做对,和map搭配会比列表解析强大的多。很可惜python的匿名函数就是一个“残废”
    的东西,它内部只能有一个表达式,而不能像正常的函数那样包含多个语句,事实上
    造成这一困境的原因就是python的缩进语法,因为匿名函数一般是作为参数传递给一
    个函数,这样匿名函数的内部就不能换行,一旦换行缩进就乱了,所以python无法实
    现一个完全正确的匿名函数。

变量的定义与赋值

python中不区分变量的定义与赋值,这也是一个设计失误

基本概念

环境(environment)

如果你写过解释器,那么你一定遇到过这个东西,简单的说环境就是用来跟踪在程序 的执行过程中,每一个标识符都绑定到什么值,所以从逻辑上它就是一个
map(identifier <–> value),和python的dict类似。环境要和作用域规则结合起来, 下面来看一个简单的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Scope(object):
def __init__(self, parent=None):
self.table = {}
self.parent = parent

def bind(self, var, val):
if var in self.table:
error("var(%s) have been binded in this environment" % var)
else:
self.table[var] = val

def get(self, var):
val = self.table.get(var, None)
if val is None:
if self.parent is None:
return None
else:
return self.parent.get(var)
else:
return val

def set(self, var, val):
if var in self.table:
self.table[var] = val
elif self.parent is not None:
self.parent.set(var, val)
else:
error("var(%s) is not binded in this environment" % var)

上面的实现可以看出作用域实际上可以看做是一个dict,只是它有一个parent, 这样 就构成了一个链式的dict,可以从下往上搜索.

声明

实际就是在当前变量所在作用域创建一个key/value对, 注意实际上是调用 bind 方 法. 前提是该变量没有在当前作用域中绑定.

赋值

改变作用域中变量的值,如果当前作用域中没有该变量,那么就父作用域中找, 如果该变量不存在那么出错. 实际上该动作就是 set 方法.

作用域

每当进入一个新的作用域,那么解释器就会创建一个scope对象,该对象的 parent指针会指向当前的scope对象,从而构成一个链式的作用域。

python的作用域规则

python有一个内置作用域,这个作用域的所有绑定是在任何地方都可见的。python的内 置函数以及内置异常都是在内置作用域中.
可以使用如下代码来查看内置作用域有哪些 绑定

1
2
import __builtin__
print dir(__builtin__)

除了内置作用域,python在两个地方会创建新的作用域,一个是模块,另一个是函数 (包括方法)

python的设计失误

python有一个巨大的设计失误就是没有明确的区分变量的 声明赋值,也就是说
python的”=”有两种语义:当前作用域不存在该变量时,它会在作用域中创建该绑定,如果
已存在,那么它就会修改该绑定的值,这种混淆的行为严重的损伤了语言的表达能力,同时 也让程序更容易出错,因为变量会自动创建,
一个打字错误可能就会造成bug,而在需要明
确声明变量的语言中,这种错误是可以被编译器或者解释器检测出来的。同时python的这种
设计也让程序不可能自然的修改父作用域,父父作用域..的变量,因为你一旦使用“=”那
么就会在当前作用域创建新变量,所以python不得不引入global这样丑陋的关键词,然而这
只能访问到最外层的全局作用域,处于中间的作用域它仍然无能为力。下面举个例子,比如
我们要实现这样一个函数,该函数第一次调用返回1,第二次调用返回2,第n次调用返回n。如
果用C语言实现我们可以使用静态变量,如果我们使用javascript实现呢?可能需要像下面 这样的代码:

1
2
3
4
5
6
7
8
var succ = (function(){
var i = 0;
var inner = function(){
i++;
return i;
}
return inner;
})()

那我们如何用python来实现呢?你可能会很自然的写出这样的代码:

1
2
3
4
5
6
7
8
9
def temp():
i = 0

def inner():
i = i + 1
return i
return inner

succ = temp()

这个代码和上面的代码逻辑是一样的,但是却达不到我们预期的效果,如果你运行的话 就会发现,它会出错(UnboundLocalError:
local variable ‘i’ referenced before assignment). 这其中有一些很细微的区别.
在inner中i=i+1会在当前作用域创建一个 绑定,这个绑定在该语句之前也是可见的, 所以在求值 i+1
这个表达式时,python 不会去外层作用域中查找, 它引用local作用域中的i,而你又是在赋值之前引用,所
以会报错。

GIL

这是python被广为诟病的一个东西,与其说它是语言本身的问题,还不如说它是官方的C
语言实现的问题,JPython以及pypy都没有这个问题。简单的说GIL是一个全局锁,GIL的
存在简化了解释器的实现,但是代价是python的多线程变成了“伪多线程”,也就是说
python的多线程程序任一时刻只能运行一个单一的线程,它无法有效的运用多核CPU的优
势。因此对于使用python编写的CPU密集型的程序,你不要指望通过多线程来提高性能,
多线程只会让这类程序的性能变得更糟。IO密集型的程序影响应该是不大的,这也是为什
么tornado这样的库能够工作的很好的原因。所以很多时候为了利用多核,你必须使用多
进程,而多进程之间共享数据的代价是比多线程高的多的,也麻烦的多。而且对于分布式
系统GIL已经变成了一个比较大的问题,只是因为python语言的特点,所以大多数时候都不
会将其作为分布式系统的实现语言,所以在分布式系统领域还不是什么大问题。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!