Python 学习笔记

• Notes
归档
本文原已在其他场合、更早之前发表,已无时效。
ABC
本文属初学作品,其时仍在学习、探索;高人不必读。

学了N年 C/C++ 的我,现在得为着学业需要而学一下 python,这简明轻快的语言风格真让我有些不习惯。最主要的是,各种各样的命令我总是忘记。以此笔记摘录一下主要内容,以免遗忘。

入门的读本是:《Python语言及其应用》,Bill Lubanovic著,丁嘉瑞、梁杰、禹常隆译。

笔记中若有提到「其他语言」,基本上是指的我已较熟悉的PascalC/C++C#Matlab等编程语言。

这份笔记并不能被当作教程使用,其中的内容穿插了很多与上面所言的「其他语言」的类比,因此思路比较混乱。发布在网上,主要是希望有经验的读者能给我提些中肯的意见。非常感谢您的耐心阅读。

0 Python之禅

在一切内容开始之前,应当拜读一下伟大的Python之禅:

>>> import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

教程上有很好的中文翻译,不过原文还是更有意思一些。【相关的注记,这里还没法写出来,只能在之后使用的过程中慢慢品悟了。

1 基本元素的处理

1.1 数值类型

python中的变量声明不需要说明变量类型,且必须初始赋值——以使得自动推断过程得以完成。首先是int整数类型:

a = 7   # a将是一个int
b = a   # b也是一个int

type()函数可获取变量类型。

>>> a = 7
>>> type(a)
<class 'int'>

数字间的运算有四则运算(特别地,整数除为//,实数除为/)、求模%和幂**(相当于一般意义上的^)。+=等缩略运算是可用的。特别地,divmod(a,b)将一次性返回a//b, a%b

python中可用的基数,除十进制外还有二进制0b、八进制0o、十六进制0x

可以用int()将浮点数、整数字符串(如'100')、布尔型(True->1,False->0)转成整数。python可以处理很大的整数,如:

>>> googol = 10 ** 100
>>> googol
10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

可以用float()将整数、小数字符串、指数字符串(如'1.0e4')、布尔型转成浮点数。可以用str()将数字、布尔型转成字符串。

1.2 字符串类型

字符串分普通的('XXX'"XXX")及换行字符串('''XXX'''"""XXX""")。单双引号都允许的原因是,可以在一种引号括起的字符串中无转义地应用另一种引号标记。另,换行字符串的用法为:

>>> poem = '''朝辞白帝彩云间,
... 千里江陵一日还。
... 两岸青山相对出,
... 孤帆一片日边来。'''

对以上的换行字符串,在控制台下直接输入poem将在输出中将换行符以转义符\n不换行地输出。用print(poem)命令,则将换行(这是我们期望的)。常用的转义字符有:\'\"\\\n\t

对字符串,常用的运算有:拼接+、复制*(后接重复次数)、指定下标提取[]。对于提取运算,python中的下标与C/C++类似,是从0~n-1。特别地,下标-1代表最后一个字符,-2则为倒数第二个,以此类推。在python中,不能利用提取运算直接修改指定位置的字符,但可以用replace()函数来操作:

>>> name = '刘伟'
>>> name.replace('刘','大张')
'大张伟'

最常用的字符串处理手法是分片操作。一般的格式是[start:end:step],即起点-终点-步长,提取由startend-1(不是end)间按步长step所取的字符。整个字符串相当于是[0:n:1]。省略startend,将默认从开头取、到结尾停。省略step,则取默认值1。例如:

>>> str[:]      # 提取整个字符串
>>> str[1:]     # 提取从第2个字符开始的整个字符串
>>> str[:5]     # 提取到第5个字符为止的整个字符串
>>> str[1:5]    # 提取第2-5个字符
>>> str[1:5:2]  # 提取第2、4个字符
>>> str[1:-3]   # 提取第2个到倒数第4个(而不是倒数第3个)字符

除此以外,分片操作允许起始点位置互换(从而有负的偏移),也容许下标越界(这时将在越界一侧取为边界下标)。例如str[::-1]将返回将str整个字符串逆序的结果。

其他的字符串处理函数还有:长度len()、分割split('X')(参数是分隔符)、合并join()(用法如:','.join(str_list))。其他一些函数在以下范例中给出,其中poem仍然是之前的那首《早发白帝城》:

>>> print(poem)
朝辞白帝彩云间
千里江陵一日还
两岸青山相对出
孤帆一片日边来
>>> poem.startswith('朝')    # 第一个字符是'朝'
True
>>> poem.endswith('来')      # 最后一个字符不是'来',是'。'
False
>>> poem.find('山')          # 查找第一个'山'字的偏移量
21
>>> poem.rfind('一')         # 查找最后一个'一'字的偏移量
29
>>> poem.isalnum()           # 这个字符串中不全是字母和数字
False

2 列表、元组、字典、集合

右四种皆为Python中的基本数据结构。一般的表示方法是:列表(list)为[...],元组(tuple)为(...),字典(dictionary)和集合(set)均为{...}。特别地,集合有时也用set()来标记,以与字典相区别。

2.1 列表

列表元组是类似的,最大的区别在于:列表是可改变的,而元组是不可改变的(类于字符串)。它们都是有序结构。

可以利用方括号[]直接按元素创建列表(特别地,若括号中什么也不填,则将创建一个空的列表),也可以用list()函数创建或转化列表。list()可以接受本节所涉及的四种数据结构,还可以接受字符串(将得到单字符列表)。在前一节中提到的字符串函数split()也可得到一个列表。

注意,list()的参数若为无序的字典、集合,则得到的列表之序应为字典、集合中的默认顺序。(但是这个默认顺序是什么??)

可以使用下标[n]访问、修改列表中的指定元素。该方法不允许越界,否则会报IndexError错误。[start:end:step]方式的分片仍然适用。此外,列表中的元素也可以是列表、元组等等,这一特性使得其他编程语言中的多维数组(大家都有)、交错数组(例如C#中的)得以实现。例如:

>>> China = ['Mainland', 'Hong Kong', 'Macau', 'Taiwan']
>>> east_Asia = [China, 'Japan', 'Mongolia', 'North Korea', 'South Korea']
>>> east_Asia
[['Mainland', 'Hong Kong', 'Macau', 'Taiwan'], 'Japan', 'Mongolia', 'North Korea', 'South Korea']
>>> east_Asia[0][1]
'Hong Kong'

列表的常用函数有:

  • append()将元素添加在列表尾部。
  • extend()函数或+=运算可将两个列表合并。(注意,这是一个procedure,将直接改变原列表!)
  • insert(index, obj)在指定偏移量的位置上插入新元素。
  • del语句用于清除指定的元素。
  • remove(value)函数清除列表中第一个(而不是所有)具有value值的元素。
  • index(value)返回具有value值的第一个元素之索引。
  • in作为一个关系运算符,用于判定元素是否在列表中。
  • count(value)返回value值在列表中出现的次数。
  • join()函数可将列表元素连接为为字符串(前面有涉及)。
  • sort()函数可将列表排序。(用的什么算法呢……)这是procedure,会直接改变原列表。
  • sorted()函数则不是procedure,其返回列表排序后的结果,不改变原列表。
  • len()当然用来得到列表长度(或者说元素个数)。
  • copy()函数用来将原列表的值复制到新列表对象中。它与=运算符的区别是:copy()按值传递,两份列表之间没有关系;=按引用传递,两份列表共用一套数据。

以上的用法中比较特殊的是del,它是一个语句而不是函数,用法如:

>>> fruits
['apple', 'pear', 'tomato']
>>> del fruits[2]
>>> fruits
['apple', 'pear']

书上对于这种操作的解释是:「del就像是赋值语句(=)的逆过程,它将一个Python对象与它的名字分离」,看起来这就像是将一个变量和它对应的句柄/地址分离。

至于join()函数的奇怪用法,作者的解释是:为了使得这个函数也能够方便地用于元组、集合等其他数据结构,就把join()函数定义为分隔符——即字符串的函数,而不用对几种数据结构分别定义一个join()函数。(在一种类型里重载,比在多种类型里面各写一个,还是好管理的多。)

>>> words = ['you', 'and', 'me']
>>> sentence = ','.join(words)
>>> print(sentence)
you,and,me

2.2 元组

元组的「构造函数」是tuple()。除此以外,也可以用()中填入元素来生成一个元组。创建元组时,括号是可省略的;每一个元素后应跟着一个逗号,不过在元素数大于1时最后一个元素跟着的逗号可以省略。比如:

>>> tuple_a = 'one',
>>> tuple_a
('one',)
>>> tuple_b = 'one', 'two'
>>> tuple_b
('one', 'two')

元组与列表的本质区别在于:元组的构成是不能改变的。因此,它除了不能用列表中那些改变列表构成的函数(比如append()sort()——但是sorted()却可以用!)之外,别的和列表一毛一样。

2.3 字典

字典又被称作关系型数组(它可被视为一张简单的关系型数据表,仅有两个字段且一个为键)或Hash表(数据结构课程中)。利用{}可按元素创建字典,其中填入的元素是形如key:value键值对,例如:

>>> symbol_dict = {
... 'C': '*' ,
... 'C++': '>>' ,
... 'TeX': '\\' ,
... 'Pascal': ':=',
... 'Python': '    ',
... }
>>> symbol_dict
{'C': '*', 'C++': '>>', 'TeX': '\\', 'Pascal': ':=', 'Python': '    '}

或者,也可用「构造函数」dict()来转化字典。该函数只接收所谓的双元素序列,比如[('C','*'),('C++','>>')],其中的任意一对[]()可以互换。每一对元素中,前者为键而后者为键值。

字典中的各种操作包括:

  • 并没有一个为字典添加元素的函数——因为有一种更方便的方法。在任何情况下,都可以用[key]得到字典中某一个键的值,也可用其修改键值;如果没有对应的键,那么这种方法就可以用于创建新的键值对——只需要假装有这样一条键值对,然后给其赋值就行了。
  • update(up_dict)接受收一个字典对象up_dict作为参数,并将其补充到现有的字典中去。如果其存在着与现有字典相同的键,新的将覆盖旧的。
  • del语句可用于删除具有指定键的元素,用法是:del the_dict[key]
  • clear()用于删除所有元素,与之等效的做法是= {}
  • in运算可用于判断归属与否,用于判断的依据是键(而不是键值)。例如,设有字典
    symbol_dict = {'C':'*','C++','>>'}
    

    那么就有

    >>> 'C' in symbol_dict
    True
    >>> '*' in symbol_dict
    False
    
  • keys()函数可得到字典的所有键。但是,这返回值并不是一个简单的列表对象,而是dict_keys()这样一种对象,是键的迭代形式。还需要再用list()函数将其转化为一个列表。例如有这样一个字典dict
    >>> dict
    {'me': '我', 'you': '你'}
    >>> dict.keys()
    dict_keys(['me', 'you'])
    >>> list(dict.keys())
    ['me', 'you']
    

    作者对于这种做法的解释是:用迭代器来储存,而非将其转化为一个列表,在处理大规模字典时可以节省时间与空间。

  • values()函数可得到字典的所有键值,items()函数可得到所有的键值对(用二元的元组储存)。与keys()函数一样,它们也得到迭代器——分别名为dict_keys()dict_items(),需要用list()将其转换为列表对象。
  • copy()函数按值将一个字典的数据传递给另一个,=运算符则可实现按引用传递。这与在列表中的情形是相同的。

2.4 集合

最后一个基本数据结构是集合,其可被看作是没有键值(或者说,键值不重要)的若干键构成的字典。使用{}就能创建集合,这里与字典区分的依据是:集合中输入的元素是若干离散的对象(如在列表[]和元组()中一样),而字典的大括号中输入的是键值对。当然,也可以用「构造函数」set()转换生成集合,被转换的可以是列表/元组,也可以是字典——这时将把字典中各键的键值剥离,只保留字典的所有键作为集合的元素。

创建空集合的方法是= set(),输入= {}得到的却是空字典。

在数学中,集合具有并、交、差等若干中运算,且这正是其最重要的特质。在python中,情形是相同的。集合对象可实现的运算包括:

  • in运算与在其他数据结构中的作用是一样的($\in$)。
  • &运算符可实现集合间的交运算($\cap$)。
  • |运算符可实现集合间的并运算($\cup$)。
  • -运算符可实现差集运算。
  • ^运算符可实现集合间的异或运算。(除了与计算机打交道的那一部分以外,数学家们看起来对这种运算是不太关心!)
  • <=运算符表示子集关系($\subset$),<运算符表示真子集关系($\subsetneqq$)。此外,issubset()函数也可以判断子集关系。

集合似乎是不可更改的,得到新集合的方法只能是以上所给的这些运算。

通过以上四种基本数据结构的组合,就可以设计出满足实际需求的复杂数据结构。例如,要设计一张世界地图,可以分层次考虑:

  • 在地球上最高层次的地理划分,大概就是七大洲了。七大洲的概念与国家、政体的变迁无关,是相对固定的地理概念,因此这一级别上应该用元组来表示。
  • 每一大洲中,可以再划分亚区域,比如亚洲就可分为东亚、东南亚、南亚、西亚、北亚等等。为了简便起见,舍去这一层次。
  • 那么,从属于每一大洲的就是具体的国家。国家是可能发生变化的,应该用列表。
  • 国家之下,是各国的行政区划。这也是可变更的,也应该用列表。
  • 行政区划同样有很多层次,可以再向下划分。

代码就不给了,因为我懒得写,懒得去查资料、敲数据。

3 代码结构

3.1 注释与断行

现在是扩充代码长度的时候了。首先,#符号代表注释,但在字符串中的#号不能起到引导注释之用。前面已经用过很多次了。字符串中的转义号\,在字符串以外则可以起到提示续行的作用,有如Matlab中的...符号。比如:

>>> father_words = '我去买几个橘子, \
... 你就在此地,\
... 不要走动。'
>>> print('父亲说:' + father_words)
父亲说我去买几个橘子你就在此地不要走动

3.2 if、while、for语句

接下来是家喻户晓的ifwhilefor(还好python没有自创一套语句结构,真令人欣慰)。首先是if,它可以与elif(相当于其他语言里的else if嵌套)、else语句组合使用:

>>> mi = 355 / 113  # 密率 ≈ 3.1416
>>> yue = 22 / 7    # 约率 ≈ 3.1429
>>> if mi > yue:
...     print('密率大于约率')
... elif mi < yue:
...     print('密率小于约率')
... else:           # 即 mi == yue
...     print('密率等于约率')
...
密率小于约率

Python中,以缩进标记代码结构的层次,python中的每一级缩进就如同在C/C++中的每一对大括号一样。一般建议用空格(而非Tab 键或二者混用)来敲出缩进,这样能够避免缩进层次的混乱;诸如if语句之类所导致的语义性缩进,一般以4个空格为单位进行,其他情况(如代码换行等)的缩进量应尽量与4的倍数错开。if语句之间可以相互嵌套。特别地,在python中允许对大小比较应用简写,即5 < x < 10这样的表达式是合法的。

关于Python中的False:除了布尔类型变量取False值外,空的迭代变量(包括空列表/元组/字典/集合/字符串)、取0的数值类型和null类型在if语句中的条件也会被视为False,即条件不成立。

接下来是while循环,一样的简单,在whlie关键字后放上条件即可。比如,这是一个模拟驱车回家的例子:

>>> count = 5
>>> while count > 0:
...     print('离家还有' + str(count) + '公里')
...     count -= 1
...
离家还有5公里
离家还有4公里
离家还有3公里
离家还有2公里
离家还有1公里

与在其他语言中类似,while循环可以用break打断或用continue步进。特别地,while语句还可以与一个else语句搭配,后者的内容仅在while循环正常结束(即不被break打断)时执行。仍用上面驱车回家的例子:

>>> count = 5
>>> while count > 0:
...     print('离家还有' + str(count) + '公里')
...     count -= 1
... else:
...     print('到家了!Ah~~')
...
离家还有5公里
离家还有4公里
离家还有3公里
离家还有2公里
离家还有1公里
到家了Ah~~

假如在while循环内加上一句if count == 3: break,那准是半路上出事了,到不了家。作者写道:「……(else语句)可以认为是循环中没有调用break后执行的检查。」

最后是for循环。Python中的for完全脱胎换骨,全然以「可迭代对象」为基础,相当于C#中的foreach语句。其用法为:

for variable in iterable: ...

其中iterable为一个可迭代的对象——诸如列表、元组等,而variable则是遍历iterable的变量。例如,利用for循环周游「四海」:

>>> ocean_tuple = '太平洋', '大西洋', '印度洋', '北冰洋'
>>> for ocean in ocean_tuple:
...     print('我来到了' + ocean + '!')
...
我来到了太平洋!
我来到了大西洋!
我来到了印度洋!
我来到了北冰洋!

对于其他语言里的for循环所常用的整数迭代,在python中可用range(start,stop,step)函数实现,其三个参数的含义与列表/字符串的切片类似,返回一个按指定规则生成的等差整数列,亦可以省略参数。除此以外,另一个生成迭代对象的函数是zip(),它可以将若干个列表/元组「打包」为一个并行的迭代对象,就像拉链将衣服两边的链牙拼合在一起:

>>> names = ['张三', '李四', '王五']
>>> scores = [90, 100, 85]
>>> ranks = ['A', 'S', 'B']
>>> for name, score, rank in zip(names, scores, ranks):
...     print(name + '考了' + str(score) + '分,等级为' + rank)
...
张三考了90分,等级为A
李四考了100分,等级为S
王五考了85分,等级为B

注意,以上的name, score, rank其实是一个迭代用的元组,而迭代对象zip(names, scores, ranks)的结构则是双层嵌套。zip()所生成的对象为zip类型,不可显示,需使用list()等函数才能转化为可见结构。特别地,若zip()的各参量长度不等,其将以最短者为准,其他参量多余的部分将被忽略。

利用zip()函数,可以很轻易的将两个列表/元组合成一个字典。例如,两列表list_alist_b的元素已经按序一一对应,则由dict(zip(list_a, list_b))便可得到由它们合成的字典。

2.3 推导式、生成器

虽然Python中的for循环已经很方便了,但是在其中创建列表等对象时还有更方便的方法——推导式。以列表为例,其推导式为

new_list = [expression for item in iterable if condition]

在推导式中,for item in iterable如同for循环一样,表示对iterable对象中元素item的遍历;其前面的expression是一个与item有关的表达式,最终填入列表的便是由这表达式确定的值(当然,也可以与item无关,这样就得到一个各元素相等的列表);其后的if语句则代表一个条件,仅有使if条件为Trueitem所对应的expression才会被添入新列表中——当然,expression在原则上也应与item相关。可以将上面的推导式用一个一般的for循环解释为:

new_list = []
for item in iterable:
    if condition:
        new_list.append(expression)

此外,如果要使用字典、集合所对应的推导式,则只要将方块号改为大括号就好了——当然,字典得改写成键值对的形式。这就是

new_dict = {key_expression : value_expression for item in iterable if condition} # 字典推导式
new_set = {expression for item in iterable if condition} # 集合推导式

元组没有推导式(也许是因为元组不可修改?),用表达式

(expression for item in iterable if condition)

所创建的是所谓的生成器(generator)。它的特性是:

  • 它是一个可迭代对象。在迭代过程中,生成器遵守的原则是:每次迭代生成器时,其会记录上次调用的位置,并返回下一个值。这样,并不能利用索引访问生成器的指定位置元素,而只能不断的迭代它。(当然,可以用list()函数实现将其变成一个可处理的列表。)
  • 生成器不储存在内存之中,迭代结束后其便被清空了。

因此,可以把生成器比作一沓便签纸,只能从顶上往下一张张的用(你也不知道用到哪一张了),用完就没了。它的优势是:不需要特别处理,节省内存。在自定义函数中,可以指定返回值为生成器对象,下面会提到。

4 函数

4.1 函数的定义

Python中定义函数需用关键字def。例如,创建一个最简单的函数:

def my_function(variable):
    pass

上面的pass命令表示什么也不做,可以将其换成其他有效的命令以执行指定任务。注意要用冒号:和统一的缩进标志出函数体的范围。函数可以没有参数(这样,函数名my_function的后面需要放上一对空括号),可以有返回值(使用return命令),也可以没有返回值——如同这里的例子一样。

什么也不做的函数太乏味了,现在写一个能做些事情的函数。比如,书中提供了一个echo()函数,它能将一个字符串「反射」成两个:

def echo(words):
    return words * 2

试试看:

>>> my_words = '大山你好啊!'
>>> echo(my_words)
'大山你好啊!大山你好啊!'

当然,这个函数可以再改进。比如,想象现在有一位小朋友在大山里喊话,山离小朋友很远,回声应该不止两道,可以有更多道——这样,可以增加一个反射次数times的参数:

def echo(words, times)
    return words * times

这样就更令人满意了,诸如此类。(自动推断变量类型真好!)特别地,如果函数没有返回值,那么默认返回一个None——也就是说,所有函数都有返回值。

>>> def do_nothing(): pass
... 
>>> print(do_nothing())
None

特别地,函数可以返回生成器对象,做法是:将返回值的return关键字改成yield;这时,yield在带来返回值的同时并不会退出函数(所以不必将其放在函数的最后),它会将yield结果依次叠放,并最终获得一个生成器。比如,可以自己重定义一款range()函数:

def my_range(start, stop, step):
    temp = start
    while(temp < stop):
        yield temp
        start += step

最后,可以为函数撰写说明性的字符串(即函数的说明文档,如同C#中的\\\注释),其位置在def语句的正下方,函数体开始之前。可以用单行字符串,亦可用多行字符串。

def foo():
    '这个函数什么也不做!'
    pass

4.2 参数默认值

可以为函数中的一些参数指定默认值

def echo(words, times = 2):
    return words * times

这样,若在别处写echo(words),得到的就是words复制两次的字符串。在使用默认值时,需要注意一点:

默认参数值在函数被定义时已经计算出来,而不是在程序运行时。

比如说:

>>> def add_a_item(the_list = [], the_item= '!'):
...     the_list.append(the_item)
...     return the_list
...
>>> add_a_item()
['!']
>>> add_a_item()
['!', '!']

注意到,尽管声明了参数the_list的默认值为空,但第二次按默认值调用add_a_item()the_list却已经是第一次调用结束时的非空列表['!']了。因此,在Python不推荐用可变的列表、字典等作为参数的默认值。

4.3 位置参数、关键字参数与可变参数

按照教程的说法,Python的参数处理方式「更加灵活」。一般情形下,声明的函数参数被称为位置参数——即实参与形参之间按顺序一一对应。这样的弊端是,有时候会弄错顺序——这就完蛋了。作为一种对策,Python提供了另一种名为关键字参数的选项。以之前声明的echo(words, times)为例,现在我忘记wordstimes两个参数谁先谁后了(真笨),为了避免出错,可以这样调用函数:

>>> print(echo(times = 3, words = '大山你好啊!'))
大山你好啊大山你好啊大山你好啊

此即关键字参数。调用参数时,可以指定对应参数(形参)的名字,顺序亦可以打乱。以上两种参数类型可以混用(何必呢……),位置参数优先。

在函数定义时,可以利用*收集数量可变的位置参数(Python没有指针!),而利用**收集数量可变的关键字参数,它们的功能相当于C#中的params关键字(当然也有区别,如C#中并没有关键字这样的机制)。例如,想要声明一个对任意多个数求和的函数;在Python中确实有对任意长度列表/元组求和的sum()函数(这样,其参数是一个列表/元组),可惜没有对任意多个参数求和的函数。为此,写一个新的sum_all()函数:

def sum_all(*numbers):
    return sum(numbers)

这样,就可以用sum(1,2,3,4)求四个数的和,诸如此类。在这里,*numbers得到了各位置参数构成的一个元组。利用**收集关键字参数时,得到的则是一个字典,这是好理解的:

>>> def print_dict(**keys):
...     print(keys))
... 
>>> print_dict(me = '我', you = '你')
{'me': '我', 'you': '你'}

若将***混用,则函数会按它们的先后顺序解析参数。特别地,如果只是单纯接受可变个参数而不使用,则可在参数表中直接用***符号接受,不需要为参数表命名。最后,显然地,这类可变参数最好在定义中置于参数表的末尾(以免引起歧义);如果要将其放在中间,也仍然可以使用,但这时在***参数表之后的参数就只能用关键字来给定了。

4.3 函数是对象

在教程中,作者将函数比作是Python世界中的一等公民:它可以作为返回值赋给变量,可以作为参数用于其他函数中,亦可以在其他函数内取得返回值。此外,函数也是对象——只要将函数标识符跟着的大括号去掉,它就表现为一个普通的对象。比如,做一个有意思的事情:

>>> print(print)
<built-in function print>

我们将print这个函数作为一个对象,用print打印出来,结果显示它是一个系统内置函数。又比如自定义的函数,例如之前声明的echo()函数:

>>> print(echo)
<function echo at 0x0000000001DCC1E0>
>>> type(echo)
<class 'function'>

可见它是一个function对象。Python中的函数是如此自由,以至于它可以如其他的对象——目前为止都可被称作变量——一样,直接作为参数传入函数,并正常的使用:

>>> def use_func(func, item):
...     func(item)
...
>>> use_func(print, '哈哈哈哈哈哈哈哈')
哈哈哈哈哈哈哈哈

这里,函数print()被作为参数传入use_func()函数中,并在其内部被调用。既然函数是一种对象,那么其也可以作为列表/元组、字典、集合等的元素。特别地,作者提到:

函数名是不可变的,因此可以把函数用作字典的

可以写一本「函数字典」喽。(在Python中,确有对应的实现。)

4.4 内部函数与闭包

Python中,可以在一个函数内定义另一个函数(到目前为止,我只在Pascal里见过这样的事情):

def apply(document):                         # 要提交一份申请
    def dean_apply(dean_document):           # 最后当然是由教务处批准
        def college_apply(college_document): # 但在此之前学院也要批准
            return college_document + ':学院批准通过!'
        return college_apply(dean_document) + '教务处批准通过!'
    return dean_apply(document) + '申请成功!'

这样,调用函数就得到:

>>> print(apply('加里敦大学退学申请'))
加里敦大学退学申请学院批准通过教务处批准通过申请成功

在内部函数机制下,还可实现一种特殊的功能——闭包。闭包是指这样的一些内部函数,在其中直接(未经参数)引用了其外部的变量。比如,写一个两层转述的函数:

def repeat(words):
    def speak():
        return words + '!'
    return speak

内部函数speak()直接引用了其外部的words变量。在其他语言中,这是不允许的;而在Python中,编译器会将speak()函数与其「引用环境」——即其外部所受的约束,如words的取值——打包起来一起使用,此曰闭包。这样,speak函数即可调用外部repeat()函数的变量words。此外,repeat()函数中未调用speak()函数,而是直接返回了speak函数对象。

闭包的优势在于:由于其与引用环境关联的特性,每次以不同参数调用闭包时都将获得独立的函数,数据互不干涉且持久存放于内存中。这与一般的函数不同——它们的变量都是局部的。在使用闭包的情况下,每一次函数运行时的参数值都是上一次运行结束后剩下的——这可帮我们省去变量储存的功夫。(但是,为每一份闭包所开辟的存储空间可没有省去,因此过度使用闭包将有内存泄漏的风险。)除此以外,其为引用环境创建副本的特性,允许我们从一个闭包出发创建不同「档位」的函数——或者说,创建一类函数,它们之间的差异由闭包接受的参数所控制。

4.5 匿名函数

可以使用lambda函数方法缩记一些不太复杂的函数。例如,要写一个参数为实数x的函数,它返回一个说明其平方值的字符串。通常的写法是:

def show_sqr(x):
    sqr = x * x
    return 'The square of ' + str(x) + ' is: ' + str(sqr)

lambda方法,则可以缩记为:

show_sqr = lambda x:'The name of '+str(x)+ ' is: '+str(x*x)

这里,lambda指明后面内容的性质,x相当于一个形参,冒号后是返回的表达式(这和推导式的机制有些类似)。由于这里没有指定函数的名称(show_sqr是另一个获得了该函数的对象之标识符),故这种函数被称作匿名函数。它的确能直接被使用:

>>> (lambda x: x * x)(4)
16

但更合适的选择是将匿名函数用在函数声明或将函数作为参数时。比如,上面的一段话按照Python的习惯应当写为:

>>> sqr = lambda x: x * x   # sqr成为一个函数对象
>>> sqr(4)
16

Python的匿名函数声明与Matlab大同小异,只不过后者是以@字符代替lambda关键字来生成匿名函数的。

4.6 装饰器

装饰器(decorator)是一种这样的函数:其将一个函数作为输入,并返回另外一个函数。(它把一个函数加工成另一个——或者说,它是真正意义上的「函数之函数」。叫它「泛函」怎么样?)尽管名字似有关联,但它和前面的迭代器、生成器并不一样,Python中并没有专门的decorator类型;其是通过具体的代码实现的,是一种语义上的定义。装饰器可以用来在不改变原始函数的情况下增强其功能,也可用于调用、测试原始函数。

例如,我想看看一个未知的函数有哪些参数、参数的名称,可惜现在不能上网,我也不知道Python是否提供了这样的帮助(其实是有的,比如用help()函数)。因此,(教程的作者)自建了一个这样的提供帮助的函数:

def document_it(func):
    def inner_func(*args, **kwargs):
        print('函数名称:', func.__name__)
        print('位置参数有:', args)
        print('关键字参数有:', kwargs)
        result = func(*args, **kwargs)
        print('返回值为:', result)
        return result
    return inner_func

这里用到了闭包(为了对不同的函数分别创建引用环境副本)、可变参数(为了适配具有不同参数表的函数)等技巧。如同之前对闭包做的一样,可以手动为装饰器赋值并运行:

>>> dec = document_it(print)
>>> dec('我是参数')
函数名称: print
位置参数有: ('我是参数',)
关键字参数有: {}
我是参数
返回值为: None

这种做法略显麻烦,与其用装饰器还不如写一个普通的函数代替之。事实上,装饰器可以用@修饰符来简化调用:

>>> @document_it
... def add_int(a, b):
...     return a + b
>>> add_int(5, 10)
函数名称: add_int
位置参数有: (5, 10)
关键字参数有: {}
返回值为: 15
15

这样就简洁很多,如果是想在程序中用装饰器来debug就非常方便啦。

此外,一个函数也可以有多个装饰器,靠近函数定义的先执行。如创建另一个将结果平方的装饰器:

def square_it(func):
    def inner_func2(*args, **kwargs):   # 注意这个内部函数的名称与之前的不同
        result = func(*args, **kwargs)
        return result * result
    return inner_func

现将之前所列的两个装饰器按两种顺序分别调用。先让square_it在前运行:

>>> @document_it
... @square_it
... def add_int(a, b):
...     return a + b
...
>>> add_int(5, 10)
函数名称: inner_func2
位置参数有: (5, 10)
关键字参数有: {}
返回值为: 225
225

注意到返回值确实被施以了平方运算。特别地,此时函数的名称变为了inner_func2,因为这时的document_it装饰器所调用的函数参数func是先经装饰器add_int所装饰过的,因此得到的就是其闭包内的函数inner_func2(可以理解为,函数没有透视术,只看得见它调用的第一层实参)。再换个顺序调用:

>>> @square_it
... @document_it
... def add_int(a , b):
...     return a + b
...
>>> add_int(5, 10)
函数名称: add_int
位置参数有: (5, 10)
关键字参数有: {}
返回值为: 15
225

这里倒是看起来一切正常了。如果我们在square_it装饰器里,也放上一条打印函数名称的命令,那么打印出来的应该是装饰器document_it的内部函数inner_fun

5 模块化与面向对象

5.1 命名空间

Python中,命名空间是指程序中的一段区域,在其中某个名称(标识符)是唯一的。不同的命名空间互不相关,每一个命名空间可以被看作是一个字典:其中的键是若干个标识符名称,而它们的键值是他们所指代的对象。

具体而言,主要的命名空间有两类:一类是全局(global)命名空间,由每个程序的主要部分所定义;另一类是局部(local)命名空间,它位于每一个函数之中。这样,在程序的主要部分中调用变量时,编译器将认为被调用的变量是全局命名空间中的;而若在函数内引用变量,编译器将认为其是局部命名空间的。

想要在函数以内调用并修改程序主要部分的变量(即全局变量),就需要在调用变量前用global关键字进行声明。例如以下的代码

def print_variable():
    print(variable)     # 编译器将识别其为全局变量
    variable = 2        # 晕了!
    print(variable)

variable = 1            # 声明了全局变量variable
print_variable()

在执行时将会报UnboundLocalError异常,因为这时程序试图在局部命名空间内修改全局变量。那么如果将print_variable()函数中的第一个print()去掉呢?这时的程序变为

def print_variable():
    variable = 2        # 编译器将创建新的局部变量variable
    print(variable)     # 此时井水不犯河水了

variable = 1            # 声明了全局变量variable
print_variable()        # 输出的是局部变量variable

此时程序不会报错,但在print_variable()函数中出现的variable将被看作是一个新创建的局部变量,因而与主程序中的variable变量没有任何关系了。可以使用id()函数确认这两个变量并不相同。

为了能够在函数中修改全局变量,避免以上出现的报错和混淆,应使用global关键字告诉编译器,variable是一个需要修改的全局变量:

def print_variable():
    global variable      # 确认一个全局变量
    print(variable)     # 输出当前值
    variable = 2        # 修改全局变量的值
    print(variable)     # 输出改变后的值

variable = 1            # 声明了全局变量variable
print_variable()        # 输出的是局部变量variable

此时编译器将先后输出12,由此达成了目标。

5.2 访问命名空间对应的字典

Python中,可以通过locals()globals()函数得到局部命名空间和全局命名空间所对应的字典。例如,在编写了以上的print_variable()函数之后的界面中打印全局命名空间所对应的字典,将得到:

>>> print(globals())    # 以下的输出内容是整理过的,实际显示时并没有换行
{'__name__': '__main__', 
'__doc__': None, 
'__package__': None, 
'__loader__': <class '_frozen_importlib.BuiltinImporter'>, 
'__spec__': None, 
'__annotations__': {}, 
'__builtins__': <module 'builtins' (built-in)>, 
'print_variable': <function print_variable at 0x0000000001D8C1E0>, 
'variable': 2}

可以看到,在字典中有诸如系统变量__name__、文档字符串__doc__等的系统内建变量,以及我们声明的print_varible()函数及variable变量。访问局部命名空间的字典时,所得的结果会更干净一些。

5.3 模块的引用

模块是指被链接、引用到一个Python程序中的其他Python程序,在概念上与其他语言中的库文件类似。(但又有区别:Python中的模块并不是以专门的格式实现的,模块与源代码同样是py文件,同样可以运行。)例如,在同一文件夹下,分别有两个Python代码文件:

  • 一个名为toolkit.py,其中声明了一个名为tool()的函数。
  • 另一个名为usetool.py,需要调用前一文件中的tool()函数。

此时,在usetool.py中,可以使用import关键字将toolkit.py作为一个模块引入:

import toolkit

这样,在程序中,只需要用toolkit.tool()命令就可调用在toolkit中已经写好的tool()函数。使用点语法标记模块来源,是为了避免多个模块中有重名的函数或变量。

如果在程序中,并不担心重名的问题(例如说,我确认在程序中不会引用两个都有tool()函数的模块),则可进一步的使用

from toolkit import tool

此时,主程序中的tool()命令将始终被看作是来自于toolkit模块的,不需要再使用点语法修饰。

import语句可以出现在代码中的任何位置。如果它出现在文件的开头,则对模块的引用将在整个程序中生效;如果它出现在for循环或自定义函数之中,则对模块的引用也便只在对应的区域生效,有如变量的定义域。

特别地,可以为引用的模块声明一个别名,以简化引用的过程:

import toolkit as tk

此时,在程序中只需要写tk.tool()就好了。(不过,说起来,tkPython之标准GUI库Tkinter的缩写……)

有时候,程序员会希望一份py代码实现双重功能:既能够被其他的程序作为模块引入、调用,又能够直接作为一份源代码被运行。在这种情况下,这份代码的「正文」(即不希望作为模块被引入,但希望在运行它本身时被调用的内容)应放在一个这样的if语句之下:

if __name__ == '__main__':    # 如果该文件作为模块被调用,则此处的条件为假,内容将不会被执行
    # 以下是程序正文
    # ...

为了日后进一步开发的方便,即使是一份普通的py源码也可以同样采用这样的格式来编写:说不定哪一天,你随便写的那个加减乘除计算器就可以被开发成一个专业计算模块了呢?(好吧,这确实是不太可能。

5.4 包

有时需要将几个模块组织成一个有机整体——。具体做法是:

  1. 在同一目录下创建若干个py源码文件,它们中的每一个都可以作为模块使用。
  2. 必须)在该目录下创建一个名为__init__.py的源码文件,可以置空(学到目前的程度,我也不知道里面该写什么……)。
  3. 此时,这一个目录就构成了一个包,它的名称即是目录的名称。

例如,我们在一个名为toolbox的目录下,分别创建了两个文件:tool_a.pytool_b.py,它们都可以被作为模块使用。现在,需要在一个文件中同时使用这两个模块。除了分别用import命令引用以外,我们还可以按上面的步骤将toolbox组织成一个包,并在我的工程代码中使用;具体而言,以下几种引用方式都是可行的:

import toolbox  # 这种用法将引用toolbox中所有的模块
from toolbox import tool_a  # 这种用法仅从包中引用指定的模块
import toolbox.tool_a       # 与上面的命令类似,区别见下文

(注意这里的from命令跟之前的那一个相比,意义又有所不同了。)这时,在调用包内两个模块的文件时,仍然需要使用点运算符定界。如果是采用的上面的from...import命令引用模块,则只需要像下面这样调用模块中的函数:

tool_a.foo()    # 调用tool_a中的一个函数
tool_b.bar()    # 调用tool_b中的一个函数

如果是使用import toolbox.tool_a引用模块,则在正文中必须始终用完整的点语法来标识:

toolbox.tool_a.foo()    # 调用tool_a中的一个函数

包的好处在于:能将许多的模块有机的组织成一个整体,方便查找和分类。特别地,在包下还可以再创建一个包(嵌套),这和C++/C#中的名字空间(namespace,与Python中的对应概念并不相同)的创建规则是类似的。

6 类与对象

对象是所谓「面向对象」之编程语言的灵魂。对于Python,也并不例外。不过,由于Python所具有的解释性编译、丰富内置数据结构、模块与包机制等特性,类与对象在Python中的支撑地位就不是很明显了;换句话说,在Python中,类与对象的特性隐藏在这种语言最光鲜亮丽的那些表面特性之下,且程序员们在使用Python时也往往不需要直接和class打交道——这与JavaC#之类的「纯面向对象」语言有所不同。

6.1 类的定义

类的定义方式与函数类似,只需要将def关键字换为class即可。其他语言中所谓的「构造函数」,在Python中由__init__函数来实现:

class Object():
    def __init__(self, name):   # 构造/初始化函数
        self.name = name        # 将传入的初始值name赋给对象的name变量

这是一个最小的非空类的示例;如果不要求非空,则只需要在定义中写一个pass命令,则可创建一个最简短的类。这里用和其他具有类特性的语言——例如C#——作比较,以快速理清创建一个类时需要注意的问题:

  • 创建类时,仍然需要在类名后面留一对括号,如同函数一样——这个括号是用于继承的。
  • 对于实例函数(与对象实例相关的、非静态1的函数)而言,总是需要在参数表的开头放上一个self变量,它代表着届时实际使用该函数的实例对象,与C#中的this指针有点类似。
  • 在类的定义中,不需要事先声明其所具有的变量。在本例的__init__函数中,通过self.name就创建了一个属于该类之对象的变量name

特别地,如果在类的方法以外声明变量,例如下面这样:

class Object():
    number = 0
    def __init__(self, name):
        self.name = name

则这里创建的number变量将是一个类变量,而不从属于任何一个实例对象(在其他语言中,这被称作静态变量)。访问该变量时,需使用Object.number命令进行。

6.2 类的继承

如同前面所说的一样,在定义类时类名后的括号内填入父类名称,即可实现继承

class Bird():
    def __init__(self, name):
        self.name = name
    def say(self):
        print(self.name, 'is a bird!')

class Duck(Bird):   # Duck类继承于Bird类
    def say(self):
        print(self.name, 'is a duck!')
    def swim(self):
        print(self.name, 'can swim!')

可以运行以下试试看:

>>> bird = Bird('Tom')
>>> bird.say()
Tom is a bird!
>>> duck = Duck('Donald')
>>> duck.say()
Donald is a duck!
>>> duck.swim()
Donald can swim!

从以上的例子中可以看出,继承能实现以下三种特性:

  • 子类可以继承父类已有的变量和函数,例如Bird类中的__init__()函数和name属性直接为Duck类所继承,不需要在其定义中照抄一遍。
  • 子类可以重写/覆盖(override)父类的方法,例如Duck类中的say()函数与Bird中的并不相同。在Python中,重写父类方法时,并不需要做特别的声明。(在C#中问题则比较复杂,需要使用override等一些关键字指定重写时的若干细节。)
  • 子类可以在父类已有的方法之外拓展新的功能,例如Duck类中新实现了Bird类中所没有的swim()方法。

若需要在子类中显式访问父类函数——特别是子类想调用已被自己覆盖过了的父类函数时,可以用super()方法来获取父类中的定义。例如,要调用父类的初始化函数,则只需这样:

super().__init__(name)

这和C#中的base关键字类似。

6.3 关于类的属性,以及名称重整

Python中的所有属性变量均是公开的——也就是说,不能通过其他语言中那样的private关键字来阻止对于某个属性的外部访问。不过,仍然有其他一些方法来改善这个问题,避免对于属性变量的不当修改。

首先,可以给类的属性变量创建getset方法(尽管是非必需的),再将它们与对应的属性变量绑定起来。例如,要为上面已写的Object类之name变量创建一个并不朴素的gettersetter,则可如下操作:

class Object():
    def __init__(self, name):
        self.inner_name = name  # inner_name是「隐藏」的内部变量
    def get_name(self):         # 一个有些不同的getter
        return self.inner_name + '!'
    def set_name(self, input_name):   # 一个有些不同的setter
        self.inner_name += input_name
    name = property(get_name, set_name) # 将getter与setter捆绑给供公开的属性变量

此时,若在表达式中访问某一Object对象的name属性时,将自动调用get_name()函数;当某一Object对象的name属性出现在赋值运算符=的左侧(俗称「左值」)时,其将默认调用set_name()函数进行「赋值」。调用一下试试:

>>> obj = Object('Tom')
>>> obj.name
'Tom!'
>>> obj.name = ' and Jerry'
>>> obj.name
'Tom and Jerry!'

当然,这样来设置gettersetter没有什么实际意义。

除了用property()方法捆绑,另一个更「优雅」的方法是使用修饰符。它们分别是:

  • @property修饰符:用于指示getter方法。
  • @name.setter修饰符:用于制定name属性的setter修饰符。

可以使用修饰符对之前的Object类进行修改:

class Object():
    def __init__(self, name):
        self.inner_name = name      # inner_name是「隐藏」的内部变量
    @property                       # 创建getter
    def name(self):                 # 一个有些不同的getter
        return self.inner_name + '!'
    @name.setter                    # 创建setter
    def name(self, input_name):     # 一个有些不同的setter
        self.inner_name += input_name

要创建一个真正意义上的属性变量,getter是必需的。特别地,如果一个属性变量只有用@property声明的getter,而未用@xxx.setter修饰符声明setter,那么该变量将不能被修改——这样就可以部分地达到变量私有性。

无论是使用以上的哪种方法,都不能创建出真正的私有变量出来;例如,对一个Object对象obj,仍然可以使用obj.inner_name直接访问内部变量,而这是我们不希望发生的。为了阻止这件事情的发生,Python内有一项特殊的机制:使用双下划线__于标识符之首的变量,将被施行所谓名称重整的操作,在此情形下将不能在类的外部通过标识符访问该变量。

仍以上面的Object类为例,我们将原来用于隐藏,实则仍可被外部访问的「内部变量」inner_name,改名为__name,这样它就成了一个真正的内部变量了:

class Object():
    def __init__(self, name):
        self.__name = name          # __name是**真正**能隐藏的内部变量
    @property                       # 创建getter
    def name(self):                 # 一个有些不同的getter
        return self.__name + '!'
    @name.setter                    # 创建setter
    def name(self, input_name):     # 一个有些不同的setter
        self.__name += input_name

再来试试看能否访问__name变量?

>>> obj = Object('Tom')
>>> obj.__name
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Object' object has no attribute '__name'

此时编译器报错,看来是已经实现这个功能了。不过,注意报错的内容是「没有属性__name」,而不是「这个属性不能访问」,这里的原因是:Python并没有真的将这些变量用黑胶布封起来(就像其他那些有private关键字的语言一样),而只是给它换了个名字。教程的作者告诉了我们这个秘密:

>>> obj._Object__name
'Tom'

编译器不过是在变量名的那双下划线前,又加了一条下划线和类名(此为「名称重整」),以这个新名称来掩盖原来的名称。所以,这个机制是防止代码作者自己胡乱访问内部变量的,不是防止别人访问的——源代码要是在别人手里,什么不能修改?(另外,要注意,用这种方式访问内部变量时,不会调用它所对应的getter方法——这里的'Tom'并没有getter所附加的感叹号。)

6.4 类方法与静态方法

除开最常用的实例方法,在Python中还可以声明类方法静态方法。在其他语言中,可能没有「类方法」这一概念,它们的「静态方法」囊括了这里所说的两种方法。

类方法是指作用于整个类的方法,与类有关,只调用类属性而不调用实例属性。需要使用@classmethod修饰符来定义具体的类方法:

class Student():
    count = 0           # 在方法外部声明的变量,是类属性而非实例属性
    def __init__(self):
        Student.count += 1  # 实现计数功能
    @classmethod
    def total(cls):
        return cls.count

其中的cls参数与实例方法所用的self参数是类似的,只不过这里的cls等同于整个类,而不是某一个具体变量。类写好后,可以在不创建任何Student之实例对象的情况下调用Student.total()方法:

>>> Student.total()
0

现在在程序中还没有Student类的实例对象。创建一些Student类的对象之后呢?

>>> for i in range(100):
...     new_student = Student()
...
>>> Student.total()
100

现在新创建了100个学生,相应的也就改变了类方法total()的调用结果。

至于静态方法,则甚至不能调用类属性——它只能默默地执行那些与类、对象无关的功能,但又在结构上从属于一个类。从这个意义上说,静态方法相当于是以一个类为命名空间的普通函数。使用@staticmethod关键字来定义静态方法:

class toolkit():
    @staticmethod
    def say_something():
        print('Hello, world!')

在程序中只需要用toolkit.say_something()就可以调用这个静态函数。静态函数的实现效果,与类、对象的状况没有任何关系——可以用「身在曹营(类)心在汉(主程序)」来描述这种感觉。

下面是对于上面所述的三种方法之特性的概括。

方法类型 调用实例属性 调用类属性 必需参数 声明修饰符
实例方法 self
类方法 cls @classmethod
静态方法 @staticmethod

特别注意:以上所用的所有以@开头的修饰符,都只对其后紧跟的那一个函数起作用!

6.5 鸭子类型

鸭子类型Python中的一种机制:如果一个类满足了程序员所要求的所有功能——具体而言,是具有了所有给定名称的方法——那么无论这是什么类,无论它的这些方法具体做了什么,那么它们都可以被统一地调用。打个比方就是:

Python中,只要一个类声称自己会叫、会游泳……具有鸭子所必须的所有属性、方法,那么无论这个类叫做「变种鸭子」、「鸡」、「汤姆」或是「计算器」,无论它所定义的「游泳」方法是否有效,这个类都可以被流畅地运用于那些为真正的鸭子类所设计的场合,而不会如其他语言那些被判为类型错误。

多个类以不同地方式实现同一接口,本是多态的内涵之一,在各种面向对象语言中均有实现;但是,在这些语言中,都严格地要求:只有存在着继承关系的类之间才能实现多态。在Python中,这一要求被进一步削弱了:任何类之间的同名属性、函数,均可以实现多态,不需要继承关系。

例子不给了,网上很多。如果程序的结构非常清晰,这个特性甚至会被使用者遗忘掉。

6.6 魔术方法

魔术方法(magic method)或特殊方法(special method),是类所保留的一些默认方法,它们可以被使用者重载。对应于其他语言,Python中的魔术方法大概囊括了其他语言中的默认方法(比如C#中的ToString()方法)、运算符重载等多个功能;这里为方便起见,就按默认方法运算符重载两方面来描述魔术方法的具体使用。

首先是默认方法。在Python所创建的类中,最主要的默认方法有两个:str()repr()。这两个方法都是返回实例对象的相关信息(比如类型),不同之处在于:当在交互编译器中直接输入实例对象名时,调用的是repr()函数;而在用print()函数打印对象时,调用的则是str()函数。默认情况下,它们都返回标准格式的信息(因而看似没有差别),例如对于一个按之前的方法创建的Object对象来调用:

>>> obj = new Object()
>>> obj     # 此时调用的是obj.repr()函数
<__main__.Student object at 0x0000000002BB7EF0>
>>> print(obj)  # 此时调用的是obj.str()函数
<__main__.Student object at 0x0000000002BB7EF0>

所谓的魔术方法就是:你可以使用__str__作为函数名来重写这对应的str()函数,使用__repr__来重写repr()函数。【此处实例待补充】

同样的,可以用魔术方法来重写运算符。例如,可以用__eq__作为标识符来重写类的等于运算符==。重写的原因是:类所默认定义的等于关系是以对象本身为判断标准的,因而每两个对象之间都不相等;如果我需要以对象下属的某个属性(比如name属性)来判断对象之间的相等关系,就必须自己动手:

class Object():                         # 你是不是不想再看见Object这六个字母了……
    def __init__(self, name):
        self.name = name                # 而且你也不想再看见这个name属性了,不过下面有……
    def __eq__(self, other):            # 新东西!这里看开始重写等于运算符
        return self.name == other.name  # 以name属性作为判断相等的依据

注意,这里除了self,还有另一个参数other,它也是必须的,用来获取那==的右值。

我们来试试这新功能:

>>> obj1 = Object('Tom')
>>> obj2 = Object('tom')
>>> obj1 == obj2        # 两个变量的name属性值不同,结果为False
False
>>> obj2.name = 'Tom'   # 将obj2的name改成与obj1相同的
>>> obj1 == obj2        # 此时就返回True了
True

除开str()这样的默认函数和等于运算符,其他许多的运算符都可以用魔术方法来重写,最常用的一些如下表所示。所谓「被调用的场合」,就是指:执行对应的表达式,就相当于与其对应的魔术方法。

魔术方法名 被调用的场合
__eq__(self, other) self == other
__ne__(self, other) self != other
__lt__(self, other) self < other
__gt__(self, other) self > other
__add__(self, other) self + other
__sub__(self, other) self - other
__mul__(self, other) self * other
__truediv__(self, other) self / other
__str__(self) str(self)
__repr__(self) repr(self)
__len__(self) len(self)

更详细的内容,可以参见Python关于魔术方法的官方文档

  1. 按这里的说法,在其他语言中,大概只有「静态函数」和「实例函数」两种区分。在Python中,除了实例函数和静态函数,还有所谓的类函数,在后文有提到。