第三讲 复合类型与函数 DONE
Table of Contents
1 Python 复合类型
Python 的基本数据类型包括整型、浮点型、布尔型与字符串。这些类型都可以组合起来。
1.1 列表
列表用 []
表达,元素用 ,
分离。元素类型任意,甚至可以不同。
[1,2,3], ["天","地","人"], ["物理",3.14]
([1, 2, 3], ['天', '地', '人'], ['物理', 3.14000000000000])
也可以嵌套,我们仿照集合论的自然数构造方法,构造一系列合法的列表:
[], [[]], [[], [[]]], [[], [[], [[]]]]
([], [[]], [[], [[]]], [[], [[], [[]]]])
在 Python 看来,这些个列表都各不相同。
1.1.1 汇总
列表常用来汇总。生成空列表,使用 .append()
方法逐步加入元素,例如:
li = [] li.append("手机") li.append("身份证") li.append("钥匙") print(li)
['手机', '身份证', '钥匙']
列表可用作迭代器,
for x in li: print(f"出门之前,记得带{x}!")
出门之前,记得带手机! 出门之前,记得带身份证! 出门之前,记得带钥匙!
也可以用下标取出特定的元素,用法与字符串一样:
li[0], li[1:3], li[-1]
('手机', ['身份证', '钥匙'], '钥匙')
可以当成一个集合来判断元素的归属:
"手机" in li, "眼镜" in li
(True, False)
1.2 字典
字典是 Python 标志性的数据结构。顾名思义,单词放进字典,它个单词(key)的解释对应字典中的值(value)。词与值之间用 :
分隔,词与词之间用 ,
分隔。我们把教室里的学生人数创建一个字典,字典可通过赋值加新词,也可以判断词的归属:
sc = {"工物": 20, "物理": 40} print(sc["工物"], sc["物理"]) sc["上海交大"] = 2 # 创建了新的词条 print(sc["上海交大"]) print("牛津" in sc, "工物" in sc)
20 40 2 False True
1.2.1 条件语句字典化
字典构建了从词到值的映射关系,当条件语句有这样的特点时,可用字典方便地替代。体会下面的例子,已经学生群体的变量名 aff
,找出学生人数:
aff = '工物' if aff == '工物': print(20) elif aff == '物理': print(40) elif aff == "上海交大": print(2) else: print(1) # 用字典查询更加方便 print(sc[aff])
20 20
"字典查询"替代了多级的条件,更适合直觉。
1.2.2 字典的使用
字典中的词或者值都可以转化为列表,或者迭代器,
list(sc.keys()), list(sc.values())
(['工物', '物理', '上海交大'], [20, 40, 2])
for k in sc: print(k) for v in sc.values(): print(v)
工物 物理 上海交大 20 40 2
更常用是把词与值一起迭代循环,
for k, v in sc.items(): print(f"教室里有{v}名{k}的学生。")
教室里有20名工物的学生。 教室里有40名物理的学生。 教室里有2名上海交大的学生。
1.2.3 Python 内部的字典
字典是 Python 的核心数据结构,它的命名空间(namespace)就是用字典实现的。Python 环境中的变量都中某个字典的词。往往字典的妙用可以给程序带来神来之笔的重构。 字典的内部数据结构是哈希表,可以保持插入和查询的效率。
1.2.4 构造字典快捷方法
任何输出序对的迭代器,都可以快速构造出字典。如,
dict(enumerate("abcd"))
{0: 'a', 1: 'b', 2: 'c', 3: 'd'}
从中可见 Python 简单语句的表现力。一般的思维会这样写:
d = {} # 生成一个空字典 for k, v in enumerate("abcd"): d[k] = v # 通过赋值添加 k:v 组 print(d)
{0: 'a', 1: 'b', 2: 'c', 3: 'd'}
显得很冗长。有一种中间态的写法是
{k:v for k, v in enumerate("abcd")}
{0: 'a', 1: 'b', 2: 'c', 3: 'd'}
可以用来把值与词对换
{v:k for k, v in enumerate("abcd")}
{'a': 0, 'b': 1, 'c': 2, 'd': 3}
1.2.5 词的数据类型
字典的原理要求词是不可变类型。字典创建后,如果词变了,内部的哈希方案会失效。列表可变,所以不能成为字典的词。与列表对应的不可改类型是元组(tuple),可以用作词。字典的值可以是任何数据类型,与变量等价,如
{(0, 0): 6, (0, 1): "您", (1, 0): ["Python", "Bash"]}
{(0, 0): 6, (0, 1): '您', (1, 0): ['Python', 'Bash']}
这不奇怪,变量本身就是由内部的字典实现的!
1.3 删除元素
1.3.1 列表
remove
,效果为删除特定元素,使用方式为 list.remove(element)
。例如对以下样例:
ls = [1, "f", 3.0] ls.remove("f") print(ls)
[1, 3.0]
pop
,效果为删除指定索引对应的元素,使用方式为 list.pop(index)
。例如对以下样例:
ls = [1, "f", 3.0] ls.pop(0) print(ls)
['f', 3.0]
切片,效果为删除连续索引对应的多个元素,使用方式为 list[index_begin:index_end] = []
。例如对以下样例:
ls = [1, "f", 3.0] ls[0:2] = [] print(ls)
[3.0]
(注意如 0:9 表示从 index 为 0 到 index 为 8 的元素)
clear
,大规模杀伤性武器,效果为清空列表中所有元素,得到空列表,使用方式为 list.clear()
。例如对以下样例:
ls = [1, "f", 3.0] ls.clear() print(ls)
[]
使用 list = []
可实现同样效果。
del
,核武器,直接删除了列表对象,使用方式为 del list
。例如对以下样例:
ls = [1, "f", 3.0] del ls print(ls)
会报错:
Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'ls' is not defined
1.3.2 元组
由于元组具有不可变原则,故想通过类似 remove
和 pop
这样的方式删除元素无法实现。但由于课上介绍过元组可以实现加法(类似并集),故可以通过切片的方式进行删除操作。例如对以下样例:
tup = (1, 2, 3, 4) #删除元素3 tup = tup[:2] + tup[3:] print(tup)
(1, 2, 4)
与列表类似,可以使用 tuple = ()
实现全部元素的清理,也可以使用 del tuple
删除整个元组。
1.3.3 字典
不同于列表,字典的索引是通过keys实现的,是无序的,但仍可以通过对keys的索引进行元素的删除,只是不支持切片操作。字典元素的删除主要有以下几种方式:
pop
,效果为删除指定索引对应的键值对,使用方式为 dict.pop(keys)
。例如对以下样例:
dictionary = {"A":1, "B":2, "C":3} dictionary.pop("A") print(dictionary)
{'B': 2, 'C': 3}
popitem
,效果为随机返回并删除一对键值对,但似乎从 Python3.6
以后一般认为删除的是最后插入的键值对(类似于栈,FILO),使用方式为 dict.popitem()
。例如对以下样例:
dictionary = {"A":1, "B":2, "C":3, "D":4} x = dictionary.popitem() print(x, dictionary)
('D', 4) {'A': 1, 'B': 2, 'C': 3}
clear
,与列表类似,在此不再赘述,同样可以使用 dict = {}
替代。
del
,与列表类似,在此不再赘述。
2 Python 的运行模式
在命令行输入 python3 进入的是交互模式,每输入一个指令,都即时得到结果。这个环境称为“REPL”,即 “Read-Evaluate-Print-Loop”,命令行界面也属此类。交互界面更适合探索和试验。
批处理模式可以批量执行命令,组成批量命令的文件叫做“脚本”,也是广义上的程序。因为脚本运行时没有 REPL 部分,可以不受人干涉,适用于处理大量数据。
在实践中,经常在交互模式中探索出正确的指令,再把这些指令收集起来,形成脚本,自动地批处理运行。因为脚本应用于无人值守的环境,输入与输出就应当自动进行,只是根据提示交互地输入就远远不够了。
另一种从外界给程序传递信息的方式是调用参数。Python 把调用时的参数传递给了特定的字符串列表 sys.argv
。通过 sys.argv[1]
, sys.argv[2]
取得第一、第二个参数。
2.1 调试
在 REPL 与 Python 的解释器进行交互,是个区别于编译型语言很有效的调试环境。分析程序中可疑的程序片断,放在 REPL 中观察它的行为,是找到问题的好方法。
在有问题的脚本运行时给 python3
加上 -i
选项,含义是“iteractive”,例如 python3 -i prime.py
。这可让脚本出错或结束后不退出,留在 REPL 的环境中,提供给我们程序恰好出问题时的“案发现场”来寻找诱因。
2.2 Jupyter 环境
Python 有一个新兴的运行环境,叫做 Jupyter。本课之所以没有以 Jupyter 起步,是因为它只在教学和交互探索中有效,不适合大数据处理。Jupyter 对用户隐藏的细节,在科学数据中是必要的。但是 Jupyter 有它的优点,是图片、文字与程序的混排,对非专业用户直观友好。它是 literate programming ,是一次原则的体现,我们在 4 一节深入讨论。 Jupyter 从 IPython 项目起步,把交互境放到了网页上。在直观的优点之上,它带的困扰是写长的程序时,网页是很糟糕的环境,远远不如完整的编辑器的功能。 Emacs 和 VSCode 的 Jupyter 扩展,在一定程序上缓解了这一问题,使我们写 Jupyter 的程序时,可以用强大的编辑功能替代网页。 Jupyter 支持 Python 之外的运行环境,是一款值得探索的辅助工具。
3 Python 函数
一直以来,函数都是程序的基本组成部分,是由多段指令组成的功能单元,与数学函数有类似之处。函数给程序划分了逻辑层次,在函数内我们关注函数的实现方法,在函数外我们关注它的使用方法并重复调用,有助于实践“一次”原则。
Python 定义函数的语法如下:
def add(x, y): print(f"x is {x} and y is {y}") return x + y # Return values with a return statement add(3, 5)
x is 3 and y is 5 8
关键字 def
跟一个带 (...):
的表达式。返回值用 return
给出。外部调用 add(3,5)
在函数的入口, x
和 y
被赋予 3 和 5。
再举一个例子,
def swap(x, y): return y, x a = '左' b = '右' print(a,b) a, b = swap(a,b) print(a,b)
左 右 右 左
在调用 swap()
函数的一步, x
被赋予 a
的值“左”, y
被赋予“右”。经过函数计算,得到的返回值是交换的结果“右”、“左”,用于覆盖 a
和 b
的值。这个函数返回了两个值,在 Python 的内部表示为一个元组。在函数的内与外,变量有各自的定义范围,这使得我们不论从内还是从外看来,都有完整的逻辑。
3.0.1 函数命名空间
函数内与函数外的命名空间相互独立。看下面的例子,
x = 1 def scope(): x = 2 scope() print(x)
1
scope() 函数内的变量 x ,与外部独立。变量是由字典实现的,函数内与函数外的变量分属于不同的字典。函数从内部操作它外部的变量,是不鼓励的,一定要做时,使用 global
标识符:
x = 1 def scope(): global x x = 2 scope() print(x)
2
3.0.2 元组赋值
值得注意一点, Python 可以通过对元组赋值的形式,实现元组各元素赋值。
print(a, b) a, b = b, a # 是 (a, b) = (b, a) 的简写 print(a, b)
左 右 右 左
这使得置换变量的值非常简单直接,因为否则要引入中间变量,这样写,
print(a, b) tmp = a a = b b = tmp print(a, b)
左 右 右 左
十分不便。
3.0.3 函数的调用
函数可供大批量调用。使用 map
来自然定义一个作用在迭代器上的函数,是分别把它作用到每个元素后返回值组成的迭代器。例如把列表看作迭代器,
def squared(x): return x*x list(map(squared, [1, 2, 3, 4]))
[1, 4, 9, 16]
它把一列数字逐个取平方。 map()
返回的迭代器,交给 list()
转换成了列表输出。
等价于直接用迭代器输入,
list(map(squared, range(1, 5)))
[1, 4, 9, 16]
3.0.4 无名函数
如果函数名不重要,可以直接把函数无名化定义嵌入到语句中。
list(map(lambda x: x*x, range(1,5)))
[1, 4, 9, 16]
省去了函数名, return
等。 lambda
的名字来自理论计算机科学的 lambda calculus 理论,函数式程序的基础。
4 文档
4.1 注释
注释由半角的“#”引出,多行注释用多个“#”:
# 高精度整数举例 # 2**1000
10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069376
要让程序易于被人理解,一方面应提升和打磨代码风格,让程序本身的逻辑易懂,另一方面,对不明显的程序段落,通过注释来解释。
4.2 函数的文档
函数是代码复用的单元,这意味着我们经常会用到别人创作的函数,以节省精力,站在巨人或者一群小矮人的肩膀上。函数定义后紧接的字符串是它的文档,它被特殊对待,由 help()
读取输出:
def spherical_harmonic_fitter(grid, order): "求球谐函数拟合的系数" # 具体实现省略 pass help(spherical_harmonic_fitter)
Help on function spherical_harmonic_fitter in module __main__: spherical_harmonic_fitter(grid, order) 求球谐函数拟合的系数
帮助告诉我们,在 “_main__” 模块(程序默认环境)的名字空间里,有这个函数。
一行的帮助有些单薄。函数的文档,由他人使用,文档写得越详细越好。对复杂的函数而言,函数的帮助需要长篇大论。Python 的多行字符串,正好胜任这一点。多行字符串以三个引号开始,三个引号结束,单引号双引号皆可。三引号设计恰好不与人类语言冲突。
def spherical_harmonic_fitter(grid, order): ''' 求球谐函数拟合的系数 输入 ~~~ grid: 球面上连续函数在固定格点上的取值 order: 拟合时球谐函数近似截断的阶数 输出 ~~~ 拟合系数矩阵 ''' # 具体实现省略 pass help(spherical_harmonic_fitter)
Help on function spherical_harmonic_fitter in module __main__: spherical_harmonic_fitter(grid, order) 求球谐函数拟合的系数 输入 ~~~ grid: 球面上连续函数在固定格点上的取值 order: 拟合时球谐函数近似截断的阶数 输出 ~~~ 拟合系数矩阵
这个例子里,我们把文档写得更加详细。不仅有标题,还详细注明了输入和输出的含义。调用函数的人——可能是队友也可能是未来的自己——应当在不阅读的原代码的前提下,顺利使用它们。即使在写作程序时,感觉很显然,也应该认真撰写文档。在例子中,我们用多行字符串写出,函数在固定格点上取值,定义了输入变量 order 的含义,输出的意义是系统。
4.3 标准库中的文档
Python 的标准库非常重视文档,几乎所有的函数都带有详细的排版精美的多行字符串说明。
help(int)
Help on class int in module builtins: class int(object) | int([x]) -> integer | int(x, base=10) -> integer | | Convert a number or string to an integer, or return 0 if no arguments | are given. If x is a number, return x.__int__(). For floating point | numbers, this truncates towards zero. | | If x is not a number or if base is given, then x must be a string, | bytes, or bytearray instance representing an integer literal in the | given base. The literal can be preceded by '+' or '-' and be surrounded | by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. | Base 0 means to interpret the base from the string as an integer literal. | >>> int('0b100', base=0) | 4 | | Built-in subclasses: | bool | | Methods defined here: | | __abs__(self, /) | abs(self) | | __add__(self, value, /) | Return self+value. | | __and__(self, value, /) | Return self&value. | | __bool__(self, /) | self != 0 | | __ceil__(...) | Ceiling of an Integral returns itself. | | __divmod__(self, value, /) | Return divmod(self, value). | | __eq__(self, value, /) | Return self==value. | | __float__(self, /) | float(self) | | __floor__(...) | Flooring an Integral returns itself. | | __floordiv__(self, value, /) | Return self//value. | | __format__(self, format_spec, /) | Default object formatter. | | __ge__(self, value, /) | Return self>=value. | | __getattribute__(self, name, /) | Return getattr(self, name). | | __getnewargs__(self, /) | | __gt__(self, value, /) | Return self>value. | | __hash__(self, /) | Return hash(self). | | __index__(self, /) | Return self converted to an integer, if self is suitable for use as an index into a list. | | __int__(self, /) | int(self) | | __invert__(self, /) | ~self | | __le__(self, value, /) | Return self<=value. | | __lshift__(self, value, /) | Return self<<value. | | __lt__(self, value, /) | Return self<value. | | __mod__(self, value, /) | Return self%value. | | __mul__(self, value, /) | Return self*value. | | __ne__(self, value, /) | Return self!=value. | | __neg__(self, /) | -self | | __or__(self, value, /) | Return self|value. | | __pos__(self, /) | +self | | __pow__(self, value, mod=None, /) | Return pow(self, value, mod). | | __radd__(self, value, /) | Return value+self. | | __rand__(self, value, /) | Return value&self. | | __rdivmod__(self, value, /) | Return divmod(value, self). | | __repr__(self, /) | Return repr(self). | | __rfloordiv__(self, value, /) | Return value//self. | | __rlshift__(self, value, /) | Return value<<self. | | __rmod__(self, value, /) | Return value%self. | | __rmul__(self, value, /) | Return value*self. | | __ror__(self, value, /) | Return value|self. | | __round__(...) | Rounding an Integral returns itself. | Rounding with an ndigits argument also returns an integer. | | __rpow__(self, value, mod=None, /) | Return pow(value, self, mod). | | __rrshift__(self, value, /) | Return value>>self. | | __rshift__(self, value, /) | Return self>>value. | | __rsub__(self, value, /) | Return value-self. | | __rtruediv__(self, value, /) | Return value/self. | | __rxor__(self, value, /) | Return value^self. | | __sizeof__(self, /) | Returns size in memory, in bytes. | | __sub__(self, value, /) | Return self-value. | | __truediv__(self, value, /) | Return self/value. | | __trunc__(...) | Truncating an Integral returns itself. | | __xor__(self, value, /) | Return self^value. | | as_integer_ratio(self, /) | Return integer ratio. | | Return a pair of integers, whose ratio is exactly equal to the original int | and with a positive denominator. | | >>> (10).as_integer_ratio() | (10, 1) | >>> (-10).as_integer_ratio() | (-10, 1) | >>> (0).as_integer_ratio() | (0, 1) | | bit_length(self, /) | Number of bits necessary to represent self in binary. | | >>> bin(37) | '0b100101' | >>> (37).bit_length() | 6 | | conjugate(...) | Returns self, the complex conjugate of any int. | | to_bytes(self, /, length, byteorder, *, signed=False) | Return an array of bytes representing an integer. | | length | Length of bytes object to use. An OverflowError is raised if the | integer is not representable with the given number of bytes. | byteorder | The byte order used to represent the integer. If byteorder is 'big', | the most significant byte is at the beginning of the byte array. If | byteorder is 'little', the most significant byte is at the end of the | byte array. To request the native byte order of the host system, use | `sys.byteorder' as the byte order value. | signed | Determines whether two's complement is used to represent the integer. | If signed is False and a negative integer is given, an OverflowError | is raised. | | ---------------------------------------------------------------------- | Class methods defined here: | | from_bytes(bytes, byteorder, *, signed=False) from builtins.type | Return the integer represented by the given array of bytes. | | bytes | Holds the array of bytes to convert. The argument must either | support the buffer protocol or be an iterable object producing bytes. | Bytes and bytearray are examples of built-in objects that support the | buffer protocol. | byteorder | The byte order used to represent the integer. If byteorder is 'big', | the most significant byte is at the beginning of the byte array. If | byteorder is 'little', the most significant byte is at the end of the | byte array. To request the native byte order of the host system, use | `sys.byteorder' as the byte order value. | signed | Indicates whether two's complement is used to represent the integer. | | ---------------------------------------------------------------------- | Static methods defined here: | | __new__(*args, **kwargs) from builtins.type | Create and return a new object. See help(type) for accurate signature. | | ---------------------------------------------------------------------- | Data descriptors defined here: | | denominator | the denominator of a rational number in lowest terms | | imag | the imaginary part of a complex number | | numerator | the numerator of a rational number in lowest terms | | real | the real part of a complex number
Python 的文档网站的内容,就是由这些代码中的函数文档生成。这种把人类可读和机器可读的文字写在一起的思想,叫做“literate programming”,目标是让程序既适合被机器执行,也适合被人类阅读。修改程序与修改文档要保持同步。相反,如果程序与文档写在不同地方,甚至由不同的人来撰写,那么大概率经年累月,它们会有很大出入,使用文档失去了应有的价值。因此从一开始贯彻 literate programming 的原则,有助于长远的程序可读性和易用性,注意体会其中的“一次”原则:文档和程序在说同一件事情,我们只在一个地方把它们全都写出来。
在通过书籍或课程系统性地对 Python 语言和环境的整形把握之后, 随手查阅 help()
所得的在线帮助非常实用,是灵活的“工具书”。我们有了基础之后,可以借助这个强大的帮助系统边学边用,学习和工作效率都会很高。