这是我关于《如何像计算机科学家一样思考》一书的体会和总结。此书的副标题叫做《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日 添加持久化、类的相关内容初稿