编程范式——《像计算机科学家一样思考》读书笔记(上)

这是我关于《如何像计算机科学家一样思考》一书的体会和总结。此书的副标题叫做《Think Python》,作者是Allen.B.Downey,欧林计算机学院计算机科学教授,MIT计算机科学学士和硕士,UCB计算机科学博士。作者本身写作此书的原因是用来讲解的语言是java入门知识,其目标是:简短、循序渐进、专注于编程而非语言。这本书里出现的编程知识基本上是所有语言所共用的,因此用来做一个程序学习之架构是非常合适,这也是本文希望做的——在这本书的基础上建立一个学习所有编程语言的基本框架。

说实话,这是我学习Python所使用的第一本书,也是在Python学习过程中重新回过头来仔细阅读第二遍的第一本书。这本书在我刚开始接触编程的时候教给了我很多东西,比如手脚架代码,比如增量开发。我经历了数百个小时的Python学习、实践和编程,这很受用,但是,我发现现在的自己并不能够随口讲出来编程的核心和本质,或者说,我脑袋中的Python经验并没有一个可以提取的结构化的范式。因此,我重拾了这本书,并希望在之后学习C++、JS、Matlab的使用过程中能够有所启发,不必过于慌张和错失条理,可以最大化的利用自己学习Python的经验和范式架构。

注意:本文只能够提供给你一个逻辑清晰的语言架构,按照这样的顺序进行组织:变量、类型、操作符、语句、表达式、条件循环与迭代判断、函数、高级结构、面向对象编程。对于类型和结构而言,按照其基本顺序进行介绍:定义、迭代和分片、元素的选择、增,删,改、和其他类型的转换以及一些高级用法和陷阱。

计算机科学家的最重要的技能是解决问题。编程本身是一个非常有用的技能,另外,你可以使用编程作为工具,达到更高的目标。

1、综述

一言以蔽之,对于所有的计算机语言,其主要结构框架如下:

- 输入:从键盘、文件或者其他设备中获取数据

- 输出:将数据显示到屏幕或者其他文件/设备中

- 数学:基本的数学操作

- 条件执行:检查某种条件的状态,并执行相应的代码

- 重复:重复执行某个动作,往往在重复中有一些变化。

2、形式语言和自然语言

程序采用高密度低歧义的字面直接语言,其意义可以完全通过其记号和结构来理解。对于阅读程序而言,不一定要从头读到尾,最重要的是在头脑中解析顺序,辨别记号和结构,最后,细节很重要,任何拼写和标点错误,影响往往很大。 B 程序的基本组成元素

2.1 变量、表达式和语句

一个程序

a
a = 1
type(a)
b = (a + 2) * 4
b

2.1.1 值和类型

2.1.2 变量

指向一个值得名称,编程语言最强大的功能之一就是操纵变量的能力。赋值语句可以建立新的变量,比如 a=1,其中a为变量,1为值,1的类型为数字,等号包括两边的内容构成了赋值语句。

变量有提前赋值的要求,如果没有一个值,则无法构成一个变量。(Python)

2.1.3 Python变量名称规则

最好使用小写字母,不要使用数字和特殊符号,可以使用下划线,不能使用空格和保留关键字。

2.1.4 操作符、对象、顺序

数字操作符:+ - * ** / 加减乘除为操作符。其有特定的顺序,比如括号最高优先,乘方高于乘法高于加减。顺序相同从左到右进行。

逻辑操作符:大于 >=、小于 <=、等于 ==、不等 !=

**注意:这里的==是等于操作符而不是赋值操作= **。

布尔操作符:True、False、

求模操作符:%

判断操作符:in(在...之中),可使用于字典、列表和字符串等

if var in list/dict/string:pass

2.1.5 表达式和语句

表达式是值、变量和操作符的组合,单独的值或者变量也是表达式。语句是解释器运行的一个代码单元,表达式和语句的区别在于前者有值而后者没有。

2.1.6 注释

Python 采用 # 进行注释,如下:

# 这是一条注释

"""多行注释第一行
多行注释第二行
"""

2.2 函数

函数是封装好的语句调用,其同样满足“程序基本架构”,可以有一些传入值,我们称之为参数,其分为有返回值得函数和没有返回值的函数,这些是经过数学运算、条件判断以及循环后的输出值。没有返回值的函数打印结果是None.

2.2.1 基本函数

类型转换函数 int(var) 数学函数 import math; math.sin(var)

2.2.2 模块、句点表示法和组合函数

from xxx import xxx 句点表示法:从模块调用其内部函数的方法 组合函数 import math; math.sin(int(var))

2.2.3 自定义函数

def function_name(a,b,c):
    """XXX"""
    do something
    return/yield/print

function_name(a,b,c)

使用 def + 表达式 + ":" 进行函数的声明,其包含段落需要采用同样的缩进以区分函数的边界。有返回值的函数,一般使用return,不论在那个分支,只要遇到return必然结束此函数运行,直接返回,其余流程永远都无法到达(无效代码)。

2.2.4 函数的作用

代码易读、易调试;模块化使用更方便; 注意:在函数中定义的参数是局部的

2.3 键盘输入

input("Somestring > ")

2.4 条件和递归

采用If-else语句块进行条件判断。一个小程序:

def xxx(x):
    if x > 0: # 条件、条件链上的表达式
    ​    print("x is bigger than 0") # 分支语句
    elif x = 0: # 选择执行,只会执行其中一个分支
    ​    print("x is 0")
    else:
        if y > 0: # 嵌套条件
            print("x is smaller than 0")
        else:
            xxx(x-1) # 函数递归,一般不推荐使用

2.5 迭代

2.5.1 多重赋值

对于相等判断操作符“==”和赋值符号“=”应该给予区分。前者是出现在表达式中的一个操作符号,用于判断表达式是否为真,一般用于条件判断等场合。而后者则用于给变量一个值得场合,并且后者的限制更多,比如,变量必须在左边,值在右边。上文已经说,给变量赋值是语言的重要功能之一,多重赋值可以给变量赋值的情况下改变其值,这样变量代表的值就发生了变化。

2.5.2 while和break

迭代用来代替递归进行重复运算,其用途广泛。while后加一个表达式可以作为语句开始迭代过程,除非遇到break或者return等特殊语句,迭代一直会计算其表达式。

比如:

n = 1 # 这是一个简单赋值
while n < 100: # while 加上一个表达式构成迭代的开始
    print("233") 
    n += 1 # 这句话等同于 n = n + 1 ,是对于变量n的多重赋值/更新变量
    if n%2 == 0:
        break # 在迭代过程中,如果遇到特殊语句,可以跳出迭代,而不用继续判断表达式的值。

2.6 遍历:以字符串为例

2.6.1 字符串的for遍历

字符串是一种值的类型,你可以将其传递给一个变量,比如:

var_a = "Hello World" 

其中字符串的识别很简单,用双引号隔开的就是字符串。type(var_a)可以返回变量a的值的类型,可以看到其类型是字符串String。

对于字符串来说,比较常用的做法是进行遍历,如下:

for item in "Hello":
    print(item)

其中“item in “Hello World””是一个表达式,前面加“for”构成一个遍历,遍历必须有一个对象,此对象类型为iter,iter支持遍历,可以取出其中的各个组成元素。比如,字符串可以作为iter类型,其返回值为“H”,“e”,"l","l","o"。同样的,一些高级结构,比如列表、元组和字典也支持遍历。

2.6.2 字符串的切片

字符串在语言层面通过遍历来进行元素的取出,但是,对于使用者而言,可以使用切片方便的进行操作,注意,字符串只可读不可写。

"Hello"[0:2]

以上的语句表示从hello这个字符串中进行遍历,取出从第0个到第2个的元素,其结果为"He",注意,切片包含其前而不包含其后的取值,即上述语句实际上取出了第0、1位。也可以使用[-1:]来找出最后一位,使用[:-1]剔除最后一位,使用[:]进行字符串的复制等等。

2.6.3 其它

字符串有很多方法,比如使用len()来获取字符串长度,使用upper()来将小写变成大些等等。但本质上,其和遍历都密不可分。

比如,在使用len()的时候,我们可以使用遍历实现我们自己的my_len():

def my_len(string=""):
    count = 0 
    for item in string:
        count += 1
        return count

3、错误和调试

错误的类型有:

- 语法错误
- 运行时错误
- 语义错误

通常认为,编程即调试。根据线索推导出错误发生的过程以修改程序并且完成设计目标的过程。你应该从一个能做某些事的程序开始,然后一点点做出修改,并调试修改,迭代,以确保程序总是可以运行。一个可以运行的程序是最基本的程序要求。

正比如Linux——Linus最早的一个程序是交替打印AAAA和BBBB,最后这些程序演化成了Linux——一个操作系统核心。

PS. 当你探索语言的新特性的时候,一个最好的方法就是不断的犯错,因为在学习的时候犯错可以,这总比在写一个大型项目的时候犯错来的代价要小。而正因为如此,我们才对于真正的正确有了一个经验上的认识。这是我第一遍阅读这本书时所了解到的第一个也是最重要的一个编程学习原则。

PS2.总的来说,错误信息告诉你发现问题的地方,但那常常并不是问题发生的地方。

对于较大数据集调试的建议

限制输出:对于高级结构,比如列表、元组和字典,可以通过限制输入来进行调试,比如一个1000行的列表,通过限制输入10行来判断和调试,这样比较节约时间和开销。

检查概要:此外,很多时候我们没有必要返回所有的内容,我们需要检查概要信息和类型,因此有时候输出字符串长度、返回变量类型可能比将一大串结果输出出来要容易定位问题。

编写自检查逻辑:可以在代码中加入一些自动判断的代码块,可以是一致性检查或者是健全性检查,比如使用try/except/finally语句和isinstance()等判断函数。

错误的捕获和处理

通常采用try-except-finally语法块进行捕获和处理:

try: # 尝试执行此语句下的代码
    fh = open("filepath","a",encoding="utf8")
    content = fh.read()
    content = content.replace("\n\n","\n")
    fh.write(content)
except: # 上述代码块出错后立即终止处理并且调用此处代码
    print("ERROR")
finally:# 不论try代码块是否有问题,这里的代码块都会被执行
    fh.close()

详见6.1章节。

4、写程序的流程

4.1 增量开发

一般来说,要考虑先最简单的实现某一功能,比如:

def some_function():
    pass # 脚手架代码

def some_funtion(a=0,b=0,c):
    print(a,b,c,a+b+c) # 脚手架代码
    return a + b + c

先写好一个框架,然后在其中添加更多的代码,使用print打印中间变量的值以帮助自己定位程序运行情况,一旦此部分代码正常运行,则删除这些“脚手架”代码。

其要点在于:

  • 以一个可以正确运行的程序开始,每次只做小的增量修改,如果在任何时刻发现错误,都应当知道错误在哪里。

  • 使用临时变量保存计算的中间结果,这样你可以显示和检查它们

  • 一旦整个程序完成,删除脚手架代码或者将多个语句综合到一个复杂的表达式中。尽量不要增加代码阅读难度。

4.2 函数和组合函数

首先,写一些包含表达式、算术、条件判断以及循环的表达式和语句,其次,将这些语句封装在一个函数内,之后,泛化这个函数,有可能添加一些参数以及返回一些值。很多函数可以嵌套在一起完成复杂的功能,比如:

def a():
    do something
    if a > 0:
        do a1()
    else:
        do a2()

4.3 知道函数做什么就够了

跟踪程序执行流程是一个阅读程序的方法,但是,当程序很大的时候,这样做会很容易陷入迷宫。所以,当你遇到一个函数调用的时候,不去跟踪执行的流程,而是假定函数是正确工作,能够返回正确的结果,这样的话,我们就可以去把握更加重要和宏观的整体流程。实际上,当我们执行math.sin()这个函数的时候,我们并不知道发生了什么,但是我们知道它是用来求sin这个三角函数的,在很大程度上,这就够了。


_____________________________________________________

后记

2018年2月4日

标题1-4就是第一部分的内容,这是编程很小的一个子集,但那时,起始这个子集已经是一个完备的编程语言了,也就是说,任何计算的问题,都可以用这个子集的语言来完成。任何已有的程序,都可以使用这个子集重新写出来。

在下一部分,将会继续介绍迭代、字符串、以及列表、元组、字典等高级数据结构、文件处理以及 类的函数、对象、方法等OOP的内容。

2018年2月5日

这天我更新了“迭代”和“遍历”两部分。我之前写作此文的目的在于为之后学习Matlab做一个编程通用架构的基础,而这天我得到了一本很好玩的,写作业非常有意思的小书,叫做《Javascript DOM编程艺术》,为了加深我自己对于本文的理解,优化章节逻辑,我准备通过学习JS来实践这些我从《像计算机科学家一样思考Python》一书中提取的“编程骨架”。

2018年2月9日

按照计划,我阅读了moz的官方JavaScript Guide,以及《Javascript DOM编程艺术》的语法和DOM部分。这本书写的太浅,因此我觉得按照moz的手册指导一步步深入学习JS是一个好主意。moz的手册提供了很多实践练习的机会,我因此补充和写成了这篇文章: JavaScript学习笔记。回过头来,我又对本文进行了一些细微的修改,并且认识到了Python相比较JavaScript的优点:一致性强,严谨易读好用。相反,JS处处都是坑,但是其操纵HTML页面的能力,快速呈现GUI的能力以及任意现代浏览器均内置其解释器的巨大应用范围都是不得不值得钦佩的。尽管,这门脚本语言因为其使用场景限制和声名狼藉且四分五裂的名声显得不够硬核,但是却得到了广泛的应用。世事无常,就比如Python,若不是赶上了机器学习的东风,恐怕也难有现在这么火热。

本文适用人群

任何人,当然,都会发现本文易于阅读。但是,本文并非面向没有编程经验的新手,我推荐他们阅读这本书原文而不是看这些笔记,因为这本书中通过大量的有趣的生活问题引导你加深这些内容的理解,这很重要,因为我觉得只有在“实践”中才能够理解框架性的东西。自然,我是花了大概1个多月才做完这些问题,说实话,很有挑战性,甚至,我现在回去做,都有可能面对一些要卡壳的困难问题。通过这些问题的解决,相信你会对编程有所理解。

本文并非像文档一样提供一个详细的语言知识,相反,关键的在于标题,这些标题与其内容构成了编程的核心组成元素,搞清楚哪些元素被包含是更为重要的,因为在这种情况下,你可以在学习一门语言之后,按照这个套路来学习另外一门语言,轻松的,而不是一坨浆糊的又从头开始,或者干脆吊死在一棵树上。

修订历史

2018-02-04 version 0.0.1 添加文档

2018-02-05 version 0.1.0 添加文档内容,使用JS作为内容实践,添加了后记和日志。

2018-02-09 version 0.1.1 修正了部分观点,添加了一些Tip。