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

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

上部分介绍了基本的表达式、语句、条件、递归和迭代以及错误和调试。下部分是关于数据结构、文件操作、OOP面向对象编程的相关知识,这些内容会更少的通用,更多的具有Python特色,但这是这些特色部分,才让Python变得如此好用。

5 数据结构

从这一部分开始,很多东西便具有了Python语言的特色,但是其基本架构是不变的,比如对于大多数高级数据结构都有遍历、分片、添加删除更改的操作。

5.1 列表

5.1.1 列表(list)的结构

在第一部分我们认识了值和类型,其中穿插着介绍了两种不同的值的类型,分别是整数(int)和字符串(string)。而在第五部分,我们会认识一些高级的类型,比如列表、元组以及字典。正是因为这些高级的数据结构,Python才可以“学的更少,做的更多”。

列表(list)是一种数据结构、一种值的类型,就像字符串一样。列表有一系列的元素组成,其中元素可以是各种数据类型,比如字符串、整数、小数,甚至是另外一个列表。

list_a = ["a","Hello",12,23,["55",13]]

列表的表示方法如上所示,采用一对[]括起来,就像字符串使用一对“”括起来一样。各元素中间采用英文逗号隔开,元素可以是另外一个列表,这叫做“嵌套”。

5.1.2 遍历、操作符和分片

和字符串一样,列表可以使用for语句进行遍历,这是分片的基础。使用lista[m:n]来分片,m代表起始元素,n代表结束元素,m不写代表从头开始,n不写代表直到最后。注意第一个元素的下标为0,这和字符串分片一样。对于嵌套列表,可以使用[][]来取出二级嵌套元素,比如对于[1,2,3,[4,5]]而言,lista[3][0]可以取出元素“4”。

for item in [1,2,3]:
    print(item) # 结果是 1 ,2 ,3

列表常用的两个操作符是+和in,前者用来粘合列表,后者用来判断一个元素是否在列表中。

5.1.3 列表常用的操作(增、删、改)

不同于字符串的不可更改性,列表是可更改的。

你可以使用list_a[1] = 27 来替换掉列表的第一个元素,这样的话,列表就变成了:["a",27,12,23,["55",13]]。这是一种更改列表特定元素的操作,除此之外,还有一些添加元素、删除元素的操作:

list_a += item # 使用+运算符来向列表添加元素

list_a.append() # 用来扩充列表,作用同上

list_a.pop() # 根据下标删除元素,返回删除的元素

list_a.remove() # 根据元素的值来删除元素

del list_a[number] # 根据下标删除元素,无返回值

和字符串的函数不同,大多数列表函数并不返回这个列表,因为其列表本身发生了变化,所以直接调用列表就好,大多数函数的返回值都是None.

5.1.4 列表的新建

新建列表可以对于一个可迭代项目的遍历建立,也可以使用list()函数、字符串split()函数来建立。

1、通过遍历建立

for item in "Hello"
    list_a.append(item)
    # list_a结果为 ["H","e","l","l","o"]

2、通过list()函数建立

list()函数用来将可迭代的项目转换成为列表,比如可以将一个字符串分开:

var_a = list("Hello") 
=> 
var_a
["H","e","l","l","o"]

3、通过split()建立

但有时候,我们并不像这样划分字符串,可以使用字符串的split方法生成列表,比如:

var_b = "Hello World".split(" ")
=>
var_b
["Hello","World"]

5.1.5 列表的转换:字符串

可以使用字符串的join方法来将可迭代项目(比如列表)合并,比如:

"".join(["H","e","l","l","o"])
=>
"Hello"

",".join(["Hello","World"])
=>
"Hello,World"

5.1.6 映射(map)与其陷阱

不同于字符串的不可更改性,列表的可修改性造成了一个问题,就是映射,举例如下:

a = "2"
b = "2"
a is b => True

a = [0]
b = [0]
a is b => False
b[0] = 42
b => [42]

a = [0]
b = a
a[0] = 42
b => 42
a is b => True

对于第一个例子而言,a和b是同一个字符串的映射,对于第二个例子而言,a和b是不同的列表,没有映射关系,但是奇怪的是第三个例子,b=a这个赋值是一种映射关系,a和b是映射而非不同的列表。对于高级数据结构,比如列表、字典等,所有的赋值都是映射(浅赋值),这保证了其数据在运算的时候的高效,但是却很容易弄迷糊。使用b=a[:]可以复制列表而不是建立映射,或者使用b=copy.deepcopy(a)来复制列表。

映射对于字符串来说没什么影响,因为字符串不可更改,如果想要修改,必须新建字符串,但是列表元素可以更改,所以任何映射此列表的其它列表都会被这个列表的元素更改所影响。

5.2 字典

区别于列表,字典没有下标,其采用的方式是键值对(key-value)存储数据。字典更加灵活。

5.2.1 字典(dict)结构概览

字典的结构如下所示,字典使用花括号括起来,其中的键可以为字符串或者是数字类型,值可以为任意类型。

dict_a = {"key1":"value1","key2":"value2","key3":True,"key4":233}

5.2.2 遍历、操作符运算和切片

使用for进行字典的键的遍历,如下:

for key in dict_a:
    print(key) # key为字典的键
    print(dict_a[key]) # dict_a[key]可以通过键取出对应的值,区别于列表使用下标取出值的方式

字典的in操作符运算,如下:

if "key2" in dict_a: # 字典可以进行in操作符运算,判断键是否在字典中
    print("I got it")

5.2.3 字典的一些常用函数

dic = dict()

dic.values() 可以显示出来所有值的结果

dic.has_key(key) 返回字典中是否有此键的布尔值

dic.keys() 可以显示所有的键,一般直接使用for对字典遍历即可,不需要此函数

dic.items()/pop()/copy()等请自行参考Python手册: 打开CMD,输入python调出交互模式,使用dir(dict)可以查看所有dict类型的方法,使用help(dict.pop)可以查看pop方法的帮助。

5.2.4 字典的新建

使用dict()函数可以新建一个字典,或者对于一个变量直接如下指定:

dict_a = {} # 建立一个空字典
dict_a = dict() # 作用同上

5.2.5 字典的使用:暂存数据

如下所示是字典的一个计数器应用,判断字符串各字母出现的多少:

from pprint import pprint # pretty print 更人性化的打印函数
dict_a = {} # 新建一个字典结构
for key in "helloworld":
    if dict_a.has_Key(key): # 字典方法:判断是否有此键,返回布尔值
        dict_a[key] += 1
    else:
        dict_a[key] = 1
pprint(dict_a)

=> {'d': 1, 'e': 1, 'h': 1, 'l': 3, 'o': 2, 'r': 1, 'w': 1}

可以看到,字典一般被用作计算结果的内存暂存器。这些值可以被保存在字典中以备后续使用。

5.2.6 键的限制、全局变量陷阱

字典对于键和值有一定的要求。字典是采用散列表的方式实现的。散列可以接受任意类型的值并返回一个整数,字典使用这些被称之为散列值得整数来保存键值对。当键不变的时候,可以良好工作,其会根据值和键进行散列,但是,对于字典、列表这类可以变化的数据结构,如果键本身发生了变化,那么散列就不能够顺利找到键值对,因此键必须是可散列的,也就是不可变的,字典和列表都是可变对象类型,因此不能用作键。

在函数之外创建的变量称之为全局变量。而在函数内的变量称之为局部变量,如下:

var_a = 233
var_b = 233

def xxx():
    var_a = 222
    print(var_a) # 局部变量,结果为222

    global var_b # 声明此变量为全局变量
    var_b = 111
    print(var_b) # 结果为 111

xxx()
print(var_a) # 全局变量,结果为233
print(var_b) # var_b 在函数中被声明是全局变量,被修改为111,因此结果是111.

注意:对于不可变类型的全局变量,在函数内如果不声明则不能修改,但是对于可变类型的全局变量,在函数内不声明可以进行添加、删除、替换,但是,如果进行赋值,则需要声明它。

比如:

dic1 = {"a":1}

def xxx():
    dic1 = {"b":2}
    print("in xxx dic1",dic1)

xxx()
print("out1dic1",dic1) 
# dic1在xxx函数内被重新赋值,变成局部变量,因此两次输出不同

输出结果如下:in xxx dic1 {'b': 2} ;out1dic1 {'a': 1}

def xxy():
    dic1["a"] = 2
    print("in xxy dic1",dic1)

xxy()
print("out2dic1",dic1) 
# dic1在xxy函数内被更改元素,依然是全局变量,所以结果不变

输出结果如下:in xxy dic1 {'a': 2} ;out2dic1 {'a': 2}

5.3 元组

和列表类似,元组也是根据下标来计数的高级结构,但是元组的单个元素无法改变,并且其创建和使用也都更加容易。

5.3.1 元组(tuple)结构

元组采用括号和逗号以及其分隔开的元素表示:

tuple_a = (1,2,"a")

需要注意,("a")这种表示是字符串,而("a",)这种表示才算作元组。元素可以为任意类型,支持无限层级的嵌套。

其实,元组只用逗号隔开就可以,但一般而言,约定俗成的约定是加上括号。

a,b = b,a # 一个极其优雅的值交换

5.3.2 遍历和操作符、分片

由于元组元素不可变,所以不仅可以用+操作符,也可以用in操作符,还可以用<、>这样的比较操作符。对于大小比较,如果第一个元素相同则比较第二个,以此类推。

对于for遍历而言,和列表大同小异。

5.3.3 元组的新建

元组可以直接使用元素新建,比如:("a",),也可以使用tuple(),此函数传递的参数为任意可迭代的类型(iter),比如字符串或者列表,甚至字典。

tuple("Hello") => ('H', 'e', 'l', 'l', 'o')

5.3.4 元组的使用:收集分散、参数传递

元组常用于作为参数输入,对于函数而言,以*开头的参数会收集(gather)所有的参数到一个元组内:

def xxx(a,b,*args):
    print(args)

xxx(1,2,3,4,5,6)
=> 结果为 (3,4,5,6)

同样的,对于某些函数,可以接受两个参数,但是我们只有一个元组,可以这样:

def yyy(a="",b=""):
    pass

c = (1,2)

yyy(c) # 错误的,因为只能接收一个参数
yyy(*c) # 正确,星号将c元组分开成为多个参数传入函数中,这叫做scatter(分散)。 

其次,对于函数而言,返回多个值,使用列表较为麻烦,但是,使用元组就很简单,比如:

def zzz(a,b):
    return a,b # 作为元组返回

5.3.5 元组和列表、字典的关系

元组和其他高级数据结构关系密切,比如列表和字典。对于任何可迭代内容,使用zip()函数可以新建可迭代列表的元组对,比如:

zip("Hello","123456") 
=> [('H', '1'), ('e', '2'), ('l', '3'), ('l', '4'), ('o', '5')]

对于无法zip的元素自动舍弃,返回最短元素那一组。

元组也可以转换成为别的结构,比如上述结果可转换成为:

dict(zip("Hello","123456")) => {'H': '1', 'e': '2', 'l': '4', 'o': '5'}

需要注意的是,元组对转化成为字典的时候,其第一个可迭代对象作为字典的值,因此,两个l中的第一个已经被第二个覆盖了(重复键值写入)。

当然,字典也可以转换成为元组对/元组,比如

dic = {'H': '1', 'e': '2', 'l': '4', 'o': '5'}

tuple(dic) => ('H', 'e', 'l', 'o')

dic.items() => [('H', '1'), ('e', '2'), ('l', '4'), ('o', '5')]

综上所述,可以将元组对列表看作是字典。 这也就是列表、元组和字典的关系。

6 持久化

Python持久化储存可以采用二进制和字符串两种方式进行存储。常用的有文本文件存储,pickle和shelve序列化后的key-value存储,json格式、xml格式存储等。对于存储一个较为重要的模块是os.path。此模块包含了在Windows和Unix平台下由于不同的平台特性导致的各种兼容性问题的解决方法。

对于目录而言,常用的方法有:

os.path.exists(dir) # 是否存在此目录

os.path.isdir(dir) # 判断是否为目录

os.listdir(dir) # 遍历一级列表查找文件夹中的文件

对于文件而言,常用的方法有:

os.path.exists(file) #判断是否存在此文件

os.path.isfile/islink # 判断是否为文件/链接文件

open(file,"mode",encoding="utf8",ignore=True).read() # 打开文件

对于路径而言,常用的方法主要是关于平台差异的:

os.path.normpath(dir) # 此方法返回符合本平台编码的path格式。

os.path.join(dir,file) # 连接多个字段

文件和数据库的操作请自行参考Python文档。

[引用6.1]错误捕获和处理

文件的读取和存储有一定的问题,因此需要错误捕获,通常采用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()

7 类和面向对象编程

7.1 类:一种高层次的抽象

类是一种块状的抽象表达,其对应现实世界的对象。比如苹果、橘子、香蕉都属于水果,水果这一概念是各种水果的高层次抽象和共同特征集。苹果、香蕉属于水果的实例,我们也可以新建一个包含水果特征的新水果,这也是水果这个类的实例。采用以下方法定义一个类:

class Fruit(object):
    """这是水果类"""

newfruit = Fruit() # 创造一种水果类的实例

7.2 类和函数的封装

通常而言,计算机的类称之为对象,其对应真实世界中的对象,类的交互称之为函数,其对应真实世界中不同类的交互。函数有两种,其一为不涉及实参,而返回新的类对象的纯函数以及对实参进行变形、计算和处理后返回的非纯函数。通常而言,将涉及某个特定类的函数封装在一个类中方便调用,其称之为类的方法。

除了我们声明的类的方法以外,还有一些特殊的类的方法,比如init方法用来在初始化类的实例的时候进行某些操作,add方法在两个类的对象相加时进行逻辑运算,str方法会在你打印类时返回自定义的字符串,这些方法你都可以使用,以让自己的类更加强健和方便。

一个类的结构如下:

class People():
    """定义一个人的类"""

    def __init__(self):
        self.name = None
        self.age = 0
    def __str__(self):
        return self.name,self.age
    def __add__(self,*arg):
        return self.age + arg
    def dosomething(self):
        # do something
    
class Employ(People):
    """员工类,其继承了People类的方法和属性"""

    def __init__(self):
        super(Employ,self).__init__(self) 
        # 此句话用来初始化其父类的__init__方法中的代码段
    ...

封装在类中的函数一般称之为类的方法,其有两种调用方式:

其一为函数式(少用):

实例1 = 类()
类.方法(实例1)

而最常用的是:

实例1 = 类()
实例1.方法()

7.3 函数的多态和类的继承

编写一个类/函数最好可以让其能够接受不同类型的参数并且进行处理,这种理念称之为多态,这丰富了函数的用途,增强了其健壮性,是一种良好的编程实践。

类可以继承,就比如人类和雇员类这种包含关系,其代指了类的继承关系,父类的所有方法子类都可以使用,但是需要进行一个声明。self表示此函数属于类的方法。




————————————————————

更新日志

2018年2月10日

这天我更新了持久化、类和继承相关的内容。关于OOP的相关内容只是一个初稿,在写作时我正在阅读JavaScript的对象的使用,我会在JS进行一个学习和比较后继续补充本文类相关的内容。

这就是《像计算机科学家一样思考Python》的全部笔记了,而JavaScript的实践,只是一个开始。JS和DOM的HTML交互,就好比Python和Qt GUI低配版,很多东西都是通的,我惊讶的发现Python的字符串方法中竟然很多和JS的字符串方法重合,很多连名字都一样,比如split()等,而大多数也都是采用不同表述形式的翻版。而对于DOM,其作为代码和用户交互的入口,类似事件Event(evnet.target)等业务逻辑其实和作为GUI的Qt信号/槽/Event(event.sender())很类似。甚至,JS的对象结构——JSON和Python最重要的数据结构字典都长的一模一样,这可不仅仅是巧合。

我下一步的计划是阅读《像计算机科学家一样思考C++》,因为Qt的原本Driver就是C++,对C++的进一步了解,有利于我对于Qt知识的巩固,进一步发掘其价值。此外,因为JS和Python实在太相似了,我还想要自己的范式在编译型语言进行验证,这也是对于本文的一个实践方向。

最后,范式让我很快的在短短几天就把握住一门动态语言,但是我发现,这还不够,因为仅仅是语法的掌握,对于现代的业务逻辑来说是不可想象的,框架、流程,这才是重点。因此,自从我学习Python以来,才发现原来学习一门语言如此的容易,但是,要在学习过语言之后做点事情,还需要了解各种场景的逻辑和框架的流程,这才是最耗时的。不必提各种语言本身内置的玲琅满目的包,需要花费时间的还有这些包后面所驱动的各种框架和业务流程,比如前端的GUI(PC、Web),后端的SQL和NoSQL数据库,如果还要搞科学计算,视觉、语音、算法都有其逻辑体系,这些都是无关语言的。

更新历史

2018年2月6日 verion 0.0.1 添加文档,更新了列表、字典、元组部分

2018年2月10日 添加持久化、类的相关内容初稿