计算机构造与解释Python版读后感

计算机构造与解释Python版读后感

是上个月看完 Structure and Interpretation of Computer Programs的Python版本。前天又花了一天时间回顾。真是大开眼界,给你一种完全不同的视角看待程序语言,正如书中所言的magic,art,比如我们都知道program是由procedure和data构成,但我从没想过procedure就是data,data就是procedure。就Python入门而言,对一个C语言程序经验者,这本书非常难懂,难懂在于它的讲述方式跟传统的填鸭式介绍控制语言,数据结构…完全不同,但我反过来想如果对于初心者而言,或许就没有别样的难。当然本书与其说讲解Python,还不如讲是一种通用的程序语言概念,最核心的无非就是抽象和递归,只是拿Python举例了。

第一章讲procedure。程序语言中最基本的概念是什么?表达式(最基础的就是数值本身)和 表达式的绑定。在Python中,在一定范围内的自然数,都是有独立地址的,比如

a = 5,b = 5

a和b地址就相同,所以相对于C,=更确切讲是绑定而不是赋值,=提供了最基础的抽象手段。然后是调用表达式(数值+运算符)。运算符本质上就是函数,这里就引出了第一章的主题,函数。进一步稍微复杂一点,函数可以嵌套,提供了组合操作手段。函数由什么组成?语句。表达式、返回语句return和赋值语句等是最简单语句,跟C相较,因为Python可以是屏幕上交互式语言,所以像123这样单独数值的表达式就是一条独立的语句,逗号在赋值语句中分隔了多个名称和值,比如

a,b=1,2

(这本书很多东西都讲到C基础Python语言学习者的盲点上,有时候正是受困于既定思维,所以阅读此书时方才更为困难)。简单语句其基础上有def(定义),if(条件控制),while(重复)等复合语句。Python 的代码是语句的序列(语句的序列实质上也是个first+rest的递归结构)。一条简单的语句是一行不以分号结束的代码,复合语句一般占据多行,并且以一行以冒号结尾的头部开始,它标识了语句的类型。同时,一个头部和一组缩进的代码叫做子句(借助简单复合语句的概念,去理解Python与C书写格式上的异同)。函数作为一个整体可以作为参数,返回值,跟最基本的表达式一样是语言的一等元素,可以被嵌套定义,高阶函数可以像操作数据一样去操作函数,这里就可以理解为什么procedure是data。Python中对一些没有赋值和控制语句单个返回表达式的函数可以用Lambda 表达式,有点类似于C中的difine,但进一步具有匿名性(即不存在函数名)。此外,还顺道讲了一个语法糖:装饰器,结构上非常费解然而又精巧,用于不修改高阶函数原有代码的前提下对高阶函数功能追加。

第二章讲data。Python 包含了三个原始数值类型:整数(int)、实数(float)和复数(complex)。之后所有的抽象数据类型都是基于此构建起来,最终形成一个Python大厦。首先书中通过有理数的构造过程,来展示抽象的层次。表现为几分之几的有理数数据结构由二元元组(偶对的形式)来实现,而偶对的下标访问方式以及有理数数据结构的运算都能以函数操作原始数值类型来实现(从这个角度上看,偶对或者所构造的有理数就是一个函数,这就说明了为什么data是procedure)。偶对到多元元组就一回事。

这个节点,书中引入了nonlocal语句,我们知道函数也是一种数据类型,书中特地分了纯函数和非纯函数,它们之间最本质的区别就是是否可变。前者只要特定的参数,return肯定是不变的,后者假若引进nonlocal变量,return值就是可变的。这里隐含了一个赋值语句的双重作用:创建新的绑定,或者重新绑定现有名称。为了去补救重新绑定的操作引起的认识论问题:它对于两个相同的值意味着什么。(如果清楚C中指针的概念的话,其实就很好理解)。对于不可变类型,值相同对象就相同,对于可变类型就不尽然。所以Python 引入了两个比较运算符,叫做isis not,测试了两个表达式实际上是否求值为同一个对象,is和是个比相等性==更强的比较运算符。

偶对可以嵌套,满足封闭性(封闭性在任何组合手段中都是核心能力),去实现

(1, (2, (3, (4, None))))

这样的嵌套结构。这个嵌套的结构通常对应了一种非常实用的序列思考方式,一个非空序列可以划分为:它的第一个元素,以及序列的其余部分。这说明了列表是可以基于偶对递归实现,并用函数实现元素的变更(ps Python 的内建list序列类型以不同方式实现)。(传统Python介绍中先介绍list,再是tuple,然后要去解释为什么有了list后还要创造tuple?因为list基于tuple基础上实现嘛,由此可见本书编排精妙之一斑。)

字典的目的是提供一种抽象,用于储存和获取下标不是连续整数,而是描述性的键的值。所以字典实现上可以通过列表改造而来。至此就完成了从偶对到字典的主要序列容器的构建。(书中还顺手组合了非局部赋值、列表和字典来构建一个基于约束的系统,支持多个方向上的计算。这真是开了我的眼界,理解连接器和约束,把它们看成key为函数名,value为函数过程去理解,最新颖在于这个这个基于约束网络特征的无方向计算。)

我们知道序列的许多操作都是基于迭代的概念,这里就可以引进迭代器。有序序列其实隐含着迭代器接口__iter____next__,Python 拥有额外的控制语句来处理序列数据:for语句。

>>> counts = [1, 2, 3]
>>> for item in counts:
        print(item)
1
2
3

如上例,counts可以通过__iter__回应给for一个迭代器,然后for反复调用__next__来实现对序列的遍历。可以通过while显式地配合__next__来实现for语句。程序中的一个常见模式是,序列的元素本身就是序列,for语句可在头部中包含多个名称,将每个元素序列“解构”为各个元素。这个绑定多个名称到定长序列中多个值的模式,叫做序列解构。它的模式和我们在赋值语句中看到的,将多个名称绑定到多个值的模式相同。如果不需要解构序列元素,常见的惯例是将单下划线字符用于for头部,要注意对解释器来说,下划线只是另一个名称,但是在程序员中具有固定含义,它表明这个名称不应出现在任何表达式中。(这本书说明Python完全是另一套思维,不是既存的for去操作序列,而是序列的某些特性需要引入for语,而且说for语法的每一个点都说到心坎上去了,相较于C,Python中for的改造在哪里?妙不可言!其实深入思考一下,C语言中whilefor在汇编层面是一样的,但Python中不然,就因为这个迭代器概念。)

for语句常跟range配合使用,如

>>> for _ in range(3):
        print('Go Bears!')

Go Bears!
Go Bears!
Go Bears!

range事实上是一种隐式序列,迭代器提供了一种机制,可以依次计算序列中的每个值,但是所有元素不需要连续储存。反之,当下个元素从迭代器获取的时候,这个元素会按照请求计算,而不是从现有的内存来源中获取。这是惰性计算的一个例子。计算机科学将惰性作为一种重要的计算工具加以赞扬。

生成器表达式组合了过滤filter和映射map的概念,并集成于单一的表达式中,以下面的形式:

<map expression> for <name> in <sequence expression> if <filter expression>

表达式包含了mapfilter的大部分功能,但是避免了被调用函数的实际创建,实际上也是一种惰性计算。

生成器函数是一种特殊的迭代器,不同于普通的函数,因为它不在函数体中包含return语句,而是使用yield语句来返回序列中的元素。yieldreturn的区别在于,它返回值出去之后仍然可以在当前位置继续执行下去,利用这点可以去构建Python中一种轻量级的线程,即协程(coroutine)。

流提供了一种隐式表示有序数据的最终方式,流是惰性计算的递归列表,就像之前自偶对嵌套构建而来的list那样,Stream类实例化后可以响应对其第一个元素和剩余部分的获取请求,同样,Stream的剩余部分还是Stream,然而不像list,流的剩余部分只在查找时被计算,而不是事先存储,也就是说流的剩余部分是惰性计算的。(惭愧惭愧,C++中多次接触流,但始终没有理解其本质)。

讲流时提到了对象,而对象正是基本数据抽象最上层的概念,事实上可以通过在实例、类和基类之间发送含有属性的字典作为消息来实现对象系统,只是没法去实现点运算符,书中讲点运算符是 Python 的语法特征,它形成了消息传递的隐喻。(类与方法之间的点可以视作为一个运算符。其实我这个浸淫C多年的coder刚看Python就对这个点运算符比较懵逼。)点运算符隐式地把方法的第一个参数设为了self。可以在pycharm中使用debugger来追踪书中所列出来的例子看一下类与对象是如何建立起来的。先创建类,从外层的类到基类,调用层层深入进去,然后实例化对象,从基类开始往外依此调用初始化函数最终抵达实例。比喻来说,创建时先一层层进去拿到最核心的字典,即基类的字典,然后实例化时在从这个核一层层出来,包裹各层次之间的颜料,最后形成实例。每一个层次的字典(可以理解为方法集)都不一样,就可以理解类与对象各自有相应的方法,某种程度上类似于类与基类,从而也能理解各个层次间方法重载的机制,先是调用实例中的方法,如果找不到,就去类中寻找,并绑定到实例中执行。(另一方面我们也可以理解为什么C的编译器可以用C来编写,因为就如同Python的某些机制如类与对象可以由更为低层次的抽象概念字典与函数来实现一样,语言的建构是一个不断抽象累积的过程。)

第三章实现了一个解释器,前面两章分别讲了编程的两个基本元素procedure和data,事实上,procedure就是data这一点,也可以从用户的程序即是解释器的数据的角度来理解。本章最后有介绍Lsip的两种方言Scheme和Logo的解释器构造:数据求值粗略地来说可以得到基本表达式和函数,这个函数就需要调用过程,而过程中去读取每一行的代码有需要递归去调用数据求值来确定值,从而数据求值与过程互相递归,最终停止于基本表达式。

关于这两种抽象语言的解释器框架,我确实也只一知半解,但能够意识到其核心就是递归:可以通过C写一个Fibonacci数列的递归调用,从汇编层面去理解递归函数:函数前半段不断重复进去递归函数内层,然后后半段不断重复出来到最外层。书中说我们不应该关心fun(n-1)如何在fun的函数体中如何实现,只需要相信它计算了n-1的阶乘,将递归调用看做函数抽象叫做递归的“信仰飞跃”(leap of faith),这说得真好!递归与迭代思考方式上是截然不同的,通常,迭代函数必须维护一些局部状态,它们会在计算过程中改变。在任何迭代的时间点上,状态刻画了已完成的结果,以及未完成的工作总量。

书中以较抽象语言更为简单的计算器语言的解释器构造为例来抛砖引玉,介绍之前先需介绍两个概念:表达式树和字符串。就像第二章介绍的递归列表,树同样可以通过偶对嵌套的封闭性来构造,这里引入Python除tuple,list,dictionary之外第四种容器set来说明树的高效性,set可以由无序集合,有序集合和平衡二叉树集合来实现,它们对于集合的交并等计算而言,效率差别很大。(这些事实上是算法和数据结构的内容,加上编译原理,本书内容其实非常丰富。)

然后是字符串,字符串满足基本的序列容器条件,数据值的字符串表示在类似 Python 的交互式语言中尤其重要,其中“读取-求值-打印”的循环需要每个值都拥有某种字符串表示形式。Python中主要有两种字符串构造函数strrepr,略有区别。与之紧密相关的概念是接口,响应__repr____str__特殊方法的对象都实现了通用的接口,它们可以表示为字符串,这某种程度上是函数多态的一种表现。(JAVA中抽象类ADT与接口的区别不是常提的嘛)。

回到计算器语言的构造,仍然是可以用pycharm中的debugger来看书中的例子,其基础是构建一个表达式树的类来作为计算器语言的基本对象,比如add(1,2),由operator和operand构成(注意这个类本身可以递归表示为operand,形成复合计算式)。在“读取-求值-打印”循环的交互模式中(一个loop函数),利用词法分析器lexical analysis切割所输入的字符串(代码文本)产生一个token列表,然后利用语法分析器parse去生成表达式树(这里要构建一个递归逻辑去实现复合计算式的实现),中间通过异常机制健壮语法错误检查,最终将表达式树输入计算逻辑函数即semantic analysis得出结果,即输入符合语法的字符串(代码文本)被相应执行。(就C编译器而言,相对Python解释器,多optimization和code generation两个步骤,但前三步原理一样,这比看编译原理简单明了多了!)

之前有看过Lisp版本SICP的英文视频,就是完全云里雾里,也看过浙大python的教学视频,感觉就填鸭死记硬背,等到看了这本Python版本的SICP,非常惊喜,回味起来就感慨这本书的编排之美丽,果真是magic,art。明白别人所讲的:永远不要做一个不知道递归和抽象概念的程序员。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值