第三讲 复合类型与函数 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 元组

由于元组具有不可变原则,故想通过类似 removepop 这样的方式删除元素无法实现。但由于课上介绍过元组可以实现加法(类似并集),故可以通过切片的方式进行删除操作。例如对以下样例:

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) 在函数的入口, xy 被赋予 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 被赋予“右”。经过函数计算,得到的返回值是交换的结果“右”、“左”,用于覆盖 ab 的值。这个函数返回了两个值,在 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() 所得的在线帮助非常实用,是灵活的“工具书”。我们有了基础之后,可以借助这个强大的帮助系统边学边用,学习和工作效率都会很高。

Author: 续本达

Created: 2023-07-23 Sun 08:47

Validate