编程语言本身像一个集市,其中的类、函数、方法、变量等就是市场的参与者,他们无时无刻不在协作交流。我会的语言有限,就借 C、C++ 和 Python 谈谈编程范式的源流。为了讲清楚这个问题,我从 C 语言开始谈起。C 语言历史悠久,几乎所有现在流行的编程语言都是以 C 为基础拓展开来的,C++、Java、C#、Go、Python 等等,不一而足。
凡语言皆有特性,可以说 “表述方式最能限制内容”,C 可以让编写者在底层和系统细节上非常自由、灵活和精准地控制代码。然而,这些局部的优点在组织语言和实现功能上,却不那么美妙。用 C 交换两个变量,写一个 swap 函数。
void swap(int* x, int* y)
{
int temp = *x;
*x = *y;
*y = tmp;
}
我们常说 C 指针能直接操作内存数据,灵活自由,但这个函数有个最大的问题,就是它只能给 int 值用,其他很多类型 char、double、char[] 等等都有交换的需求,这可能是静态语言最糟糕的一个问题。
要做到抽象,对于 C 语言这样的类型语言来说,第一就是要抽象类型,这就是泛型编程。
C 本身提供了 void 类型,解决的结果是费力不讨好,此话怎讲?第一是费力,就是说你得始终记得一个类型的步长,第二是不讨好,你记得也不见得就不会出现编译错误。
C++ 随后提供了重载,模板来抽象,模板中的容器抽象了数据类型,迭代器抽象了算法,具体细节可以通过 C++ 的交换代码和 Python 的比:
C++,太长了,算了……
Python,一行:
b, a = a, b;
你说,人生苦短,还是学 Py……
如前所述,类型带来的问题就是我们要针对不同类型写不太相同的代码,虽然长得非常相似,但是由于类型的问题需要根据不同版本写出不同的算法,如果要做到泛型,就需要涉及比较底层的玩法。
对此,这个世界出现了两类语言,一类是静态类型语言,如 C、C++、Java,一种是动态类型语言,如 Python、PHP、JavaScript 等。
Python中常出现这样一类写法:
x = 5;
x = "hello";
这个例子中,变量 x 一开始好像是整型,之后又用作字符串型。在 C 类的语言中这是不可想象的,但在动态语言中,这是作为特性而使用的。在动态类型的语言中,会以类型标记维持程序所有数值的“标记”,并在运算任何数值之前检查标记。所以,一个变量的类型是由运行时的解释器来动态标记的,这样就可以动态地和底层的计算机指令或内存布局对应起来。不禁让我想起偏微分方程的解法,老师曾说,等式本身就包含信息。
再比如,在动态类型的语言中,下面的代码属于结果未定型。
x = 5;
y = "37";
z = x + y;
像 Python 这样贴心的语言则会直接产生运行时错误。所以,要清楚地知道,无论哪种语言,都免不了一个特定的类型系统。哪怕是可随意适应新变量类型的动态语言,在读代码的过程中也需要时刻脑补变量在运行时的类型。
由此可知,每个语言都有一个类型检查系统。动态语言更多的是在运行时做动态类型标记相关检查。所以,动态类型的语言自带一堆诸如:is_array(), is_int(), is_string() 或是 typeof() 这样的运行时类型检查函数。
“类型”有时候是一个有用的事,有时候又是一件很讨厌的事情。因为类型是对底层内存布局的一个抽象,会让我们的代码要关注于这些非业务逻辑上的东西。而且,我们的代码需要在不同类型的数据间做处理。但是如果程序语言类型检查得过于严格,那么,我们写出来的代码就不能那么随意。所以,对于静态类型的语言也开了些“小后门”:比如,类型转换,还有C++、Java 运行时期的类型测试。
编程这件事更多的是为了解决业务问题,而不是计算机自身的问题,使用者需要更贴近业务更为抽象的语言,如面向对象语言 C++ 和 Java 等。如何做到更为抽象的泛型呢?答案就是函数式编程。以函数式编程的精髓递归为例。
递归最大的好处就简化代码,它可以把一个复杂的问题用很简单的代码描述出来。注意:递归的精髓是描述问题,举个例子,有 3 辆车比赛,每辆车有 70% 的概率可以往前走一步,一共有 5 次机会,要求输出每一次这 3 辆车的状态。
from random import random
time = 5
car_positions = [1, 1, 1]
while time:
time -= 1
print ''
for i in range(len(car_positions)):
if random() > 0.3:
car_positions[i] += 1
print '-' * car_positions[i]
你会发现,封装成函数后,这些函数都会依赖于共享的变量来同步其状态。于是,在读代码的过程中,每当我们进入到函数里,读到访问了一个外部的变量时,我们马上要去查看这个变量的上下文,然后还要在大脑里推演这个变量的状态, 才能知道程序的真正逻辑。也就是说,这些函数间必需知道其它函数是怎么修改它们之间的共享变量的,所以,这些函数是有状态的。有状态并不是一件很好的事情,无论是对代码重用,还是对代码的并行来说,都是有副作用的。
Python 的 Decorator 和 Generator功能,将多个函数组合成了管道。Decorator 在使用上和 Java 的 Annotation(以及 C# 的 Attribute)很相似,就是在方法名前面加一个 @XXX 注解来为这个方法装饰一些东西。但是,Java/C# 的 Annotation 很让人望而却步,太过于复杂了。要玩,需要先了解一堆 Annotation 类库文档,感觉几乎就是在学一门新语言。
Python 使用了一种相对来说非常优雅的方法,这种方法不需要你去掌握什么复杂的 OO 模型或是 Annotation 的各种类库规定,完全就是语言层面的玩法:一种函数式编程的技巧。看一个 Python 修饰器的 Hello World 代码。
def hello(fn):
def wrapper():
print "hello, %s" % fn.__name__
fn()
print "goodbye, %s" % fn.__name__
return wrapper
@hello def Stephen():
print " I am Stephen. "
Stephen()
执行结果如下:
hello, Stephen
I am Stephen.
goodbye, Stephen
有意思么?你说。
其实呢,只能说,有时候体会不方便有助于体会方便。
我们知道,代码中还是需要处理数据的,这些就是所谓的“状态”,函数式编程需要我们写出无状态的代码。
但是天下并不存在没有状态没有数据的代码,如果函数式编程不处理状态这些东西,那么,状态会放在什么地方呢?总是需要一个地方放这些数据的。对于状态和数据的处理,有必要提一下“面向对象编程”。
记得上大学那会儿,学生中曾经流行过一本书,叫《面向对象程序设计》。这本书晦涩难懂到什么程度呢?就是看完这本书,我觉得我可能不是搞计算机这方面的料。
书里介绍了面向对象程序设计的各种概念。封装啊、继承啊、多态啊什么的,讲得头头是道。选择的编程语言也偏正统:C++。可惜啊,无论我怎样认真学习,还是搞不明白为什么要发明这么些个概念。编个程,为什么要面向什么“对象”?后来我上机多了,逐渐明白,其实学编程有点像学琴,光看书真没什么用,需要动手独立完成一些功能才能真正学到东西。
我的体会,设计“类”的好处是能和现实世界对应起来。类是可以重用的。这个模式也表现了面向对象的一个特点——喜欢组合,厌恶继承。比如:时间类、地点类、事件类、人物类的组合几乎可以描述一切业务的属性,配套各种类型的操作。
和函数式编程比较,函数式强调动词,而面向对象强调名词,面向对象更多的关注接口间的关系。
编程范式千千万,作为应用者来说,Python 的优点相对是丰富的,所以,重点关注泛型和对象是极有好处的。
领取专属 10元无门槛券
私享最新 技术干货