Notes about Python.

PythonGuido van Rossum1989年圣诞节期间,为打发无聊的圣诞节而编写的编程语言。Python的主要优点是“优雅”、“明确”、“简单”。Python缺点包括:

  1. 运行速度慢Python解释型语言,代码在执行时会一行一行地翻译成CPU能理解的机器码,这个翻译过程非常耗时;而C语言等编译型语言,运行前直接编译成CPU能执行的机器码,所以非常快。
  2. 代码不能加密:发布Python程序,实际上就是发布源代码,这一点跟C语言不同;C语言不用发布源代码,只需要把编译后的机器码(也就是Windows上常见的xxx.exe文件)发布出去。

本文目录:

  1. Python的基础语法:输入输出、
  2. Python的数据类型:整数、浮点数、常量、变量
  3. Python的数据结构:字符串、
  4. Python的函数式编程:
  5. Python的面向对象编程:
  6. Python的模块:

1. Python的基础语法

Python使用缩进来组织代码块,请务必遵守约定俗成的习惯,坚持使用4个空格的缩进。在文本编辑器中,需要设置把Tab自动转换为4个空格,确保不混用Tab和空格。

(1)输入和输出

⚪ 输入:input()

input()可以让用户输入字符串,并存放到一个变量里,返回的数据类型是字符串str;可以显示字符串来提示用户:

name = input('please enter your name: ')

# 输入一个整数
a = int(input())

# 输入一个数组
a = input().split(" ")#以空格的方式输入。如果split(",")表示以逗号的形式输入。
li = [int(i) for i in range(a)]

⚪ 输出:print()

print()函数可以接受多个字符串,用逗号“,”隔开,依次打印每个字符串,遇到逗号“,”会输出一个空格

print('')print('\n')可以换行。如果在循环打印的过程中不需要换行,可用print( , end = '')

输出字符串的格式化

在Python中,输出字符串采用的格式化方式和C语言是一致的,%运算符就是用来格式化字符串的。有几个%占位符,后面就跟几个变量或者值,顺序要对应好。如果只有一个%,括号可以省略。格式化整数和浮点数还可以指定是否补0和整数与小数的位数:

>>> print('%2d-%02d' % (3, 1))
 3-01
>>> print('%.2f' % 3.1415926)
3.14

常见的占位符:

另一种格式化字符串的方法是使用字符串的format()方法,它会用传入的参数依次替换字符串内的占位符{0}、{1}……:

>>> 'Hello, {0}, 成绩提升了 {1:.1f}%'.format('小明', 17.125)
'Hello, 小明, 成绩提升了 17.1%'

(2)基本运算

在Python中,有两种除法:

# 返回整型
a % (10**9+7)

# 返回浮点型
a % (1e9+7)

⚪ 按位与、按位或、按位异或

按位操作先转换成二进制再操作。

按位与a & b

按位或a | b

按位异或a ^ b

⚪ 移位操作

⚪ divmod()函数

divmod()函数同时取得商和余数:

div, mod = n // 10, n % 10
# equels to
div, mod = divmod(n, 10)

(3)控制结构

⚪ 条件判断

if语句的完整形式是:

if <条件判断1>:
    <执行1>
elif <条件判断2>:
    <执行2>
elif <条件判断3>:
    <执行3>
else:
    <执行4>

⚪ 循环

Python的循环有两种,一种是for...in循环,依次把list或tuple中的每个元素迭代出来。第二种循环是while循环,只要条件满足,就不断循环,条件不满足时退出循环。

在循环中,break语句可以提前退出循环;continue语句跳过当前的这次循环,直接开始下一次循环。

程序陷入“死循环”,可以用Ctrl+C退出程序,或者强制结束Python进程。

⚪ 迭代器

判断一个对象是否可迭代,方法是通过collections模块的Iterable类型判断:

>>> from collections import Iterable
>>> isinstance([1,2,3], Iterable) # list是否可迭代
True
>>> isinstance(123, Iterable) # 整数是否可迭代
False

enumerate函数可以把可迭代对象变成索引-元素对,这样就可以在循环中同时迭代索引和元素本身:for i, v in enumerate([1,2,3])

可以直接作用于for循环的对象统称为可迭代对象Iterable。可以使用isinstance()判断一个对象是否是Iterable对象:

>>> from collections.abc import Iterable
>>> isinstance([], Iterable)
True

可以被next()函数调用并不断返回下一个值的对象称为迭代器Iterator。可以使用isinstance()判断一个对象是否是Iterator对象:

>>> from collections.abc import Iterator
>>> isinstance((x for x in range(10)), Iterator)
True

生成器都是Iterator对象,但list、dict、str虽然是Iterable,却不是Iterator。

把list、dict、str等Iterable变成Iterator可以使用iter()函数:

>>> isinstance(iter('abc'), Iterator)
True

Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。

⚪ 生成器

在Python中,一边循环一边计算的机制,称为生成器generator

生成器的创建方法①

>>> g = (x * x for x in range(10))
>>> g
<generator object <genexpr> at 0x1022ef630>

可以通过next()函数获得generator的下一个返回值:

>>> next(g)
0
>>> next(g)
1
...
>>> next(g)
9
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

每次调用next(g),就计算出g的下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出StopIteration的错误。

也可以使用for循环调用生成器:

>>> g = (x * x for x in range(10))
>>> for n in g:
...     print(n)

生成器的创建方法②

如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator:

def odd():
    print('step 1')
    yield 1
    print('step 2')
    yield 3

在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。

>>> o = odd()
>>> next(o)
step 1
1
>>> next(o)
step 2
3
>>> next(o)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

也可以使用for循环调用生成器。

(4)异常处理

在程序运行的过程中,如果发生了错误,可以事先约定返回一个错误代码,这样就可以知道是否有错,以及出错的原因。高级语言通常都内置了一套try…except…finally…的错误处理机制。

当某些代码可能会出错时,就可以用try来运行这段代码,如果执行出错,则后续代码不会继续执行,而是直接跳转至错误处理代码,即except语句块,执行完except后,如果有finally语句块,则执行finally语句块,至此,执行完毕。

错误应该有很多种类,如果发生了不同类型的错误,应该由不同的except语句块处理。如果没有错误发生,可以在except语句块后面加一个else,当没有错误发生时,会自动执行else语句。

try:
    print('try...')
    r = 10 / int('2')
    print('result:', r)
except ValueError as e:
    print('ValueError:', e)
except ZeroDivisionError as e:
    print('ZeroDivisionError:', e)
else:
    print('no error!')
finally:
    print('finally...')

Python的错误其实是一个class,捕获一个错误就是捕获到该class的一个实例。所有的错误类型都继承自BaseException,常见的错误类型和继承关系包括exception-hierarchy

Python内置的logging模块可以非常容易地记录错误信息,把错误堆栈打印出来;程序打印完错误信息后会继续执行,并正常退出:

import logging

try:
    r = 10 / 0
except Exception as e:
    logging.exception(e)

可以用raise语句抛出一个错误的实例:

def foo(s):
    n = int(s)
    if n==0:
        raise ZeroDivisionError('invalid value')
    return 10 / n

可以用断言(assert)辅助检查可能有问题的变量;如果断言失败,assert语句本身就会抛出AssertionError

def foo(s):
    n = int(s)
    assert n != 0, 'n is zero!'
    return 10 / n

def main():
    foo('0')

启动Python解释器时可以用-O参数来关闭assert

$ python -O main.py

也可以通过pdb.set_trace()方法在程序中设置一个断点:

# err.py
import pdb

s = '0'
n = int(s)
pdb.set_trace() # 运行到这里会自动暂停
print(10 / n)

运行代码,程序会自动在pdb.set_trace()暂停并进入pdb调试环境,可以用命令p 变量名查看变量,或者用命令c继续运行:

$ python err.py 
> /Users/err.py(7)<module>()
-> print(10 / n)
(Pdb) p n
0
(Pdb) c

(5)文件读写

读写文件是最常见的IO操作(Input/Output,输入和输出)。由于程序和运行时数据是在内存中驻留,由CPU计算核心来执行,涉及到数据交换的地方,通常是磁盘、网络等,就需要IO接口。

程序完成IO操作会有InputOutput两个数据流(Stream)。Input Stream是数据从外面(磁盘、网络)流进内存,Output Stream就是数据从内存流到外面去。

由于CPU和内存的速度远远高于外设的速度,所以在IO编程中,就存在速度严重不匹配的问题。同步IO是指CPU*等待磁盘,也就是程序暂停执行后续代码;异步IO是指CPU**不等待,后续代码可以立刻接着执行。

同步和异步的区别就在于是否等待IO执行的结果。使用异步IO来编写程序性能会远远高于同步IO,但是异步IO的缺点是编程模型复杂,需要设置回调模式或者轮询模式。

在磁盘上读写文件的功能都是由操作系统提供的,现代操作系统不允许普通的程序直接操作磁盘,所以读写文件就是请求操作系统打开一个文件对象(通常称为文件描述符),然后通过操作系统提供的接口从这个文件对象中读取数据(读文件),或者把数据写入这个文件对象(写文件)。

⚪ 读文件 read()

要以读文件的模式打开一个文件对象,使用Python内置的open()函数,传入文件名和标示符;如果文件不存在,open()函数就会抛出一个IOError的错误;如果文件打开成功,可以调用相关方法读取文件,把内容读到内存;最后一步是调用close()方法关闭文件。文件使用完毕后必须关闭,因为文件对象会占用操作系统的资源,并且操作系统同一时间能打开的文件数量也是有限的。

>>> f = open('/Users/test.txt', 'r') # 标示符'r'表示读
>>> f.read() # 一次读取文件的全部内容,用一个str对象表示
>>> f.read(size) # 每次最多读取size个字节的内容,用一个str对象表示
>>> f.readlines() # 一次读取所有内容并按行返回list
>>> f.close()

由于文件读写时都有可能产生IOError,一旦出错后面的f.close()就不会调用。所以为了保证无论是否出错都能正确地关闭文件,Python引入了with语句来自动调用close()方法:

with open('/path/to/file', 'r') as f:
    print(f.read())

读取文件默认都是读取文本文件,并且是UTF-8编码的文本文件。要读取二进制文件,比如图片、视频等等,用'rb'模式打开文件即可:

>>> f = open('/Users/test.jpg', 'rb')

要读取非UTF-8编码的文本文件,需要给open()函数传入encoding参数,例如读取GBK编码的文件。在文本文件中可能夹杂了一些非法编码的字符。遇到这种情况,open()函数还接收一个errors参数,表示如果遇到编码错误后如何处理,最简单的方式是直接忽略:

>>> f = open('/Users/gbk.txt', 'r', encoding='gbk', errors='ignore')

⚪ 写文件 write()

写文件是调用open()函数时,传入标识符'w'或者'wb'表示写文本文件或写二进制文件:

with open('/Users/test.txt', 'w') as f:
    f.write('Hello, world!')

写文件时,操作系统往往不会立刻把数据写入磁盘,而是放到内存缓存起来,空闲的时候再慢慢写入。只有调用close()方法时,操作系统才保证把没有写入的数据全部写入磁盘。

'w'模式写入文件时,如果文件已存在,会直接覆盖(相当于删掉后新写入一个文件)。如果希望追加到文件末尾,可以传入'a'以追加(append)模式写入。

⚪ StringIO和BytesIO

StringIOBytesIO是在内存中操作strbytes的方法,使得和读写文件具有一致的接口。

>>> from io import StringIO
>>> f = StringIO()
>>> f.write('hello')
5
>>> print(f.getvalue())
hello

>>> from io import BytesIO
>>> f = BytesIO(b'\xe4\xb8\xad\xe6\x96\x87')
>>> f.read()
b'\xe4\xb8\xad\xe6\x96\x87'

2. Python的数据类型

(1)整数

Python可以处理任意大小的整数,在程序中的表示方法和数学上的写法一模一样;Python的整数没有大小限制,而某些语言的整数根据其存储长度是有大小限制的。

有时候用十六进制表示整数比较方便,十六进制用0x前缀和0-9a-f表示。

⚪ 进制转换

python内的进制转换:

注意表内的$x$是字符串类型。

(2)浮点数

浮点数可以用数学写法,也可以用科学计数法表示(把10e替代);Python的浮点数也没有大小限制,但是超出一定范围就直接表示为inf(无限大)。

整数和浮点数在计算机内部存储的方式是不同的,整数运算永远是精确的(除法也是精确的),而浮点数运算则可能会有四舍五入的误差。

(3)常量

Python中,习惯上通常用全部大写的变量名表示常量。

布尔值和布尔代数的表示完全一致,可以用andornot运算。

空值是Python里一个特殊的值,用None表示。

(4)变量

变量用变量名表示,变量名必须是大小写英文、数字和_的组合,且不能用数字开头

在Python中,等号=是赋值语句,可以把任意数据类型赋值给变量,同一个变量可以反复赋值,而且可以是不同类型的变量。

这种变量本身类型不固定的语言称之为动态语言,与之对应的是静态语言。静态语言在定义变量时必须指定变量类型,如果赋值的时候类型不匹配,就会报错。例如Java是静态语言。和静态语言相比,动态语言更灵活。

理解变量在计算机内存中的表示也非常重要。如a = 'ABC'时,Python解释器干了两件事情:

  1. 在内存中创建了一个'ABC'的字符串;
  2. 在内存中创建了一个名为a的变量,并把它指向'ABC'

解释下列代码:

a = 'ABC'
b = a
a = 'XYZ'

python中变量保存的是对应的地址。如a = 10会在内存空间中找一个位置保存10,变量a保存的是指向10的地址。由于python中变量保存的是地址而不是具体的值,所以可以使用a, b = b, a等指令。

(5)字符

Python中字符的编码格式如下:

现在计算机系统通用的字符编码工作方式:

⚪ 字符的常用方法与函数

(6)字符串 str

字符串是以单引号'或双引号"括起来的任意文本,''""本身只是一种表示方式,不是字符串的一部分。如果'本身也是一个字符,那就可以用""括起来,或使用转义字符\

Python允许用'''...'''的格式表示多行内容,如:

print('''Hello
World''')

字符串是不可变对象;对于不可变对象,调用对象自身的任意方法,不会改变该对象自身的内容,而是会创建新的对象并返回:

>>> a = 'abc'
>>> b = a.replace('a', 'A')
>>> b
'Abc'
>>> a
'abc'

⚪ 字符串的常用方法

⚪ 转义字符

常见的转义字符:

Python还允许用r''表示''内部的字符串默认不转义。

⚪ Python的字符串编码

当Python解释器读取源代码时,为了让它按UTF-8编码读取,通常在文件开头写上这两行:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

3. Python的数据结构

(1)列表 list

列表是一种有序的集合,可以随时添加和删除其中的元素。列表是可变对象!

⚪ 列表生成式 List Comprehensions

列表生成式是Python内置的非常简单却强大的可以用来创建list的生成式。

>>> [x * x for x in range(1, 11)]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
>>> [x * x for x in range(1, 11) if x % 2 == 0]
[4, 16, 36, 64, 100]
>>> [x if x % 2 == 0 else -x for x in range(1, 11)]
[-1, 2, -3, 4, -5, 6, -7, 8, -9, 10]
>>> [m + n for m in 'ABC' for n in 'XYZ']
['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']

⚪ 列表降维

列表降维例子如下:

oldlist = [[1, 2, 3], [4, 5]]
# 想得到结果:
newlist = [1, 2, 3, 4, 5]

可以用sum函数实现:

newlist = sum(oldlist, [])

sum(iterable[, start]) 函数的第一个参数是可迭代对象,如列表、元组或集合等,第二个参数是起始值,默认为 0 。其用途是以 start 值为基础,再与可迭代对象的所有元素相“加”。

⚪ 快速初始化一个二维数组

初始化一个$m×n$的二维数组:

array = [[0] * n for i in range(m)]

下面这种做法是错误的:

array = [[0] * n] * m

这会导致array[i] == array[j],改变其中一个子数组的值,另一个子数组也会一起变。

⚪ 数组的排序

如果需要返回数组alist = [a_0,a_1,...]的排序下标,又不想修改原数组,则应写作:

idx = list(range(len(alist)))
idx.sort(key = lambda x: alist[x])

如果要将二维数组alist = [[a_0,b_0],[a_1,b_1],...]按照子数组的第二个元素排序,则应写作:

alist.sort(key = lambda x: x[1])

如果首先按照第一个元素的降序排列,元素相同者再按照第二个元素的升序排列,则应写作:

alist.sort(key = lambda x: (x[0], -x[1]), reverse=True)

对于一组字符串,优先排列长度最长且字母序最小的字符串,则依据字符串长度的降序和字典序的升序进行排序:

strs.sort(key = lambda x: (-len(x), x))

(2)元组 tuple

元组一旦初始化就不能修改。因为tuple不可变,所以代码更安全。tuple所谓的“不变”是说,tuple的每个元素,指向永远不变,指向的复合数据类型(如列表)本身是可变的!

要定义一个只有1个元素的tuple,如果这么定义:t = (1),定义的不是tuple,是1这个数!这是因为括号()既可以表示tuple,又可以表示数学公式中的小括号,Python规定,这种情况下,按小括号进行计算,计算结果自然是1。只有1个元素的tuple定义时必须加一个逗号,,来消除歧义:t = (1,)。Python在显示只有1个元素的tuple时,也会加一个逗号,,以免误解成数学计算意义上的括号。

(3)字典 dict

字典使用键-值(key-value)存储,具有极快的查找速度。dictkey必须是不可变对象(字符串、整数)。通过key计算位置的算法称为哈希算法(Hash)

list比较,dict有以下几个特点:

  1. 查找和插入的速度极快,不会随着key的增加而变慢;
  2. 需要占用大量的内存,内存浪费多。

list相反:

  1. 查找和插入的时间随着元素的增加而增加;
  2. 占用空间小,浪费内存很少。

所以,dict是用空间来换取时间的一种方法。

⚪ 寻找字典中的最大、最小值

python中寻找字典中最大、最小值对应的key可采用匿名函数:

max_key = max(dict, key = lambda x: dict[x])
min_key = min(dict, key = lambda x: dict[x])

若要得到最大、最小值,索引即可:

max_value = dict[max_key]
min_value = dict[min_key]

需要注意的是,这种方式获得的key只有一个,如果想要获得所有最大、最小值,可以利用min函数先找到最小value,然后遍历字典一遍找对应的key值。

(4)集合 set

集合和dict类似,也是一组key的集合,setdict的唯一区别仅在于没有存储对应的value。由于key不能重复,所以在set中没有重复的key。集合可以用于数据去重

要创建一个set,需要提供一个list作为输入集合:s=set([1,2,3])。两个set可以做数学意义上的交集&、并集|等操作。

4. Python的函数式编程

函数是一种最基本的代码抽象的方式。面向过程的程序设计:通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务。

函数式编程Functional Programming是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,称之为没有副作用。由于Python允许使用变量,因此不是纯函数式编程语言。

(1)内置函数

可以在交互式命令行通过help(func_name)查看内置函数的帮助信息。

函数名其实就是指向一个函数对象的引用,可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”:

>>> a = abs # 变量a指向abs函数
>>> a(-1) # 所以也可以通过a调用abs函数
1

使用下列语句进行错误提示:

if False:
    raise TypeError('there is something wrong')

函数执行完毕也没有return语句时,自动return None;函数可以同时返回多个值,但其实就是返回一个tuple。

常用的内置函数:

⚪常用内置函数:zip()

zip()接受一系列可迭代的对象作为参数,将对象中对应的元素打包成一个个tuple,然后返回由这些tuples组成的list

若传入参数的长度不等,则返回list的长度和参数中长度最短的对象相同。直接输出zip(list1, list2)返回的是一个zip对象, 在前面加上*返回列表中的数据对应的元组,可以将list unzip(解压),直接转化成列表或字典。

应用一:同时遍历多个列表:

for a, b in zip(list1, list2):
    print(a, b)

应用二:矩阵的行列互换

a = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
zip(*a)
# 输出结果是:[(1, 4, 7), (2, 5, 8), (3, 6, 9)]
map(list, zip(*a))
# 输出结果是:[[1, 4, 7], [2, 5, 8], [3, 6, 9]]

(2)定义函数

pass语句什么都不做,可以用来作为占位符,比如现在还没想好怎么写函数的代码,就可以先放一个pass,让代码能运行起来。

定义函数,还需要定义参数:

⚪ 函数的嵌套定义

python允许创建嵌套函数。也就是说可以在函数里面定义函数,而且现有的作用域和变量生存周期依旧不变。

def outer():
    name="python"
    def inner():  # outer函数内部定义的函数
        print(name)
    return inner()  # 返回该内部函数
	
outer()
# python

inner函数中,python解析器需要找一个叫name的本地变量,查找失败后会继续在上层的作用域里面寻找,这个上层作用域定义在outer函数里,python函数可以访问封闭作用域。

对于outer函数中最后一句,返回inner函数调用的结果,需要知道非常重要一点就是,inner也仅仅是一个遵循python变量解析规则的变量名,python解释器会优先在outer的作用域里面对变量名inner查找匹配的变量,把恰好是函数标识符的变量inner作为返回值返回回来。每次函数outer被调用的时候,函数inner都会被重新定义,如果它不被当做变量返回的话,每次执行过后它将不复存在。

python里,函数就是对象,也只是一些普通的值而已。也就是说可以把函数像参数一样传递给其他的函数或者说从函数了里面返回函数。

⚪ 关键字global和nonlocal

global关键字用来在函数或其他局部作用域中使用全局变量。但是如果不修改全局变量也可以不使用global关键字:

# 修改全局变量
gcount = 0
def global_test():
    global  gcount
    gcount+=1
    print(gcount)
	
# 不修改全局变量
gcount = 0
def global_test():
    print(gcount)

nonlocal声明的变量不是局部变量,也不是全局变量,而是外部嵌套函数内的变量:

def make_counter():
    count = 0
    def counter():
        nonlocal count
        count += 1

(3)递归函数

如果一个函数在内部调用自身本身,这个函数就是递归函数。递归函数的优点是定义简单,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。

使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。

解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。

尾递归是指,在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。

(4)高阶函数 Higher-order function

高阶函数:一个函数可以接收另一个函数作为参数。

def add(x, y, f):
    return f(x) + f(y)

Python内置的高阶函数:

map()

map()函数接收两个参数,一个是函数,一个是Iterablemap将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。

map()函数返回的是一个Iterator,也就是一个惰性序列,所以要强迫map()完成计算结果,有时需要用list()函数获得所有结果并返回list。

比如,把这个list所有数字转为字符串:

>>> list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
['1', '2', '3', '4', '5', '6', '7', '8', '9']

reduce()

reduce()把一个函数作用在一个序列$[x1, x2, x3, …]$上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,其效果就是:

reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)

例如把序列$[1, 3, 5, 7, 9]$变换成整数$13579$:

>>> from functools import reduce
>>> def fn(x, y):
...     return x * 10 + y
...
>>> reduce(fn, [1, 3, 5, 7, 9])
13579

函数当然可以用匿名函数lambda的形式。

filter()

filter()函数用于过滤序列。接收一个函数和一个序列,把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素。

如用埃拉托色尼筛选法计算质数:

# 生成器生成从3开始的无限奇数序列
def _int_iter():
    n = 1
    while True:
        n += 2
        yield n
		
# 定义筛选函数
def _not_divisible(n):
    return lambda x: x%n > 0

def primes():
    yield 2  #返回第一个质数2
    it = _int_iter()  #候选无限奇数序列
    while True:
        n = next(it)
        yield n  #返回下一个质数
        it = filter(_not_divisible(n), it) #过滤这个质数的倍数
		
# 构造循环条件,使之可以输出任何范围的素数序列
for n in primes():
    if n < 1000:
        print(n)
    else:
        break

sorted()

sorted()函数也是一个高阶函数,用于对列表等进行从小到大的排序。它还可以接收一个key函数来实现自定义的排序,key指定的函数将作用于list的每一个元素上,并根据key函数返回的结果进行排序。

如按绝对值大小排序:

>>> sorted([36, 5, -12, 9, -21], key=abs)
[5, 9, -12, -21, 36]

默认情况下,对字符串排序,是按照ASCII的大小比较的,由于‘Z’ < ‘a’,结果大写字母Z会排在小写字母a的前面。

要进行反向排序,不必改动key函数,可以传入第三个参数reverse=True

>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True)
['Zoo', 'Credit', 'bob', 'about']

(5)返回函数

高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。这种程序结构称为“闭包(Closure)”。

def lazy_sum(args=[1, 3, 5, 7, 9]):
    def sum():
        ax = 0
        for n in args:
            ax = ax + n
        return ax
    return sum

在函数lazy_sum中又定义了函数sum,并且内部函数sum可以引用外部函数lazy_sum的参数和局部变量,当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中。当我们调用lazy_sum()时,返回的并不是求和结果,而是求和函数:

>>> f = lazy_sum(1, 3, 5, 7, 9)
>>> f
<function lazy_sum.<locals>.sum at 0x101c6ed90>

调用函数f时,才真正计算求和的结果:

>>> f()
25

返回闭包时牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量。

(6)匿名函数

关键字lambda x: f(x)表示匿名函数,冒号前面的$x$表示函数参数。

匿名函数有个限制,就是只能有一个表达式,不用写return,返回值就是该表达式的结果。

用匿名函数有个好处,因为函数没有名字,不必担心函数名冲突。

匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数;或也可以把匿名函数作为返回值返回。

(7)装饰器 decorator

假设要增强函数的功能,比如,在函数调用前后自动打印日志,但又不希望修改函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)

⚪ 不带参数的decorator

本质上,decorator就是一个返回函数的高阶函数。如定义一个能打印日志的decorator:

import functools

def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper

其中Python内置的functools.wraps是把原始函数的__name__等属性复制到wrapper()函数中,否则,有些依赖函数签名的代码执行就会出错。

借助Python的@语法,把decorator置于函数的定义处:

@log
def func():

相当于执行了语句:func = log(func)

原来的func()函数仍然存在,只是现在同名的func变量指向了新的函数,于是调用func()将执行新函数,即在log()函数中返回的wrapper()函数。

wrapper()函数的参数定义是(*args, **kw),因此,wrapper()函数可以接受任意参数的调用。在wrapper()函数内,首先打印日志,再紧接着调用原始函数。

带参数的decorator

带参数的decorator:

import functools

def log(text):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kw):
            print('%s %s():' % (text, func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator

和两层嵌套的decorator相比,3层嵌套的效果是这样的:func = log('text')(func)

上面的语句首先执行log('text'),返回的是decorator函数,再调用返回的函数,参数是func函数,返回值最终是wrapper函数。

(8)偏函数

偏函数functools.partial的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。

int()函数可以把字符串转换为整数,当仅传入字符串时,int()函数默认按十进制转换;int()函数还提供额外的base参数,默认值为10。如果传入base参数,就可以做N进制的转换:

>>> int('1000000', base=8)
64

如果多次调用该函数,每次都需要设置参数。当函数的参数个数太多,需要简化时,使用functools.partial可以创建一个新的函数,这个新函数可以固定住原函数的部分参数,从而在调用时更简单:

>>> import functools
>>> int2 = functools.partial(int, base=2)
>>> int2('1000000')
64

创建偏函数时,实际上可以接收函数对象、*args**kw这3个参数。当传入:

max2 = functools.partial(max, 10)

实际上会把10作为*args的一部分自动加到左边,也就是:

max2(5, 6, 7)

相当于:

args = (10, 5, 6, 7)
max(*args)

结果为10。

5. 面向对象编程

面向对象编程(Object Oriented Programming,OOP)对象作为程序的基本单元,一个对象包含了数据和操作数据的函数,后者称之为对象的方法(Method)

Python中,所有数据类型都可以视为对象,当然也可以自定义对象。自定义的对象数据类型就是面向对象中的类(Class)的概念。面向对象的设计思想是抽象出Class,根据Class创建实例Instance。面向对象的三大特点是数据封装、继承和多态。

Python中定义类是通过class关键字,创建实例是通过类名+()实现的。在创建类的时候,可以通过__init__方法把一些必须绑定的属性(attribute)强制填写进去。

class Student(object):
    # 创建类属性, 所有实例共享
    count = 0
    # __init__方法的第一个参数是self,表示创建的实例本身
    def __init__(self, name, score):
        # 创建实例属性,属于各个实例所有,互不干扰
        self.name = name
        self.score = score

# 创建实例时必须传入与__init__方法匹配的参数
# self不需要传,Python解释器自己会把实例变量传进去
bart = Student('Tom', 90)

可以用type()判断对象类型:

>>> type(bart)
<class '__main__.Student'>

可以用isinstance()判断对象是否指向类型(或者位于该类型的父继承链上):

>>> isinstance(bart, Student)
True

也可以通过type()函数动态地创建类。要创建一个class对象,type()函数依次传入3个参数:

  1. class的名称;
  2. 继承的父类集合;
  3. class的方法名称与函数绑定。
>>> def fn(self, name='world'): # 先定义函数
...     print('Hello, %s.' % name)
...
>>> Hello = type('Hello', (object,), dict(hello=fn)) # 创建Hello class

(1)数据封装

类是创建实例的模板,而实例则是一个一个具体的对象;方法是与实例绑定的函数,可以直接访问实例的数据。

通过在实例上调用方法,可以直接操作对象内部的数据,但无需知道方法内部的实现细节,这样就把“数据”给封装起来了。数据封装是指直接在类的内部定义访问数据的函数,外部代码可以通过直接调用实例变量的方法来操作数据,这样就隐藏了内部的复杂逻辑。

class Student(object):
    def __init__(self, name, score):
        self.name = name
        self.score = score

    # 在类中定义的函数第一个参数永远是实例变量self
    # 并且调用时不用传递该参数
    def print_score(self):
        print('%s: %s' % (self.name, self.score))

⚪ 操作对象的属性和方法

如果要获得一个对象的所有属性和方法,可以使用dir()函数,它返回一个包含字符串的list

>>> dir(bart)
['__class__',..., 'name', 'print_score', 'score']

配合getattr()setattr()以及hasattr(),可以直接操作一个对象的属性和方法:

>>> hasattr(bart, 'gender') # 有属性'gender'吗?
False
>>> setattr(bart, 'gender', 'male') # 设置一个属性'gender'
>>> getattr(bart, 'gender', -1) # 获取属性'gender', 如果不存在返回默认值-1
male

也可以给实例绑定新的方法:

>>> def set_age(self, age): # 定义一个函数作为实例方法
...     self.age = age

# 给一个实例绑定的方法,对另一个实例是不起作用的
>>> from types import MethodType
>>> bart.set_age = MethodType(set_age, bart) # 给实例绑定一个方法

# 给所有实例都绑定方法
>>> Student.set_age = set_age

⚪ 私有变量

默认情况下,Python中的成员函数和成员变量都是公开的(相当于java中的public)。在python中没有public,private等关键词来修饰成员函数和成员变量。例如在上述定义中,外部代码还是可以自由地修改一个实例的属性:

>>> bart.score = 99
>>> bart.score
99
>>> del bart.score # 删除实例的score属性

如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__,在Python中,实例的变量名如果以__开头,就变成了一个私有变量(private),无法从外部访问。

class Student(object):
    def __init__(self, name, score):
        self.__name = name
        self.__score = score

私有变量确保了外部代码不能随意修改对象内部的状态,这样通过访问限制的保护,代码更加健壮。如果需要访问或修改私有变量,可以增加新的方法:

class Student(object):
    ...
    def get_score(self):
        return self.__score

    def set_score(self, score):
        self.__score = score

私有变量__score不能直接访问是因为Python解释器对外把__score变量改成了_Student__score,所以仍然可以通过_Student__score来访问__score变量。

Python中的下划线定义变量的规则如下:

  1. 单前导下划线_var:命名约定(该约定在python代码书写规范PEP 8中有定义),仅供内部使用。通常不会由Python解释器强制执行(通配符导入除外),这样的实例变量外部是可以访问的,但是按照约定俗成的规定,请把它视为私有变量,不要随意访问。
  2. 单末尾下划线var_:按约定(该约定在PEP 8中有定义)使用以避免与Python关键字的命名冲突。
  3. 双前导下划线__var:当在类上下文中使用时,触发”名称修饰“(双下划线前缀会导致Python解释器重写属性名称,以避免子类中的命名冲突)。由Python解释器强制执行。
  4. 双前导和双末尾下划线__var__:表示Python语言定义的特殊变量。特殊变量是可以直接访问的,不是private变量。避免在自己的属性中使用这种命名方案。
  5. 单下划线_:有时用作临时或无意义变量的名称(“不关心”)。也表示Python REPL中最近一个表达式的结果。

@property装饰器

Python内置的@property装饰器负责把类的一个方法变成属性调用。除了创建@property本身如果又创建了另一个装饰器@method.setter,则负责把一个setter方法变成属性赋值,从而拥有一个可控的属性操作;否则定义只读属性。

class Student(object):
    @property
    def score(self):
        return self.__score

    @score.setter
    def score(self, value):
        self._score = value

    @property
    def gap(self):
        return 100 - self.__score

(2)继承和多态

定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subclass),而被继承的class称为基类、父类或超类(Base class、Super class)。通常如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。

class Teacher(Student):
    pass

继承后子类获得父类的全部功能。在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类。当子类和父类都存在相同的方法时,子类的方法覆盖了父类的方法,在代码运行的时候,总是会调用子类的方法,这是继承的另一个好处:多态

多态的好处是,当需要传入子类时,只需要接收父类类型,按照父类类型进行操作即可。对于一个变量,只需要知道它是父类类型,无需确切地知道它的子类型,就可以放心地调用相关的方法,而具体调用的方法是作用在子类对象上,由运行时该对象的确切类型决定。这就是著名的“开闭”原则

动态语言的“鸭子类型”是指如果需要传入某类型并调用相关方法,则不一定需要传入该类型,只需要保证传入的对象有一个相关的方法就可以了。即并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。

⚪ 多重继承

多重继承使得一个子类可以同时获得多个父类的所有功能,这种设计通常称之为MixIn

class Dog(Mammal, RunnableMixIn, CarnivorousMixIn):
    pass

MixIn的目的就是给一个类增加多个功能,这样在设计类的时候,优先考虑通过多重继承来组合多个MixIn的功能,而不是设计多层次的复杂的继承关系。

(3)定制类

Pythonclass允许定义许多定制方法,可以非常方便地生成特定的类。形如__xxx__的变量或者函数名可用于定制类。

__slots__

Python允许在定义class的时候,定义一个特殊的__slots__变量,来限制该class实例能添加的属性:

class Student(object):
    __slots__ = ('name', 'score') # 用tuple定义允许绑定的属性名称

__slots__定义的属性仅对当前类实例起作用,对继承的子类是不起作用的。

__len__()

__len__()方法返回长度。在Python中,如果调用len()函数试图获取一个对象的长度,实际上在len()函数内部,它自动去调用该对象的__len__()方法。

>>> len('ABC')
3
>>> 'ABC'.__len__()
3

__str__()

__str__()方法能够修改显示变量,但不能修改直接敲变量打印出来的实例。

>>> class Student(object):
...     def __str__(self):
...         return 'Student object'

>>> print(Student())
Student object
>>> s = Student()
>>> s
<__main__.Student object at 0x109afb310>

这是因为直接显示变量调用的不是__str__(),而是__repr__(),两者的区别是__str__()返回用户看到的字符串,而__repr__()返回程序开发者看到的字符串,也就是说,__repr__()是为调试服务的。

解决办法是再定义一个__repr__()。但是通常__str__()__repr__()代码都是一样的,所以可写为:

>>> class Student(object):
...     def __str__(self):
...         return 'Student object'
...     __repr__ = __str__

__iter__()

如果一个类想被用于for循环,就必须实现一个__iter__()方法,该方法返回一个迭代对象,Pythonfor循环就会不断调用该迭代对象的__next__()方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环。

class Fib(object):
    def __init__(self):
        self.a, self.b = 0, 1 # 初始化两个计数器a,b

    def __iter__(self):
        return self # 实例本身就是迭代对象,故返回自己

    def __next__(self):
        self.a, self.b = self.b, self.a + self.b # 计算下一个值
        if self.a > 100000: # 退出循环的条件
            raise StopIteration()
        return self.a # 返回下一个值

>>> for n in Fib():
...     print(n)

__getitem__()

如果想要从一个类中按照下标取出元素,需要实现__getitem__()方法:

class Fib(object):
    def __getitem__(self, n):
        a, b = 1, 1
        for x in range(n):
            a, b = b, a + b
        return a

>>> f = Fib()
>>> f[0]
1

与之对应的是__setitem__()方法,把对象视作list或dict来对集合赋值。还有一个__delitem__()方法,用于删除某个元素。

__getattr__()

__getattr__()方法可以动态返回一个属性。当调用不存在的属性时,比如scorePython解释器会试图调用__getattr__(self, 'score')来尝试获得属性:

class Student(object):
    def __getattr__(self, attr):
        if attr=='score':
            return 99

注意到任意调用如s.abc都会返回None,这是因为定义的__getattr__默认返回就是None

__call__()

任何类只需要定义一个__call__()方法,就可以直接对实例进行调用。

class Student(object):
    def __call__(self):
        print('My name is Tom.')

>>> s = Student()
>>> s() # self参数不要传入
My name is Tom.

通过callable()函数可以判断一个对象是否是“可调用”对象,能被调用的对象就是一个Callable对象,比如函数和带有__call__()的类实例。

>>> callable(Student())
True

(4)枚举类

Python提供了Enum类来实现枚举类型的定义,其中每个常量都是class的一个唯一实例。

from enum import Enum
Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))

枚举类Month可以直接使用Month.Jan来引用一个常量,或者枚举它的所有成员:

for name, member in Month.__members__.items():
    print(name, '=>', member, ',', member.value)

既可以用成员名称引用枚举常量,又可以直接根据value的值获得枚举常量。value属性则是自动赋给成员的int常量,默认从1开始计数。如果需要更精确地控制枚举类型,可以从Enum派生出自定义类:

from enum import Enum, unique

@unique
class Weekday(Enum):
    Sun = 0 # Sun的value被设定为0

@unique装饰器可以检查保证没有重复值。

6. Python的模块

模块是一组Python代码的集合,在Python中,一个.py文件就称之为一个模块(Module)。使用模块最大的好处是大大提高了代码的可维护性。其次,编写代码不必从零开始。当一个模块编写完毕,就可以被其他地方引用。在编写程序的时候经常引用其他模块,包括Python内置的模块和来自第三方的模块。

使用模块还可以避免函数名和变量名冲突。相同名字的函数和变量完全可以分别存在不同的模块中,因此在编写模块时不必考虑名字会与其他模块冲突。但是尽量不要与内置函数名字冲突。点这里查看Python的所有内置函数。

为了避免模块名冲突,Python又引入了按目录来组织模块的方法,称为包(Package)。如下面的目录结构就是一个包,mycompany顶层包名abc.py文件就是一个名字叫abc的模块。

mycompany
├─ __init__.py
├─ abc.py
└─ xyz.py

每一个包目录下面都会有一个__init__.py的文件,这个文件是必须存在的,否则,Python就把这个目录当成普通目录,而不是一个包。__init__.py可以是空文件,也可以有Python代码,因为__init__.py本身就是一个模块,而它的模块名就是顶层包名mycompany

(1)模块的使用和安装

模块的常见开头:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
' a test module '
__author__ = 'Michael Liao'

if __name__ == '__main__':

一个python的文件有两种使用的方法,第一是直接作为脚本执行,第二是import到其他的python脚本中被调用(模块重用)执行。因此if __name__ == '__main__':的作用就是控制这两种情况执行代码的过程,在if __name__ == '__main__':下的代码只有在第一种情况下(即文件作为脚本直接执行)才会被执行,而import到其他脚本中是不会被执行的。

if __name__=='__main__':
    test()

每个python模块都包含内置的变量__name__,当运行模块被执行的时候,__name__等于文件名(包含了后缀.py);如果import到其他模块中,则__name__等于模块名称(不包含后缀.py)。而__main__等于当前执行文件的名称(包含了后缀.py)。进而在命令行运行该模块文件时,结果为真。而如果在其他地方导入该模块时,if判断将失败,因此,这种if测试可以让一个模块通过命令行运行时执行一些额外的代码,最常见的就是运行测试。

⚪ 调用自定义模块

(1)主程序与模块程序在同一目录下,如下面程序结构:

-- src  
    |-- mod1.py
    |-- test1.py

若在程序test1.py中导入模块mod1, 则直接使用import mod1from mod1 import *

(2)主程序所在目录是模块所在目录的父(或祖辈)目录,如下面程序结构:

`-- src
    |-- mod1.py
    |-- mod2
    |   `-- mod2.py
    `-- test1.py

若在程序test1.py中导入模块mod2, 需要在主文件夹和mod2文件夹中建立空文件__init__.py文件(也可以在该文件中自定义输出模块接口); 然后使用from mod2.mod2 import *import mod2.mod2

或者使用下面的代码把调用的文件加入到搜素目录中:

import sys
sys.path.append('./mod2/mod2.py')

⚪ 安装第三方模块

Python中,安装第三方模块,是通过包管理工具pip完成的。注意:MacLinux上有可能并存Python 3.xPython 2.x,因此对应的pip命令是pip3

当试图加载一个模块时,Python会在指定的路径下搜索对应的.py文件,如果找不到,就会报错ImportError。默认情况下,Python解释器会搜索当前目录所有已安装的内置模块第三方模块,搜索路径存放在sys模块的path变量中:

>>> import sys
>>> sys.path
['', '/Library/Frameworks/Python.framework/Versions/3.6/lib/python36.zip', '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6', ..., '/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages']

如果要添加自己的搜索目录,有两种方法:一是直接修改sys.path,添加要搜索的目录;这种方法是在运行时修改,运行结束后失效。

>>> import sys
>>> sys.path.append('/Users/my_py_scripts')

第二种方法是设置环境变量PYTHONPATH,该环境变量的内容会被自动添加到模块搜索路径中。设置方式与设置Path环境变量类似。注意只需要添加自己的搜索路径,Python自己本身的搜索路径不受影响。

(2)内建模块

Python内置了许多非常有用的模块,无需额外安装和配置,即可直接使用。一些常用的内建模块可参考

(3)第三方模块

Python有大量的第三方模块。基本上所有的第三方模块都会在PyPI - the Python Package Index上注册,只要找到对应的模块名字,即可用pip安装。一些常用的第三方模块可参考

⭐ Reference