最近在工作的项目中,有一个模块的服务是使用 Python 脚本编写的,刚巧最近通过用户的反馈发现这个脚本有些 bug ,而我周围同事又都是 Java 体系的,对 Python 不太了解。基于这个原因,加上我之前也模糊了解过,大概知道 Python 的入门是很简单的。所以就干脆花点时间好好地学一下 Python ,预期能够入门并适用到一些简单的工作场景中。

关于开发语言的热度:https://www.tiobe.com/tiobe-index

当然,不同开发语言各有优劣,脱离实际需要进行对比也无意义,能适应需要的语言,就是好的语言。

另外,本文作为 Python 基础学习的笔记,限于篇幅,对于部分较为深广的知识不做扩展,但响应部分都会尽量留出扩展链接,供有兴趣的读者进行深化了解。

# 简介

Python 的优点很多,简单的可以总结为以下几点。

  1. 简单和明确,做一件事只有一种方法。

  2. 学习曲线低,跟其他很多语言相比,Python 更容易上手。

  3. 开放源代码,拥有强大的社区和生态圈。

  4. 解释型语言,天生具有平台可移植性。

  5. 支持两种主流的编程范式(面向对象编程和函数式编程)都提供了支持。

  6. 可扩展性和可嵌入性,可以调用 C/C++ 代码,也可以在 C/C++ 中调用 Python。

  7. 代码规范程度高,可读性强,适合有代码洁癖和强迫症的人群。

Python 的缺点主要集中在以下几点。

  1. 运行速度慢,和 C 程序相比非常慢,因为 Python 是解释型语言,你的代码在执行时会一行一行地翻译成 CPU 能理解的机器码,这个翻译过程非常耗时,所以很慢。

  2. 代码无法加密,发布出去的 Python 代码都是源代码,这是所有解释型语言的共性。

  3. 在开发时可以选择的框架太多(如 Web 框架就有 100 多个),有选择的地方就有错误。

目前 Python 在 Web 应用开发、云基础设施、DevOps、网络爬虫开发、数据分析挖掘、机器学习等领域都有着广泛的应用,因此也产生了 Web 后端开发、数据接口开发、自动化运维、自动化测试、科学计算和可视化、数据分析、量化交易、机器人开发、图像识别和处理等一系列的职位。

# 安装

本文作为学习示例,以 windows 作为学习环境,Python 当前文档版本号: 3.9.6

# Windows 环境

官网下载:https://www.python.org

Python官网首页

从官网下载安装文件 python-3.9.6-amd64.exe 并双击执行安装:

Python安装引导

注意:安装时请勾选 Add Python *.* to PATH ,它将会为您自动配置环境变量。

检验是否安装成功:

校验Python安装

如图所示,则表示 Python 安装成功!

# Linux 环境

部分主机或虚拟机版本会内置安装 Python,如内置 Python 以足够,即可跳过此小节。

  1. 选择 Python 版本(可选)。

    如需指定版本号,点击此处前往官网进行选择,本文将以 3.9.6 作为示例。

  2. 安装并更新。

    在合适的位置下载安装包:

    wget https://www.python.org/ftp/python/3.9.6/Python-3.9.6.tgz # 下载
    tar -zxvf Python-3.9.6.tgz -C /opt/module/ # 解压
    yum -y install gcc zlib* libffi-devel # 安装编译 Python 所需要的开发包

    进入解压目录,进行编译和安装:

    cd /opt/module/Python-3.9.6
    ./configure --prefix=/usr/local/bin/python3
    make&&make install

    如在编译过程中提示 make 命令不存在,会给出相应的提示,可根据提示进行安装。

    删除原先的命令,并建立新的软连接:

    python3 -V # 此时仍是旧版本
    rm -rf /usr/bin/python3
    ln -s /usr/local/bin/python3/bin/python3 /usr/bin/python3
    python3 -V # 此时被替换为新安装的版本

    注意:

    1. 如果安装过程中出现网络问题,可尝试临时关闭防火墙。

    2. 如果出现权限问题,可尝试使用 root 或 sudo 用户身份。

# Hello World

安装成功后,我们将开始学习如何编写第一个 Python 程序。

# 命令行模式与 Python 交互模式

正式编写 Python 程序之前,需要先了解一下什么是命令行模式,什么是 Python 模式。

  1. 命令行模式

    命令行模式,即命令提示符,也就是我们常说的 CMD 、小黑框等,它用于执行一些 Windows 命令。

  2. Python 交互模式

    在命令行模式下,输入 python ,即可进入 Python 交互模式,它以 >>> 作为提示符。

    在 Python 交互模式下,输入 exit() 或使用快捷键 Ctrl + Z 并回车,即可退出,回到命令行模式。

    Python 只是为调试代码提供方便,并非正式运行 Python 代码的环境。

尝试在 Python 交互模式下,执行一些简单的指令:

>>> 100 + 20
120
>>> print('Hello World~')
Hello World~
>>> 2 ** 10
1024

在命令行模式下,也可以执行 python hello.py 运行一个 .py 文件。

hello.py 内容示例:

100 + 20
print('Hello World~')
print(2 ** 10)

在命令行模式下执行:

C:\>python hello.py
Hello World~
1024

# 选择开发工具

在上文中,我们已经能够通过命令行执行一些简单的 Python 指令,但…… 总有点重上幼儿园的感觉。好吧,这不怪我们,怪只怪没有用上趁手的工具。这里提出几款可供选择的 Python 开发工具:

  • PyCharm(推荐 1)

    下载地址:https://www.jetbrains.com/pycharm

  • Visual Studio Code(推荐 2)

  • Eclipse(不推荐)

PyCharm 社区版是免费的,商业版是收费的。我对用过的所有 JetBrains 的产品都欲罢不能,在选择开发工具方面,我个人的首选当然是 PyCharm 啦。

使用 PyCharm 创建一个 Python 项目并运行,如下图所示:

PyCharm第一个程序

# 输入打印

在上述示例中,我们反复用到了 print() 这个函数,它的作用就是将内容打印(输出)到控制台。而如果需要从控制台获取用户自定义输入的内容,则需要使用到 input() 函数,使用方式如下:

name = input('Please input your name: ')# 开启输入,并提示
print('Hello, ' + name)# 打印用户输入的内容

注意,与 Java 不同,Python 每一行代码的结尾部分不需要添加 ;

# Python 基础

Python 的语法比较简单,采用缩进方式,示例代码如下:

# print absolute value of an integer:
a = 100
if a >= 0:
    print(a)
else:
    print(-a)

Python 语法特点:

  1. 关于注释

    Python 以 # 来表示注释,与其他语言一样,注释的作用只是为开发人员提供方便,对程序没有实际意义,解释器(或编译器)会忽略掉注释内容。

  2. 代码块

    当缩进的语句以冒号 : 结尾时,缩进的语句被视为代码块。

  3. 缩进

    Python 采用缩进方式组织代码,它对缩进是十分敏感的,但同时,它又并未规定具体的缩进方式,理论上两个或四个空格的缩进方式与一个制表符的缩进方式都是可以的,甚至在很多情况下混合使用也能够正常执行。但缩进作为 Python 语法的一部分,它是需要严谨对待的,编码人员一定要强制要求自己,在书写 Python 代码时,使用 4 个空格作为缩进方式。

    注意区分 4 个空格与 1 个 tab 的区别。

    另外,Python 的缩进与 Java 不同,Java 缩进的作用仅仅是为了代码的美观和可读性。

  4. 字母大小写

    和 Java 一样,Python 程序也是大小写敏感的。

# 数据类型和变量

  1. 整数

    Python 可以处理任意大小的整数(包括负整数),如需使用十六进制,则需要以 0x 作为前缀,例如 0xff00

    对于较大的数值,为了提高可读性,Python 支持使用下划线来分割位数,例如 10_000_000_000

  2. 浮点数

    浮点数也就是小数,之所以称为浮点数,是因为按照科学记数法表示时,一个浮点数的小数点位置是可变的。浮点数可以用数学写法,例如 3.14 。但是对于很大或很小的浮点数,就必须用科学计数法表示。

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

  3. 字符串

    字符串是以英文单引号或双引号括起来的任意文本,引号本身只是一种表示方式,不是字符串的一部分。如果字符串内容本身带有引号,则要进行区分,关于引号的各种情况,可以参照以下代码清单:

    print('It\'s a nice day!')  # 输出结果:It's a nice day!
    print("It's a nice day!")  # 输出结果:It's a nice day!
    print("He said: \"It's a nice day!\"")  # He said: "It's a nice day!"

    这里使用到的 \ 被称为转义字符,它除了可以对引号进行转义之外,还可以表示换行( \n )、制表符( \t )以及该字符本身( \\ )。但是,当字符串中存在较多需要进行转义处理的字符时,Python 还允许使用 r'' 来表示引号内部的字符串内容不转义,例如:

    print(r'\n表示换行' + "\nr''表示引用原字符")

    但如果字符串内存在多个换行,使用 \n 换行时候,会对阅读造成不便,因此 Python 允许使用 '''...''' 的格式表示多行内容,例如:

    print('''My learning tasks:
    Java
    Python
    Golang
    ''')

    同样,多行字符 '''...''' 前也可以加上 r 字符结合使用。

  4. 布尔值

    布尔值和布尔代数的表示完全一致,一个布尔值只有 TrueFalse 两种值,要么是 True ,要么是 False ,在 Python 中,可以直接用 TrueFalse 表示布尔值(注意大小写),也可以通过布尔运算计算出来:

    bool = 1 < 2
    print(1 > 2)  # False
    print(bool)  # True
    print(True)  # True
    print(bool == False)  # False

    布尔值可以使用 andornot 进行运算。

    t1 = 1 < 2  # True
    f1 = 1 > 2  # False
    t2 = 2 > 1  # True
    f2 = 2 < 1  # False
    print('and ------->')
    print(t1 and f1)  # True and False => False
    print(t1 and t2)  # True and True => True
    print(f1 and f2)  # False and False => False
    print('or ------->')
    print(t1 or f1)  # True or False => True
    print(t1 or t2)  # True or True => True
    print(f1 or f2)  # False or False => False
    print('not ------->')
    print(not t1)  # not True => False
    print(not f1)  # not False => True
  5. 空值

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

  6. 变量

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

    age = 20  # age 是一个数值型的变量,它的值为 20
    birth_98 = '1998年'  # birth_98 是一个字符串类型的变量,它的值为【1998 年】
    MyAnswer = True  # 布尔型变量...

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

    totalPrice = 0
    bookPrice = 299
    pencilPrice = 84
    print('この本は いくらですか?')  # 询问价格
    priceAStr = bookPrice.__str__() + '円です'
    print(priceAStr)
    totalPrice = totalPrice + bookPrice
    print('その鉛筆は?')
    print(pencilPrice)
    totalPrice += pencilPrice  # A += B ==> A = A + B
    print('全部でいくらですか?')
    totalPrice = totalPrice.__str__() + '円です'
    print(totalPrice)
    # 哈哈,最近在日语入门,有点疯魔,不要介意

    示例中,变量 totalPrice 被进行了多次赋值,而且可以看到,对于同一变量,可以为其赋予不同类型的值。

    这种变量本身类型不固定的语言称之为动态语言,与之对应的是静态语言。静态语言在定义变量时必须指定变量类型,如果赋值的时候类型不匹配,就会报错。也可以理解为强类型与弱类型,Python 是弱类型的语言。

    和静态语言相比,动态语言更灵活。

    注意理解赋值语句与数学上的等号的区别。

  7. 常量

    所谓常量就是不能变的变量,比如常用的数学常数 π 就是一个常量。在 Python 中,通常用全部大写的变量名表示常量:

    PI = 3.14159265359

    但其实 Python 中并不存在常量,因为即便是通过大写的方式生命变量名,但它并没有与其他语言的 const 类似的修饰符,它不能保证它所谓的常量不被修改。而使用大写标识常量也只是人为的规定而已,实际上是可以被修改的。简单来说,Python 中没有常量

Python 中的除法:

Python 的除法有两种方式:

  1. / 获取浮点数结果。

    这种除法方式的结果始终都是浮点数,不论是否能够被整除。

    print(10 / 3)  # 3.3333333333333335
    print(9 / 3)  # 3.0
  2. // 被称为地板除。

    这种除法会舍去运算结果的小数部分,如果是整数相除,则结果为整数类型,否则,结果为浮点类型,例如:

    print(10 // 3)  # 3
    print(9 // 3)  # 3
    print(9.6 // 3)  # 3.0
    print(9.6 // 3.2)  # 2.0
    print(0.0006 // 3)  # 0.0

注意:和 Java 不同,Python 的整数没有大小限。Python 的浮点数也没有大小限制,但是超出一定范围就直接表示为 inf (无限大)。

# 字符串和编码

在最新的 Python 3 版本中,字符串是以 Unicode 编码的,也就是说,Python 的字符串支持多语言。

对于单个字符的编码,Python 提供了 ord() 函数获取字符的整数表示, chr() 函数把编码转换为对应的字符,例如:

print(ord('A'))  # 65
print(ord('甲'))  # 30002
print(ord('あ'))  # 12354
print(chr(65))  # A
print(chr(30002))  # 甲
print(chr(12354))  # あ

另外,你也可以直接使用字符的整数编码,例如:

print('\u7532\u4e59\u4e19\u4e01')  # 甲乙丙丁

关于中文转码,可以使用在线转码工具:https://www.bejson.com/convert/unicode_chinese

由于 Python 的字符串类型是 str ,在内存中以 Unicode 表示,一个字符对应若干个字节。如果要在网络上传输,或者保存到磁盘上,就需要把 str 变为以字节为单位的 bytes

Python 对 bytes 类型的数据用带 b 前缀的单引号或双引号表示:

x = b'ABC'

以 Unicode 表示的 str 通过 encode() 方法可以编码为指定的 bytes ,例如:

print('ABC'.encode('ascii'))  # b'ABC'
print('中文'.encode('utf-8'))  # b'\xe4\xb8\xad\xe6\x96\x87'
print('中文'.encode('ascii'))  # 报错,因为 ASCII 无法表示中文

反过来,如果需要从磁盘上读取字节流,就需要将 bytes 转换为字符串,这时就需要使用 decode() 方法:

print(b'ABC'.decode('ascii'))  # ABC
print(b'\xe4\xb8\xad\xe6\x96\x87'.decode('utf-8'))  # 中文

Python 还提供了一个 len() 函数,用于计算字符长度或字节数:

print(len('中文'))  # 2
print(len(b'\xe4\xb8\xad\xe6\x96\x87'))  # 6
len('中文'.encode('utf-8'))  # 6

在 Unicode 编码中,一个中文字符占用 3 个字节,在字符串操作过程中,如果经常需要对将字符串与字节流进行转换,则需要尽量保持编码的一致性。

我们在保存 Python 源代码时,为了让 Python 解释器在读取源码时按照 UTF-8 的编码读取,我们可以在文件开头位置指定读取的编码方式:

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

字符串的格式化方式:

  1. % 占位符格式化

    在 Python 中,也可以对字符串进行格式化,它采用的格式化方式和 C 语言是一致的,用 % 实现,例如:

    print('Hello, %s!' % 'World')
    print('I\'m learning %s! It had took me %d hours yet.' % ('Python', 4))

    % 用于占位和替换,其对应的占位符有四种类型:

    1. %s 表示字符串占位。

    2. %d 表示整数占位。

    3. %f 表示浮点数占位。

    4. %x 表示十六进制整数占位。

    其中,格式化整数和浮点数还可以指定是否补 0 和整数与小数的位数:

    print('%.2f' % 3.1415926)  # 保留 2 位小数
  2. format() 格式化

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

    这种方式相比占位符更加麻烦。

  3. f-string 格式化

    这种格式化字符串的方法是使用以 f 开头的字符串,称之为 f-string ,它和普通字符串不同之处在于,字符串如果包含 {xxx} ,就会以对应的变量替换:

    r = 2.5
    s = 3.14 * r ** 2
    print(f'The area of a circle with radius {r} is {s:.2f}')
    # The area of a circle with radius 2.5 is 19.62

# list 和 tuple

# list

list(列表)是 Python 内置的一种数据类型,它是一种有序的集合,可以随时添加和删除其中的元素。

使用 len() 函数则可以获取到集合的大小。

language = ['Zh-CN', 'En', 'Ja']
print(len(language))  # 3

Python 中的列表概念 Java 基本一样,都是一个有序的列表,都是以 0 作为起始索引,所以超出范围会报越界错误。

我们可以向列表中插入追加数据,数据会默认追加到最后位置。

language = ['Zh-CN', 'En', 'Ja']
language.append('Zh-TW')
print(language)  # ['Zh-CN', 'En', 'Ja', 'Zh-TW']

当然,也可以将新的数据插入到指定的下标位置,例如:

language = ['Zh-CN', 'En', 'Ja']
language.insert(1, 'Zh-TW')
print(language)  # ['Zh-CN', 'Zh-TW', 'En', 'Ja']

删除列表中的元素,需要使用 pop() ,如下:

language = ['Zh-CN', 'Zh-TW', 'Ko', 'En', 'Ja', 'Fran']
language.pop()  # 不指定 pop () 函数的参数,则删除最后一个元素
print(language)
language.pop(2)  # 删除指定下标位置的元素
print(language)  # ['Zh-CN', 'Zh-TW', 'En', 'Ja']

替换元素:

language = ['Zh-CN', 'En', 'Ja']
language[0] = 'Zh-TW'
print(language)  # ['Zh-TW', 'En', 'Ja']

列表中元素的数据类型并不一定要保持一致,如果数组列表中包含有列表类型的元素,则被称为多维数组。

# tuple

tuple(元组)是另一种有序列表,它和 list 非常相似,但 tuple 一旦初始化就不能修改。

language = ('Zh-CN', 'En', 'Ja')
language[0] = 'Zh-TW'  # 报错:'tuple' object does not support item assignment
print(language)

元组列表没有提供 append()insert() 类似的方法。

因为 tuple 初始化后是不可改变的,因而更具有安全性。

tupleA = ()  # 定义一个空的 tuple
tupleB = (5,)  # 当 tuple 内只有一个元素时,也需要添加逗号,以避免和数学公式符号混淆
print(len(tupleB))  # 1

可变的 tuple ?

t = ('a', 'b', ['A', 'B'])
t[2][0] = 'X'
t[2][1] = 'Y'
print(t)  # ('a', 'b', ['X', 'Y'])

这里的可变并不是真正的可变,这里变化的只是 tuple 内部的 list 元素,而 tuple 本身的指针指向并没有变。

# 条件判断

Python 使用 if-elif-else 作为条件判断的语法结构,其中 elif 其实是 else if 的缩写,该语法的完整形式的使用如下:

condition = 'A'
if condition.__eq__('A'):
    print('do A')
elif condition.__eq__('B'):
    print('do B')
elif condition.__eq__('C'):
    print('do C')
else:
    print('do D')

if 判断条件不一定要求是布尔类型或布尔类型的表达式,也可以是一些其他的数据类型,如果这些数据类型是非零数值、非空字符串、非空数组等,条件判断则为 True ,否则为 False ,例如:

str = input('Please input and enter to judge true or false: ')
if str:
    print('Your input is: ' + str + ', it\'s true')
else:
    print('Your input is: ' + str + ', it\'s false')

通过输入不同内容,即可判断该输入内容是否为 True ,可以在您的开发工具中尝试一下,并观察返回结果。但同时,请注意,通过 input() 获取到的用户输入的所有内容都是字符串类型。

如需要将字符串内容转换为数值型,可以使用 int() 函数。

# 循环

Python 中有两种循环方式:

  1. for...in

    total = 0
    arr = [1, 2, 3, 4, 5]
    for num in arr:
        total += num
    print(total)

    Python 也提供了一个 range() 方法用于生成整数序列,而通过 list() 方法可以将整数序列转换为数组,例如:

    str = '| '
    arr = list(range(5))
    for num in arr:
        str += num.__str__() + ' | '
    print(str)  # | 0 | 1 | 2 | 3 | 4 |

    range() 函数有两个参数,一个是起始值,一个是终止值,当起始为 0 时,可以省略不写。

  2. while

    对于 while 循环,只要条件满足,就不断循环,条件不满足时退出循环。

    # 计算 100 以内所有奇数之和
    sum = 0
    n = 99
    while n > 0:
        sum += n
        n = n - 2
    print(sum)

breakcontinue

  1. break

    在循环中, break 语句可以提前退出循环。

    # 获取大于 100 的最小立方数
    result = 0
    n = 0
    while True:
        n += 1
        result = n * n * n
        if result > 100:
            break
    print(result)  # 125
  2. continue

    在循环过程中,也可以通过 continue 语句,跳过当前的这次循环,直接开始下一次循环。

    # 获取 100 以内所有奇数的总和
    total = 0
    n = 0
    while n < 100:
        n += 1
        if (n % 2) == 0:
            continue  # 当条件满足时,进入下一次循环
        total += n
    print(total)  # 2500

breakcontinue 的区别在于,前者是直接跳出整个循环结构,而后者当前正在循环的某一轮,可以理解为跳过当前循环,继续下一次。另外, break 往往适用于从众多数据中找到某一个符合特定条件的数据,而 continue 则适用于从众多数据中找到某一系列符合条件的数据。

注意:由于逻辑不够完善,或循环和跳出语句使用不当,可能会出现死循环的情况,就需要终止当前程序并检查代码逻辑。(在终端可使用 Ctrl + C 终止程序)

# dict 和 set

# dict

Python 内置了字典(在其他语言中也称为 map ),它使用 key-value (键值对)的方式存储,具有极快的查找速度。

data = {'dataA': 30, 'dataB': 73, 'dataC': -7}
print(data['dataB'])  # 73

也可以对 dict 中的值进行添加、修改或移除等操作:

data = {'dataA': 30, 'dataB': 73, 'dataC': -7}
print(len(data))  # 计算 dict 元素个数
data['dataB'] = 54  # 根据 key 值修改 dict 中的元素
data['dataD'] = 108  # 向 dict 中添加元素
data.pop('dataA')
print(data)  # {'dataB': 54, 'dataC': -7, 'dataD': 108}
# dict 的遍历
for key in data:
    print(data[key])
data.clear()  # 清空 dict
print(data)  # {}

但是,在根据 key 从 dict 中获取值时,可能会出现 key 不存在的情况,这时就会引起程序报错,我们可以通过如下方式避免:

data = {'dataA': 30, 'dataB': 73, 'dataC': -7}
# 方式一:判断要查询的 key 是否存在
if 'dataD' in data:
    print(data['dataD'])
# 方式二:推荐
print(data.get('dataD'))  # 未查询到时,返回 None
print(data.get('dataD', -1))  # 未查询到时,指定返回内容(这里是 - 1)

注意:在 Python 交互环境中,返回值为 None 是不会输出到控制台的,另外, dict 内部元素的存放顺序和 key 放入的顺序无关。

dict 和 list 比较:

  1. 对于查找和插入,dict 处理极快,不会随着 key 的增加而变慢,而 list 则会因为元素的不断增加变慢。

  2. 对于内存占用而言,dict 由于要存储键值对,占用内存较多,而 list 则占用很少。

所以,list 的优势在于空间占用少,而 dict 的优势在于时间消耗少。

此外,由于 dict 是通过 key 来计算 value 的存储位置,所以程序需要保证 key 的对象的不可变性,这种不可变性,需要通过哈希算法进行保证。在 Python 中,字符串和整数都是不可变的,可以作为 dict 的 key 使用,但 list 却是可变的,不应当作为 key 使用。

# set

set 和 dict 类似,也是一组 key 的集合,但是 set 中不存储 value,由于 key 是不可重复的,所以 set 中不存在重复的数据。

要创建一个 set,需要先提供一个 list 作为输入集合:

s1 = {1, 2, 2, 3}  # 直接声明 set,重复值时无效的
s1.add(4)  # 向 set 中添加元素
s1.add(1)  # 如果添加元素已存在,不会引起报错,但也不会重复添加
s1.remove(2)  # 移除 set 中某个元素
print(s1)  # {1, 3, 4}
print(len(s1))  # 计算元素个数
list1 = [1, 2, 3, 3, 3]
s2 = set(list1)  # 将 list 转为 set
print(s2)  # {1, 2, 3}
s1.clear()  # 清空 set

set 无序不重复的元素集合,它可以做数学意义上的交集、并集处理:

s1 = {1, 2, 3, 4}
s2 = {2, 4, 6, 8}
# 交集
print(s1 & s2)  # {2, 4}
# 并集
print(s1 | s2)  # {1, 2, 3, 4, 6, 8}
# set 中存放 list 会导致报错
s3 = {[1, 2], 2, 3}
print(s3)  # 报错:TypeError: unhashable type: 'list'

set 中存放 list 作为元素会引起报错,其原理和 dict 是一样的。

# 函数

Python 内置了很多有用的函数,我们可以直接调用,例如在上文中频繁使用到的 len()

# 调用函数

要调用一个函数,需要知道函数的名称和参数,而且需要保证参数的类型及顺序的一致,否则就会引起程序错误。

也有一些函数可以接收任意多个参数。

print(abs(-13.2))  # 13.2
print(max(2, 3, 18, 9, 33, 5))  # 33
# 类型转换函数
print(int('15'))  # 15
print(float('2.18'))  # 2.18
print(str(1.62))  # 1.62
print(bool(1))  # True
print(bool(''))  # False

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

a = abs
print(a(-6))  # 6

# 定义函数

在 Python 中,定义一个函数要使用 def 语句,依次写出函数名、括号、括号中的参数和冒号 : ,然后,在缩进块中编写函数体,函数的返回值用 return 语句返回(无返回值不写即可)。

def division(m, n):
    if n == 0:
        return  # return None 可以直接简写为 return
    else:
        return m / n
print(division(10, 4))  # 2.5

当暂未想好如何定义函数的实现时,可以定义一个空函数,它的目的是进行占位,当想好函数的实现内容时在填充即可。

i = 1001
u = 1005
def say(str):
    print(str)
    pass
if i == u:  # if I were You
    pass  # I would ...  # if 内容暂未确定,但留空又会报错
say('Anyway!!!')  # but I'm not, so, >>>

调用函数时,如果参数个数不对,Python 解释器会自动检查出来,并抛出 TypeError 错误,但是如果参数类型不对,Python 解释器就无法帮我们检查。

但是现在的 IDE 工具很智能了,PyCharm 在代码编写时就可以帮助我们规避很多错误,例如参数检查。

因此,我们定义函数的时候,可以进行参数类型检查:

def division(m, n):
    if not isinstance(m, (int, float)):
        raise TypeError('您传入的除数必须是数值型')
    if not isinstance(n, int):
        raise TypeError('您传入的被除数必须是整数')
    if n == 0:
        return
    else:
        return m / n
print(division(10, 4.5))  # 抛出【您传入的被除数必须是整数】错误

Python 函数也可以返回多个值:

def maxAndMin(arr):
    max = arr[0]
    min = arr[0]
    for item in arr:
        if item > max:
            max = item
        if item < min:
            min = item
    return max, min
max, min = maxAndMin([2, 53, 6, -1, 28, 7])
print('max = ', max)  # max =  53
print('min = ', min)  # min =  -1
print(maxAndMin([2, 53, 6, -1, 28, 7]))  # (53, -1)

从示例中可以看到,Python 函数返回多个值其实是一种假象,它返回多个值,其实本质上是返回一个 tuple 。

尝试定义一个函数 quadratic(a, b, c) 来求出一元二次方程的解,公式定义:ax2+bx+c=0ax^2 + bx + c = 0

提示:

  1. 一元二次方程的求根公式为:

    $ x = \frac{-b±\sqrt {b^2-4ac}}{2a} \end {array} $

使用下面的代码来尝试一下吧:

import math
def quadratic(a, b, c):
    pass  # 在这里写入你的代码逻辑
a = input('Input parameter a: ')
b = input('Input parameter b: ')
c = input('Input parameter c: ')
try:
    a = float(a)
    b = float(b)
    c = float(c)
except ValueError:
    raise ValueError('参数错误:请传入数值类型的参数')
print('Get result = ' + quadratic(a, b, c).__str__())

+++ 点击这里查看参考答案。

import math
def quadratic(a, b, c):
    x1 = None
    x2 = None
    # 1. 参数校验
    if a == 0:
        print('参数错误:一元二次方程的a值不能为0')
        return
    # 2. 判断有多少个解
    d = b ** 2 - 4 * a * c
    if d < 0:  # 该方程只有一个解,或两个解相同
        print('该一元二次方程没有【无解】')
    elif d == 0:
        x1 = b / (-2 * a)
        x2 = x1
    else:  # 该方程有两个解
        x1 = (-b + math.sqrt(d)) / (2 * a)
        x2 = (-b - math.sqrt(d)) / (2 * a)
    return x1, x2
a = input('Input parameter a: ')
b = input('Input parameter b: ')
c = input('Input parameter c: ')
try:
    a = float(a)
    b = float(b)
    c = float(c)
except ValueError:
    raise ValueError('参数错误:请传入数值类型的参数')
print('Get result = ' + quadratic(a, b, c).__str__())

可以尝试输入一些下面这几组参数进行验证:

1, 2, -3 -> (1.0, -3.0)

4, 6, 0 -> (0.0, -1.5)

1, 4, 4 -> (-2.0, -2.0)

+++

# 函数的参数

  1. 位置参数

    在前文中,我们已经多次使用到位置参数,所谓位置参数,就是在调用函数时,指定的实际参数的数量,必须和形式参数的数量保持一致,否则就会抛出 TypeError 异常。

    def power(x, n):
        s = 1
        while n > 0:
            s *= x
            n -= 1
        return s
    print(power(2, 3))
  2. 默认参数

    默认参数是指当函数调用方不传递时,函数指定参数具有一个默认的值。

    def power(x, n=2):  # 次方数默认为 2
        s = 1
        while n > 0:
            s *= x
            n -= 1
        return s
    print(power(4))

    使用默认参数需要记住以下原则:

    • 位置参数放前,默认参数放后。

    • 确定性大的参数放前,确定性小的参数放后。

    • 调用的函数需要传递默认参数,但参数位置不匹配时,需要指定参数名。

      def test(name, age, school='', gender='Male'):
          pass
      a = 1
      test('Susan', 20, gender='Female')
    • 默认参数必须指向不变对象【坑点】。

      例如,当默认参数定义为数组时,将出现如下情况:

      def add_end(l=[]):
          l.append('END')
          return l
      print(add_end([1, 2, 3]))  # [1, 2, 3, 'END']
      print(add_end())  # ['END']
      print(add_end())  # ['END', 'END']
      print(add_end())  # ['END', 'END', 'END']

      这是因为默认参数指定为 list 时,指定的其实只是一个指针,当我们每次使用默认参数,使用的都是指针对应的值,多次调用就会反复修改 list 指针对应的值,导致多重拼接。

      要理解这一点,请务必理解可变对象和不可变对象。(在 Java 语言中,被称为值类型和引用类型)

  3. 可变参数

    可变参数,即参数的个数是可以变化的,可以传入任意多个参数(包括 0 个)。定义函数可变参数的方式如下:

    def sum(*nums):
        sum = 0
        for num in nums:
            sum += num
        return sum
    print(sum(3, 4, 1, 5, 29))  # 42

    可变参数接收到的是一个 tuple 类型。

    如果调用函数时,某一个参数本身即是 tuple 类型的参数,为了区分可变参数与参数的 tuple 类型参数,可以使用如下方式调用函数:

    def sum(*nums):
        sum = 0
        for num in nums:
            sum += num
        return sum
    nums = [3, 4, 1, 5, 29]
    print(sum(*nums, 8))  # 50

    这里是将 tuple 转换为可变参数,再传入到函数中。

  4. 关键字参数

    关键字参数允许传入任意多个参数,这些关键字参数在函数内部会自动组装成一个 dict 。

    def person(name, age, **kw):
        print('name:', name, 'age:', age, 'extra:', kw)
    person('Lily', 24, city='Shanghai', job='teacher')  # name: Lily age: 24 extra: {'city': 'Shanghai', 'job': 'teacher'}

    关键字参数可以扩展函数的功能,函数调用方可以根据情况自由选择是否传递某些参数字段,上面的函数也可以做如下简化:

    def person(name, age, **kw):
        print('name:', name, 'age:', age, 'extra:', kw)
    extra = {'city': 'Shanghai', 'job': 'teacher'}
    person('Lily', 24, **extra)

    注意:函数内的关键字参数只是传入实际参数的一份拷贝,对它进行改动并不会影响到函数调用方的变量。

  5. 命名关键字参数

    对于关键字参数,调用方对参数名称可以任意指定,但如需进行参数名称限制,我们可以进行如下声明:

    def person(name, age, *, city, job):
        print('name:', name, 'age:', age, 'city:', city, 'job:', job)
    extra = {'city': 'Shanghai', 'job': 'teacher'}
    person('Lily', 24, **extra)

    这种通过 * 分隔被命名的关键字参数,可以实现对参数命名的限制,但通过这种方式传递参数时,调用方必须传递所有关键字参数(除非关键字参数中使用了默认参数),否则会引起报错。

    另外,当函数中已经定义了一个可变参数,那么,这个函数中的可变参数前就不再需要使用 * 分隔符了,例如:

    def person(name, age, *args, city='Taiyuan', job):
        pass
  6. 参数组合

    在 Python 中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这 5 种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数,例如:

    def f1(a, b, c=0, *args, school, **kw):
        print('a =', a, 'b =', b, 'c =', c, 'args =', args, school, 'kw =', kw)
    f1('zhangsan', 'nanjing', 5, 12, 33, school='NanJin University', email='abc@q.com', phone='15625376')
    # a = zhangsan b = nanjing c = 5 args = (12, 33) NanJin University kw = {'email': 'abc@q.com', 'phone': '15625376'}

    但是,由于多种类型的组合参数可读性并不太高,不推荐同时使用较多种类型的参数进行组合。

# 递归函数

在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数,例如:

# 阶乘
def fact(n):
    if n == 1:
        return 1
    return n * fact(n - 1)
print(fact(5))  # 120

递归函数的优点是定义简单,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。

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

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

对于 Python 而言,因为其解释器并未对尾递归做优化,所以,Python 使用尾递归方式也不能解决栈溢出的问题。

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

# 尾递归阶乘
def fact(n):
    return fact_iter(n, 1)
def fact_iter(num, product):
    if num == 1:
        return product
    return fact_iter(num - 1, num * product)
print(fact(5))  # 120

两者的区别在于:

# 递归方式:
fact(3)
3 * fact(2)
3 * (2 * fact(1))
3 * (2 * 1)
3 * 2
6
# 尾递归方式:
fact_iter(3, 1)
fact_iter(2, 3)
fact_iter(1, 6)
6

# 高级特性

# 切片

在 Python 中,可以通过切片截取数组中任意任意位置的元素。

arr = []
for i in range(100):
    arr.append(i)
print(arr[0:3])  # [0, 1, 2]
print(arr[:3])  # [0, 1, 2]
print(arr[10:14])  # [10, 11, 12, 13]
print(arr[-1])  # [99] # 倒数第一个元素
print(arr[-4:-1])  # [96, 97, 98]
print(arr[-4:])  # [96, 97, 98, 99] # 倒数四个参数
print(arr[-4:0])  # []
print(arr[:10:3])  # [0, 2, 4, 6, 8] # 前 10 个数中,每 3 个数取一个
print(arr[:])  # 拷贝原数组

倒数第一个元素的索引是 -1。

字符串和 tuple 也是看做是一种 list ,因此它们同样支持切片操作:

print((0, 1, 2, 3, 4, 5)[:3])  # (0, 1, 2)
print('PYTHON'[:3])  # PYT
print('PYTHON'[:5:2])  # PTO

在很多编程语言中,针对字符串提供了很多各种截取函数(例如,substring),其实目的就是对字符串切片。Python 没有针对字符串的截取函数,但可以直接使用切片方式实现。

# 迭代

对于一个 list 或 tuple ,我们可以通过循环来进行遍历,这种遍历被称为迭代。

在 Python 中,迭代是通过 for ... in 来完成的,它既可以迭代一个 list 和 tuple ,也可以迭代字符串。

而且在 Python 中,程序只需要关注对象是否可迭代,而不需要关注迭代对象的类型,判断对象是否可迭代:

from collections.abc import Iterable
print(isinstance('abc', Iterable))  # True
print(isinstance([1, 2, 3], Iterable))  # True
print(isinstance(123, Iterable))  # False # 整数不可迭代

另外,Python 也内置了一个 enumerate 函数,可以将 list 变为一个 索引-元素 对,这样就可以在 for 循环中同时迭代索引和元素本身:

for i, v in enumerate(['A', 'B', 'C']):
    print(i, v)

Python 也可以同时遍历多个内部元素,但需要注意个数需要完全匹配,否则会报错:

for x, y, z in [(1, 1, 3), (2, 4, 3), (3, 9, 3)]:
    print(x, y, z)
for x, y, z in [(1, 1, 3), (2, 4, 3), (3, 9)]:  # ValueError: not enough values to unpack
    print(x, y, z)

# 列表生成式

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

print([x * x for x in range(1, 11)])  # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

for 循环后面还可以加上条件判断:

print([x * x for x in range(1, 11) if x % 2 == 0])  # [4, 16, 36, 64, 100]
print([x * 2 if x > 5 else x * x for x in range(1, 11) if x % 2 == 0])  # [4, 16, 12, 16, 20]

注意:在列表生成式中, for 后面的条件判断的作用是进行筛选,所以不能带有 elifelse 判断。而 for 前面也可以使用 if 语句,但如果是在 for 前面添加条件判断,它的作用则是作为一个表达式,这时它必须有 else 语句,以便能够保证所有的情况都得到处理。

在一个列表生成式中, for 前面的 if ... else 是表达式,而 for 后面的 if 是过滤条件,不能带 else

还可以使用多个 for 循环生成全排列:

print([a + b + c for a in 'BC' for b in '12' for c in 'あう'])
# ['B1 あ ', 'B1 う ', 'B2 あ ', 'B2 う ', 'C1 あ ', 'C1 う ', 'C2 あ ', 'C2 う ']

运用列表生成式,可以写出非常简洁的代码。例如,列出当前目录下的所有文件和目录名:

import os
print([d for d in os.listdir('.')])  # ['.idea', 'main.py', 'venv']

for 循环其实可以同时使用两个甚至多个变量,比如 dict 的 items () 可以同时迭代 key 和 value:

d = {'x': 'A', 'y': 'B', 'z': 'C'}
for k, v in d.items():
    print(k, '=', v)
print([k + '=' + v for k, v in d.items()])  # ['x=A', 'y=B', 'z=C']

另外,还可以对字符串进行处理,例如:

L = ['Hello', 'World', 'IBM', 'Apple']
print([s.lower() for s in L])  # ['hello', 'world', 'ibm', 'apple']

# 生成器

通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含 100 万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。

但如果列表元素可以按照某种算法推算出来,那我们就可以在循环的过程中不断推算出后续的元素,这样就不必创建完整的 list,从而节省大量的空间。在 Python 中,这种一边循环一边计算的机制,称为生成器: generator

要创建一个 generator,有很多种方法。第一种方法很简单,只要把一个列表生成式的 [] 改成 () ,就创建了一个 generator:

d = {'x': 'A', 'y': 'B', 'z': 'C'}
g = (k + '=' + v for k, v in d.items())
print(g)  # <generator object <genexpr> at 0x00000210690ADBA0>
print(next(g))  # x=A
print(g.__next__())  # y=B
print(next(g))  # z=C
print(next(g))  # 抛出异常 StopIteration

generator 可以通过实例中 next() 的方式依次取出相应的值,但这种做法较为笨拙,而且越界时会抛出异常,由于 generator 也是可遍历对象,因此更加常见的是结合循环进行使用:

d = {'x': 'A', 'y': 'B', 'z': 'C'}
g = (k + '=' + v for k, v in d.items())
for n in g:
    print(n)

另一种创建 generator 的方法:

著名的斐波拉契数列(Fibonacci),除第一个和第二个数外,任意一个数都可由前两个数相加得到:

1, 1, 2, 3, 5, 8, 13, 21, 34, ...

# 函数实现斐波拉契数列
def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        print(b)
        a, b = b, a + b
        n = n + 1
    return 'done'
fib(10)

将上述算法修改为一个生成器:

# 斐波拉契数列生成器
def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b  # yield 中断执行并返回每轮执行的结果
        a, b = b, a + b
        n = n + 1
    return 'done'
# 这里得到的 fib (10) 是一个 generator 对象
for n in fib(10):
    print(n)

这就是第二种创建 generator 的方法,如果一个函数中定义了 yield 字段,这个函数就不再是一个普通函数,而是一个生成器。

函数和生成器有一个较为突出的区别:函数是按照顺序执行,如果遇到 return 语句或执行到最后一行语句就返回;而生成器却是在每次调用 next() 方法时才执行,遇到 yield 语句时就返回,而当再次执行 next() 时,就从上一次 yield 返回语句处继续执行。

# 在这里,注释中的 [ ] 表示执行顺序,其后表示输出内容
def odd():
    print('step 1')  # [1] step 1
    yield 1
    print('step 2')  # [3] step 2
    yield 3
    print('step 3')  # [5] step 3
    yield 5
odd = odd()
print(next(odd))  # [2] 1
print(next(odd))  # [4] 3
print(next(odd))  # [6] 5
# 也可以使用循环进行遍历
# for n in odd():
#     print(n)

如果在取出生成器中的所有值之后,希望获得生成器中返回语句定义的内容,可以通过异常捕获的方式:

def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b  # yield 中断执行并返回每轮执行的结果
        a, b = b, a + b
        n = n + 1
    return 'done'
fib = fib(6)
while True:
    try:
        x = next(fib)
        print('g:', x)
    except StopIteration as e:
        print('Generator return value:', e.value)  # 获取到生成器中 return 语句的返回值。
        break

# 迭代器

在前文的迭代部分,我们已经提到过迭代器,可以直接作用于 for 循环的对象,都被称为可迭代对象,并且可以使用 isinstance() 函数来判断一个对象是否可迭代。而其实,可以被 next() 函数调用并不断返回下一个值的对象,即为迭代器(Iterator)。

生成器都是迭代器对象,而数组、字典、字符串等虽然具有可迭代属性,却并不是迭代器对象,这是因为迭代器对象可以被 next() 函数不断获取下一个值,但其本身却并不知晓当前序列的长度,只能通过 next() 函数实现按需计算下一个元素。因此,迭代器的计算是惰性的,只有在需要返回下一个数据的时候,才会进行计算。

迭代器甚至可以表示一个无限大的数据流,而列表等类型则是有限的。判断是否是迭代器对象很简单,只需要知道它是否是可作用于 next() 函数的对象即可。

# 函数式编程

函数是 Python 内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计。函数就是面向过程的程序设计的基本单元。

而函数式编程(Functional Programming)虽然也可以归结到面向过程的程序设计,但其思想更接近数学计算。

函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!

Python 对函数式编程提供部分支持。由于 Python 允许使用变量,因此,Python 不是纯函数式编程语言。

# 高阶函数

函数本身可以赋值给一个变量,即变量可以指向函数。

当一个变量指向了某个函数,那么就可以通过这个变量来调用该函数:

f = abs
print(f(-12))  # 12

其实,函数名本身就是一个指向函数的变量,那么,这个变量自然也可以被重新赋值:

from collections.abc import Iterable
abs = isinstance
print(abs('abc', Iterable))  # True
print(abs(-5))  # 报错:TypeError

由此可知,函数本身也是一个变量,反过来说,我们可以定义一个函数类型的变量,而函数本身是可以接收变量作为参数的,那么,同样函数就也可以接收一个函数类型的参数。这种接收函数作为参数的函数,就被称为高阶函数,例如:

def add(x, y, f):
    return f(x) + f(y)
result = add(2, -4, abs)
print(result)  # 6

总而言之,把函数作为参数传入,这样的函数称为高阶函数,函数式编程就是指这种高度抽象的编程范式。

# map / reduce

Python 内建了 map()reduce() 函数。

  1. map

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

    def f(x):
        return x * x
    r = map(f, list(range(1, 10)))  # 返回值是一个 Iterator 对象
    print(list(r))  # [1, 4, 9, 16, 25, 36, 49, 64, 81]

    通过 map 传递不同的函数参数,可以实现对数组的不同处理。

  2. reduce

    reduce 把一个函数作用在一个序列上,这个函数必须接收两个参数, reduce 会把结果继续和序列的下一个元素做累积计算,其效果就是:

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

    例如:

    from functools import reduce
    def f(x, y):
        return (y - 2) ** x
    result = reduce(f, list(x for x in range(2, 7) if x % 2 == 0))
    print(f(f(2, 4), 6))  # 256
    # f(f(2, 4), 6) ==> (6 - 2) ** ((4 - 2) ** 2) ==> 4 ** (2 ** 2) ==> 4 ** 4 ==> 256
    print(result)  # 256

    当然,map 和 reduce 也可以进行组合使用。

# filter

Python 内建的 filter() 函数用于过滤序列,它和 map() 类似,也接收一个函数和一个序列。但不同的是, filter() 把传入的函数依次作用于每个元素,然后根据返回值是 True 还是 False 决定保留还是丢弃该元素,例如:

# 过滤掉奇数,保留偶数
def is_odd(x):
    return x % 2 == 1
f = filter(is_odd, list(range(10)))
print(list(f))  # [1, 3, 5, 7, 9]

除了上述示例的用法,还可以有很多用途,例如:去除字符串中的特殊字符、移除数组中的空元素、保留数组中具有某种特征的元素等,总之,filter 的主要作用在于对序列进行过滤和赛选。

map 和 filter 返回的都是 Iterator 。由于迭代器是惰性序列,因此,要强迫其完成计算结果,需要使用 list() 函数获取所有结果并返回数组。

可以尝试用已有知识求出 100 以内所有素数。

# sorted

排序也是在程序中经常用到的算法。无论使用冒泡排序还是快速排序,排序的核心是比较两个元素的大小。如果是数字,我们可以直接比较,但如果是字符串或者两个 dict 比较的过程就必须通过函数抽象出来。

Python 内置的 sorted() 函数就可以对 list 进行排序:

print(sorted([36, 5, -12, 9, -21]))  # [-21, -12, 5, 9, 36]

sort() 作为一个高阶函数,除了默认的排序之外,它还可以接收一个 key 指定的函数来实现自定义的排序,例如:

def f(x):
    return x ** x - x
# 按绝对值大小排序
print(sorted([36, 5, -12, 9, -21], key=abs))  # [5, 9, -12, -21, 36]
# 按自定义函数方式进行排序
print(sorted([36, 5, -12, 9, -21], key=f))  # [-12, -21, 5, 9, 36]

而如果要对字符串进行排序,sort 默认会对字符串的 ASCII 码进行比较。

而要进行反向排序,只需要指定一个 reverse=True 参数即可:

print(sorted([36, 5, -12, 9, -21]))  # [-21, -12, 5, 9, 36]
print(sorted([36, 5, -12, 9, -21], reverse=True))  # [36, 9, 5, -12, -21]

# 返回函数

高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回,例如:

# 可变参数求和的懒执行方式
def lazy_sum(*args):
    def sum():
        ax = 0
        for n in args:
            ax = ax + n
        return ax
    return sum
f = lazy_sum(1, 3, 5, 7, 9)
# 返回的函数并不是立即执行
print(f)  # <function lazy_sum.<locals>.sum at 0x000001875C48EB80>
# 而是 f () 函数调用时才执行
print(f())  # 25

这种在函数中定义的函数,称为内部函数,内部函数可以引用外部函数的参数和局部变量,当外部函数被调用并返回内部函数时,相关的参数和变量都会保存在返回的函数中,这种被称为闭包的结构在许多程序中都有广泛的应用。

另外,每次调用外部函数时,返回的内部函数都是一个新的实例:

def fn(x):
    def f():
        return abs(x)
    return f
f1 = fn(-5)
f2 = fn(-5)
print(f1 == f2)  # False

另外,需要注意的是,返回函数不要引用任何循环变量,或者后续会发生变化的变量,反例如下:

def count():
    fs = []
    for i in range(1, 4):
        def f():
             return i * i
        fs.append(f)
    return fs
f1, f2, f3 = count()
print(f1())  # 9 (预期是 1)
print(f2())  # 9 (预期是 4)
print(f3())  # 9

这就会出现函数的返回值和预期不一致,这是因为在返回的函数中引用了循环变量,但这种返回函数并非立即执行,而当多个循环函数都被返回时,循环变量已经变为循环周期的最大值,所以最终调用函数进行计算时,就不会得到预期的循环效果产生的值。

而如果一定要引用循环变量,可以通过如下方式,将循环变量作为参数传入到另一内部函数中,这样就可以保证被绑定的循环变量参数不变:

def count():
    def f(j):
        def g():
            return j * j
        return g
    fs = []
    for i in range(1, 4):
        fs.append(f(i))  # f (i) 立刻被执行,因此 i 的当前值被传入 f ()
    return fs
f1, f2, f3 = count()
print(f1())  # 1
print(f2())  # 4
print(f3())  # 9

# 匿名函数

当我们在传入函数时,有些时候,不需要显式地定义函数,直接传入匿名函数更方便。

n = list(map(lambda x: x * x, list(range(1, 10))))
print(n)  # [1, 4, 9, 16, 25, 36, 49, 64, 81]

关键字 lambda 表示匿名函数,冒号前面的变量表示函数参数。

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

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

f = lambda x: x * x
print(f(4))  # 16
def fn(x):
    return lambda: x ** x
print(fn(3)())  # 27

# 装饰器

装饰器(Decorator)可以在代码运行期间,动态地增加功能,而其实本质上,装饰器就是一个返回函数的高阶函数。

由于函数也是一个对象,而且函数对象可以被赋值给变量,所以,通过变量也能调用该函数。而函数对象有一个 __name__ 属性,可以获取到函数的名字,据此,我们可以尝试实现一个简单的打印日志的功能:

def log(func):
    def wrapper(*args, **kw):
        print('call %s()' % func.__name__)
        return func(*args, **kw)
    return wrapper
@log  # 通过 Python 的 @语法,将装饰器置于函数的定义处,相当于执行了:say_hi = log (say_hi)
def say_hi(name):
    print('Hi,', name)
say_hi('Zhang San')
print(say_hi.__name__)
# call say_hi()
# Hi, Zhang San
# wrapper  # 函数名错误

当然,你也可以为以上程序添加断点,并观察其执行流程。

但是,如果装饰器本身需要传入参数,那就需要编写一个返回装饰器的高阶函数,相对而言会更加复杂:

def log(text):
    def decorator(func):
        def wrapper(*args, **kw):
            print('%s %s()' % (text, func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator
@log("Python")  # 相当于执行了:now = log ('Python')(say_hi)
def say_hi(name):
    print('Hi,', name)
say_hi('Zhang San')
print(say_hi.__name__)
# Python say_hi()
# Hi, Zhang San
# wrapper  # 函数名错误

但如代码所示,通过现在的方式调用,被装饰器修饰的函数(say_hi)在执行结束后,其函数名称被替换了,这是因为装饰器是将被修饰的函数(say_hi)作为参数传递到了装饰器的内部,并且最终将其赋值为装饰器内部定义的一个内部函数(wrapper),因此,装饰器修饰的函数所对应的名称(say_hi),其实际上指向的不再是原本定义的函数,而是指向其作为参数传递,并且被赋值到的装饰器的一个内部参数(wrapper),所以最终通过 say_hi.__name__ 获取到的函数名称变为了 wrapper

而要解决这样的问题,我们就需要在装饰器中实现类似 wrapper.__name__ = func.__name__ 的功能,但我们其实并不需要手动编写这样的代码,因为 Python 内置了一个 functools.wraps 函数可以实现这样的功能:

import functools  # 导入 functools 模块
def log(text):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kw):
            # wrapper.__name__ = func.__name__  # 当然,通过这种方式也是可以的,但不推荐
            print('%s %s()' % (text, func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator
@log("Python")
def say_hi(name):
    print('Hi,', name)
say_hi('Zhang San')
print(say_hi.__name__)
# Python say_hi()
# Hi, Zhang San
# say_hi  # 函数名正确

# 偏函数

Python 的 functools 模块提供了很多有用的功能,其中一个就是偏函数(Partial function)。

通过函数 functools.partial ,我们可以创建一个偏函数:

# hex 字符串转 10 进制
import functools
int2 = functools.partial(int, base=2)
print(int2('101000', base=10))  # 101000
print(int2('101000'))  # 40
# 相当于
print(int('101000', **{'base': 2}))  # 40

functools.partial 的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,这样调用这个新函数会更简单。而且,调用方可以根据自己的需要,选择是否传递新的值覆盖默认值。

创建偏函数时,实际上可以接收函数对象、 *args**kw 这 3 个参数,在上面的二进制示例中,实际上是固定了 int() 函数的关键字参数 base 。而其他偏函数也是类似地,是将向偏函数中传入的值作为远函数的一部分,自动添加到所有参数左侧,例如:

import functools
args1 = (5, 6, 7)
max1 = functools.partial(max, 10)
print(max1(*args1))
# 相当于
args2 = (10, 5, 6, 7)
max2 = max(*args2)
print(max2)

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

课间休息,听听音乐吧!

@start.audio

  • title: 音乐推荐
    list:

    • https://music.163.com/#/song?id=1501450521

    • https://music.163.com/#/song?id=28461702

    • https://music.163.com/#/song?id=857606

    • http://music.163.com/#/song?id=31545838

    • https://music.163.com/#/song?id=1815109509

    • https://music.163.com/#/song?id=1485319473
      @end.audio

# 模块

在计算机程序的开发过程中,随着业务逐渐完善,代码会越来越多,也越来越不容易维护。所以,为了便于更好地维护代码,提高开发效率,所有开发语言都有意识地将代码模块进行分化,这也是也是必然的。

为了编写可维护的代码,我们把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少,很多编程语言都采用这种组织代码的方式。在 Python 中,一个 .py 文件就称之为一个模块(Module)。

在不同的模块中,函数的命名是可以重复的,但是我们所编写的函数,应当尽量与 Python 的内置函数区别开来,避免引起不必要的误会和冲突。

可以在官方文档中查看 Python 的所有的内置函数。

另外,对于模块的命名, Python 按照目录来组织模块结构,并且称之为包(Package),而这种目录结构,通常推荐使用公司或企业的组织结构进行命名。

Python 的每个包目录下,都应当存在一个 __init__.py 文件,这个文件是必须的,否则,这个目录就不会被识别为包,而这个 __init__.py 文件本身可以是空文件,也可以有代码内容。

在开发时创建模块,命名时需要注意避开 Python 自带的模块名称,例如系统自带了 sys 模块,就不可再命名为 sys.py ,否则将导致系统的 sys 模块无法导入。

# 使用模块

Python 本身内置了很多非常有用的模块,只要安装完毕,这些模块就可以立刻使用,一个标准的 Python 文件模板如下:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" a test module """
__author__ = 'Chinmoku'
import sys
def test():
    args = sys.argv
    print(args)  # ['A:/pycharm-workspace/py-test/main.py']
    if len(args) == 1:
        print('Hello, world!')
    elif len(args) == 2:
        print('Hello, %s!' % args[1])
    else:
        print('Too many arguments!')
if __name__ == '__main__':
    test()

前两行是标准注释,第一行注释指定了 Python 的环境,可以让这个文件直接在 Unix/Linux/Mac 环境上运行;而第二行注释则表明了这个文件使用的编码格式,避免解释器读取代码时产生乱码。第四行的字符串是用来指定当前模块的文档注释内容,任何模块的第一个字符串都将被视为该模块的文档注释。而第六行的 __author__ 则指定了当前文件的开发者姓名,方便合作开发和维护。

要使用一个模块,第一步是导入该模块,与其他开发语言大同小异,Python 导入模块的语法如下:

import sys

sys 模块被导入后,就可以通过 sys 这个变量来访问该模块的所有功能。

sys.argv 变量使用 list 存储了命令行的所有参数,它最少有一个参数,即是当前 .py 文件的全名称。

另外,在一个模块中,我们可能会定义很多函数或变量,但是我们希望一些函数或变量仅仅在模块内部进行使用,这时就要通过 _ 前缀来实现。

正常的函数和变量名是公开的(public),可以被直接引用,,比如: abcx123PI 等;而特殊变量使用 __xxx__ 方式命名,可以被直接引用,但是有特殊用途,例如 __author____name__ 就分别代表作者和当前模块名,而且模块定义的文档注释其实也可以使用变量 __doc__ 来访问(非常规)。

类似 _xxx__xxx 这样的函数或变量就是非公开的(private),不应该被直接引用,比如 _abc__abc 等;

但是,Python 其实并没有一种方法可以完全限制访问 private 函数或变量,但是,从编程习惯上不应该引用 private 函数或变量。

外部不需要引用的函数尽量全部定义成 private,只有外部需要引用的函数才定义为 public。

# 安装第三方模块

在 Python 中,安装第三方模块,是通过包管理工具 pip 完成的。

一般来说,第三方库都会在 Python 官方的 pypi.python.org 网站注册,要安装一个第三方库,必须先知道该库的名称,可以在官网或者 pypi 上搜索,比如 Pillow 的名称叫 Pillow,因此,安装 Pillow 的命令就是:

pip install Pillow

在使用 Python 时,我们经常需要用到很多第三方库,例如,上面提到的 Pillow,以及 MySQL 驱动程序,Web 框架 Flask,科学计算 Numpy 等。用 pip 一个一个安装费时费力,还需要考虑兼容性。因此,更加推荐地是使用 Anaconda ,这是一个基于 Python 的数据处理和科学计算平台,它已经内置了许多非常有用的第三方库,可以从 Anaconda 官网下载安装包。

下载完成后进行安装,推荐不要在安装时勾选自动配置环境变量的选项,而是手动进行配置,配置示例:

path 环境变量中,添加如下配置:

A:\anaconda3
A:\anaconda3\Scripts
A:\anaconda3\Library\bin

在命令行中输入 python ,在回车输出的结果中,如果有 Anaconda 字符,就表示安装成功。

当我们试图加载一个模块时,Python 会在指定的路径下搜索对应的 .py 文件,如果找不到,就会抛出 ModuleNotFoundError 异常。

默认情况下,Python 解释器会搜索当前目录、所有已安装的内置模块和第三方模块,搜索路径存放在 sys 模块的 path 变量中:

import sys
print(sys.path)

如果要添加自定义的搜索目录,有以下两种方式:

  1. 直接修改 sys.path ,添加自定义的搜索目录:

    import sys
    sys.path.append('/Users/chinmoku/py_test)

    这种方法是在运行时进行修改,程序运行结束后即失效。

  2. 设置环境变量 PYTHONPATH ,该环境变量的内容会被自动添加到模块搜索路径中。

# 面向对象编程

由于本人的第一开发语言 Java 本身就是一种面向对象的语言,因此,部分与 Java 共通的思想或基础概念,这里会有所省略。

# 类和实例

Python 通过如下方式定义一个类:

class Student(object):
    pass

其中, class 是定义类的关键字, Student 是类名,而紧接着的 (object) 表示当前类所继承的父类,所有的类最终都会继承自 object 类。

根据已有的类创建类的实例:

# 定义一个类
class Student:
    def __init__(self, name):
        self.name = name
    def get_name(self):  # 方法
        return self.name
zhang = Student('zhang')
print(zhang)  # <__main__.Student object at 0x000002153559BA60>
print(zhang.get_name())  # zhang

__init__() 是一个特殊的函数,这个方法在获取类的实例时被调用,可以进行对实例进行一些初始化定义,它可以有多个参数,但第一个参数必须是 self ,表示创建的实例本身。在创建实例时,要根据 __init__() 函数所需要的参数匹配传值,但 self 参数在传值时可以忽略,因为 Python 解释器会自动将实例变量传入到这个函数中。

在类中声明的函数与普通函数基本一样,但在类中声明的函数默认会有一个 self 参数,和 __init__() 函数一样,调用函数时忽略 self 参数即可。

== 数据封装 == 是面向对象编程的一个重要特点。

我们定义一个类,并且通过 __init__() 函数可以创建出该类的实例,但对于创建的实例,是可以调用类内部定义的方法的,这些在类内部的函数与类本身是关联起来的,我们称这些函数为类的方法,如上述代码片段中的 get_name() 函数。而要调用类的方法也很简单,通过实例直接调用即可。这样,对于实例而言,它并不需要关心调用的方法内部的实现细节,这是封装的好处之一。封装的另一好处是可以给类增加新的方法,而类的实例只需要直接调用方法即可。

总而言之:

  • 类是创建实例的模板,而实例则是一个一个具体的对象,各个实例拥有的数据都互相独立,互不影响;

  • 方法就是与实例绑定的函数,和普通函数不同,方法可以直接访问实例的数据;

  • 通过在实例上调用方法,我们就直接操作了对象内部的数据,但无需知道方法内部的实现细节;

Python 允许对实例变量绑定任何数据,也就是说,对于两个实例变量,虽然它们都是同一个类的不同实例,但拥有的变量名称都可能不同。

# 访问限制

在 Class 内部,可以有属性和方法,而外部代码可以通过直接调用实例变量的方法来操作数据,这样,就隐藏了内部的复杂逻辑。但是,外部仍旧可以自由访问和修改一个实例内部的属性,如果不希望内部属性被外部访问,可以使用 __name 这种方式对属性进行命名。该变量就会变为一个私有变量,这样,这个变量就只有内部可以访问,而外部是无法访问的。

class Student:
    def __init__(self, name):
        self.__name = name
    def get_name(self):
        return self.__name
    def set_name(self, name):
        self.__name = name
zhang = Student('zhang')
print(zhang._Student__name)  # zhang
print(zhang.__name)  # AttributeError: 'Student' object has no attribute '__name'
zhang.set_name('zhou')
print(zhang.get_name())  # zhou

其实在上文已经多次提到,以 __xxx 方式命名变量,并不能保证变量不被外部直接访问,例如在上面的代码清单中,其实通过 .Student__name 依然能够得到私有变量的值,而之所以说通过 __xxx 方式可以声明为私有变量,其实仅仅是出于约定俗成的规定,而非出于程序的强制要求,往往更多的是需要开发者拥有主动意识,自觉遵守这些约定或规范。

注意: __xxx__ 命名方式,表示这个变量是一个特殊变量,它不是私有变量,可以被直接访问,所以在属性命名时,不应当以这种方式命名。另外,以 _xxx 这种方式命名的变量,是可以被外部访问的,但作为一种约定俗成的规定,我们应当尽量将它当做私有变量看待。

# 继承和多态

前面提到封装是面向对象编程的一个重要特点,而继承和多态则是面向对象编程的另外两大特点。

当我们定义一个类的时候,我们定义的这个类可以从某个类继承,因此新创建的类相对被继承的类而言就是子类,而被继承的类就是基类、父类或超类。

class Animal:
    def run(self, ani):
        print('%s can run' % ani)
class Dog(Animal):
    def __init__(self, type_name):
        self.type_name = type_name
    def get_type_name(self):
        return self.type_name
    # 子类可以重写父类的方法
    def run(self, type):
        print('%s can run every!' % type)
class Cat(Animal):
    def __init__(self):
        pass
catA = Cat()
catA.run('Cat')  # 子类可以调用父类的方法
# Cat can run
dogA = Dog('Dog')
dogA.run(dogA.get_type_name())
# Dog can run every!

继承的好处在于不用进行任何声明就可以获得父类的全部功能,而且子类仍然可以对自身进行扩展和增强而不受父类限制,例如新增属性和方法。

当子类和父类存在相同的方法时,我们便说子类覆盖了父类的方法。当实例调用该同名方法时,就只会调用子类的方法,而不会调用父类,这种特性在面向对象的思想里被称为多态。

在 Python 基础中我们提到过,可以使用 isinstance() 函数来判断某个变量是否是某种类型。而在这里的实例中,我们同样可以通过这种方法来对拥有继承关系的子类对象和父类进行判断:

print(isinstance(catA, Cat))  # True
print(isinstance(dogA, Dog))  # True
print(isinstance(catA, Animal))  # True
print(isinstance(dogA, Animal))  # True
animal = Animal()
print(isinstance(animal, Dog))  # False

静态语言与动态语言:

对于静态语言来说(例如 Java),如果需要传入 Animal 类型,那么就必须传入 Animal 类型或它的子类,否则无法调用其 run() 方法。

而对于 Python 这样的动态语言来说,则不一定非要传入 Animal 类型或其子类,我们只需要保证传入的对象有一个 run() 方法即可。

Python 具有这种 file-like-object 的特点,它并不严格要求继承体系,这就是所谓的 “鸭子类型”。

鸭子类型:一个对象只要 “看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。

# 获取对象信息

当我们获得一个对象的引用时,我们可以通过 type() 函数来判断该对象的类型,当然,如果一个变量指向函数或类,也可以使用 type() 函数进行判断:

print(type(dogA))  # <class '__main__.Dog'>
print(type(dogA) == Dog)  # True
print(type(dogA) == type(catA))  # False

type() 函数返回的是一个 Class 类型,除了自定义的类型之外,也可以判断内置的类型:

import types
def fn():
    pass
print(type(fn) == types.FunctionType)  # True
print(type(lambda x: x) == types.LambdaType)  # True
print(type((x for x in range(10)))==types.GeneratorType)  # True

type 函数可以判断一个对象的类型,但对于拥有继承关系的对象而言,就无法判断它的父级类型,对于这种情况,我们仍旧可以使用 isinstance() 函数进行判断,它可以判断对象是否处于某一继承链上。

print(isinstance(catA, Animal))  # True
print(isinstance(dogA, Animal))  # True
print(isinstance(dogA, Cat))  # False
print(isinstance(dogA, (Cat, Dog)))  # True # 判断是否是多个对象中的一种

如果要获得一个对象的所有属性和方法,可以使用 dir() 函数,它返回一个包含字符串的 list ;如果想要获取对象长度,可以使用 __len__() 方法或 len() 函数,他们是等价的,并且,我们可以重写类中的 __len__() 方法。

class Cat(Animal):
    def __init__(self):
        pass
    
    def __len__(self):
        return 20
catA = Cat()
print(dir(catA))  # ['__class__', '__delattr__', '__dict__', '__dir__' ...
print(len(catA))  # 20

另外,对象还提供了 getattr()setattr()hasattr() 函数,可用于操作类中的属性。

# print(getattr(dogA, 'color'))  # AttributeError: 'Dog' object has no attribute 'color'
print(getattr(dogA, 'color', None))  # None # 获取属性时,可以传入一个默认值,当属性不存在时生效
print(hasattr(dogA, 'color'))  # False
setattr(dogA, 'color', 'brown')
print(hasattr(dogA, 'color'))  # True
print(getattr(dogA, 'color'))  # brown
print(getattr(dogA, 'run'))  # <bound method Dog.run of <__main__.Dog object at 0x000001BB085C2FA0>> # 判断是否有某方法

这些用于获取属性的方法或函数,通常是在不知道某一个类是否具有该方法时才使用的。

# 实例属性和类属性

对于一个类的实例,我们可以通过实例变量直接声明或使用 self 参数的方式声明以为其绑定属性。但对一个类本身,我们也可以通过如下方式为这个类声明属性:

class Student:
    name = 'Student'
    def __init__(self):
        pass
student = Student()
print(student.name)  # Student
print(Student.name)  # Student
student.name = 'Teacher'
print(student.name)  # Teacher
del student.name
print(student.name)  # Student

这个为类声明的属性虽然为类所有,但这个类的所有实例均可以访问该属性。如果类属性与实例属性同名,调用属性时默认从实例属性中获取,如实例属性未获取到,则从类属性中获取。因此,在编程时,我们应当尽量避免出现实例属性与类属性同名的情况。

# 面向对象高级编程

# _slots_

对于一个已声明的对象的实例,我们可以为其绑定任何属性和方法,但是为某一实例绑定的属性或方法,仅对当前实例有效,如果需要为所有实例都绑定属性或方法,则可以直接将属性或方法绑定到类上。给类绑定实例或方法后,这个类的所有实例均可调用。

class Student:
    def __init__(self):
        pass
student1 = Student()
student1.name = 'Zhang'
print(student1.name)  # Zhang
student2 = Student()
# print(student2.name)  # AttributeError: 'Student' object has no attribute 'name'
Student.age = 17
print(student1.age)  # 17
print(student2.age)  # 17

但是,为示例绑定属性或方法并不受任何限制,如果需要限定实例需要绑定的属性或方法,可以通过 __slots__ 来指定:

class Student:
    __slots__ = ('name', 'study')
    def __init__(self):
        pass
def study(name):
    print('%s is studying!' % name)
student1 = Student()
student1.name = 'Zhang'
student1.study = study
student1.study(student1.name)  # Zhang is studying!
student1.age = 17  # AttributeError: 'Student' object has no attribute 'age'

__slots__ 使用 tuple 来指定多个属性名,实例在绑定属性或方法时,所绑定的属性或方法的名称必须在 __slot__ 属性中存在,否则就会抛出异常。

注意: __slots__ 中定义的属性仅仅对当前类的实例起作用,对继承的子类是不起作用的,除非在子类中也进行声明,这样,子类和父类的 __slots__ 限制会进行叠加。

class Person:
    __slots__ = ('name', 'age')
class Student(Person):
    __slots__ = ('grade', 'score')
student1 = Student()
student1.name = 'Zhang'
print(student1.name)  # Zhang
student1.character = 'forthright'  # AttributeError: 'Student' object has no attribute 'character'

# @property

对于一个类的属性,声明的实例直接调用属性是不安全而且无法进行任何限制的,如果需要对属性进行限制,我们可以使用 Python 内置的 @property 装饰器来将一个方法变为属性调用。

class Student(object):
    @property  # getter
    def score(self):
        return self._score
    @score.setter  # setter
    def score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value
student1 = Student()
# student1.score = 'C'  # ValueError: score must be an integer!
# student1.score = 120  # ValueError: score must between 0 ~ 100!
student1.score = 60
print(student1.score)  # 60

如果需要定义一个只读属性,那么,我们只需要不对 setter 进行定义即可。

但需要注意一点,属性的方法名不应当与实例变量重名,这样会导致无线递归,最终赵成栈溢出,错误示例:

class Student(object):
    @property
    def score(self):  # 属性方法名为 score
        return self.score  # 实例变量名也为 score

# 多重继承

在面向对象的思想中,一个类是可以同时继承多个类的,但在设计类的继承关系时,通常,主线都是单一继承下来的,如果需要 “混入” 额外的功能,通过多重继承就可以实现,这种设计通常称之为 MixIn

class Dog(Mammal, RunnableMixIn, CarnivorousMixIn):
    pass

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

Python 自带的很多库也使用了 MixIn。例如 Python 自带了 TCPServerUDPServer 这两类网络服务,而要同时服务多个用户就必须使用多进程或多线程模型,这两种模型由 ForkingMixInThreadingMixIn 提供,于是,我们可以通过不同的组合来创建合适的服务。

# 多进程模式的 TCP 服务
class MyTCPServer(TCPServer, ForkingMixIn):
    pass
# 多线程模式的 UDP 服务
class MyUDPServer(UDPServer, ThreadingMixIn):
    pass

通过这种 MixIn 方式,我们不需要复杂而庞大的继承链,只要选择组合不同的类的功能,就可以快速构造出所需的子类。

只允许单一继承的语言(如 Java)不能使用 MixIn 的设计。

# 定制类

# _str_

当我们在打印一个类的实例时,通常打印的都是都是这个实例的内存地址信息,如需要有意义的自定义实例信息,可以使用 __str__() 方法:

class Student:
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return 'class Student(name: %s)' % self.name
class Teacher:
    pass
teacher1 = Teacher()
print(teacher1)  # <__main__.Teacher object at 0x000001D2DAC21FD0>
student1 = Student('Zhang')
print(student1)  # class Student(name: Zhang)
# _iter_

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

# 斐波那契数列
class Fib:
    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 > 1000:  # 退出循环的条件
            raise StopIteration()
        return self.a  # 返回下一个值
fib = Fib()
tu = []
for x in fib:
    tu.append(x)
print(tu)  # [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987]
# _getitem_

在上面的斐波那契数列代码清单中,声明的实例对象虽然和 list 很像,但它还不能通过下标获取对应的元素,要实现这一点,可以通过加入 __getitem__() 方法来实现:

def __getitem__(self, n):
    a, b = 1, 1
    for x in range(n):
        a, b = b, a + b
    return a
print(Fib()[4])  # 5

而如果要支持切片方式,则需要更加完善的逻辑:

def __getitem__(self, n):
    if isinstance(n, int): # n 是索引
        a, b = 1, 1
        for x in range(n):
            a, b = b, a + b
        return a
    if isinstance(n, slice): # n 是切片
        start = n.start
        stop = n.stop
        if start is None:
            start = 0
        a, b = 1, 1
        L = []
        for x in range(stop):
            if x >= start:
                L.append(a)
            a, b = b, a + b
        return L
print(fib[5:10])  # [8, 13, 21, 34, 55]
# _getattr_

当一个实例调用不存在的属性或方法时,就会抛出异常,我们可以定义一个 __getattr__() 方法来动态地返回属性或函数。

class Student(object):
    def __init__(self):
        self.name = 'Michael'
    def __getattr__(self, attr):
        if attr == 'score':
            return 60
        if attr == 'age':
            return lambda: 18
        raise AttributeError('\'Student\' object has no attribute \'%s\'' % attr)
student1 = Student()
print(student1.score)  # 60
print(student1.age())  # 18
print(student1.test)  # None

注意:只有在没有找到属性的情况下,才会调用 __getattr__() ,并且,当调用一个不存在的方法或属性时, __getattr__() 方法会默认返回为 None ,我们也可以做自定义处理,例如抛出自定义异常。

实现一个动态路由链:

class Chain(object):
    def __init__(self, path=''):
        self._path = path
    def __getattr__(self, path):
        return Chain('%s/%s' % (self._path, path))
    def __call__(self, path):
       return Chain('%s/%s' % (self._path, path))
    def __str__(self):
        return self._path
url = Chain().user('zhang').status.timeline.list
print(url)  # /user/zhang/status/timeline/list
# _call_

在动态路由链的代码清单中,我们使用到了一个 __call__() 方法,这个方法可以直接在实例本身的基础上调用方法。

class Person(object):
    def __init__(self, name):
        self.name = name
    def __call__(self, str):
        print('Mr. %s is %s' % (self.name, str))
Person('Zhang')('working!')  # Mr. Zhang is working!
person1 = Person('Wang')
print(callable(person1))  # True # 可以使用 callable () 函数判断是否是可调用对象
person1('studying!')  # Mr. Wang is studying!

Python 的 class 允许定义许多定制方法,可以让我们非常方便地生成特定的类,还有很多可定制的方法,详情可参考官方文档

# 枚举类

Python 枚举类的声明及使用方式如下:

from enum import Enum
Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))
print(Month.May)  # Month.May
print(Month.May.name)  # May
print(Month.May.value)  # 5 # 默认从 1 开始计数
for name, member in Month.__members__.items():
    print(name, '=>', member, ',', member.value)

如果需要更精确地控制枚举类型,可以从 Enum 派生出自定义类:

from enum import Enum, unique
@unique  # 用于检查重复
class Weekday(Enum):
    Sun = 0  # 自定义值
    Mon = 1
    Tue = 2
    Wed = 3
    Thu = 4
    Fri = 5
    Sat = 6
print(Weekday.Thu)  # Weekday.Thu
print(Weekday.Thu.name)  # Thu
print(Weekday.Thu.value)  # 4
print(Weekday(3))  # Weekday.Wed # 也可以通过 value 来获取对应的枚举
for name, member in Weekday.__members__.items():
    print(name, '=>', member, ',', member.value)

# 元类

动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。

通过 type() 函数可以查看一个类型或变量的类型,同时, type() 函数既可以返回一个对象的类型,又可以创建出新的类型,而无需通过 class Hello(object)... 这样的方式定义。

def fn(self, name='world'):
    print('Hello, %s!' % name)
Hello = type('Hello', (object,), dict(hello=fn))
h = Hello()
h.hello()
# Hello, world!

通过 type 函数创建类需要接收三个参数:

  1. class 类名。

  2. 继承的父类集合。

  3. class 的方法名称与函数绑定。

通过 type 函数创建的类和直接创建的类没有任何区别,因为通过 class 定义的类在解释器执行时,实际上也是通过调用 type 函数来进行定义的。正常情况下,我们通过 class 来创建类即可,但 type 函数允许我们动态地将类创建出来。也就是说,动态语言本身支持运行期动态创建类。

除了使用 type 函数动态创建类以外,要控制类的创建行为,还可以使用 metaclass (元类),元类可以对类进行创建和修改等操作。

# 定义一个元类
class ListMetaclass(type):
    def __new__(cls, name, bases, attrs):
        attrs['add'] = lambda self, value: self.append(value)
        return type.__new__(cls, name, bases, attrs)
# 使用元类来创建类
class MyList(list, metaclass=ListMetaclass):
    pass
l = MyList()
l.add(1)
l.add(9)
print(l)  # [1, 9]

通过这种方式创建类,当程序执行到 metaclass=Xxx 时,就会调用对应元类的 __new__() 方法来创建类,在该函数中,通过 type.__new__() 方法创建类,并依次传入以下参数:

  1. 当前准备创建的类的对象。

  2. 类名。

  3. 所继承的父类集合。

  4. 类所有方法的集合。

这种动态创建类的方式,一个较为常见的引用常见即是 ORM 框架。

❗TODO 待完善

# 错误、调试和测试

# 错误处理

在程序运行的过程中,如果发生了错误,可以事先约定返回一个错误代码,这样,就可以知道是否有错,以及出错的原因。但是,返回错误码的方式常常容易与业务数据混合在一起,造成调用者必须用大量的代码来判断是否出错,因此,Python 也提供了一套错误处理机制。

异常捕获:

当我们认为某些代码可能出现异常时,我们可以通过 try 语句块来运行这段代码,如果出错,则不会继续,而是直接跳转错误代码处理,即 except 语句块中,执行完异常处理之后,如果有 finally 语句块,则执行该语句块。

try:
    a = 1 / 0
    print(a)  # 不会执行
except ValueError as e:
    print('ValueError:', e)
except ZeroDivisionError as e:  # 错误也可以同时有多个类型
    print('ZeroDivisionError:', e)
else:  # 指定未出现异常时的处理
    print('No error!')
finally:
    print('finally')
print('end')

Python 的错误其实也是 class,所有的错误类型都继承自 BaseException ,所以在使用 except 时需要注意的是,它不但捕获该类型的错误,还把其子类也一并捕获,这时需要注意捕获的层级,先捕获层级较低的异常,后捕获层级较高的异常。

Python 所有的错误都是从 BaseException 类派生的,常见的错误类型和继承关系可以通过官方文档进行查看。

使用 try...expect 也可以跨越多层调用处理异常,而不需要在每一个调用层都重复进行异常处理:

def foo(s):
    return 10 / int(s)
def bar(s):
    return foo(s) * 2
def main():
    try:
        bar('0')
    except Exception as e:
        print('Error:', e)
    finally:
        print('finally...')
main()

如果没有对错误进行处理,错误就会一直向上一级抛出,直到最后被 Python 解释器捕获并打印出错误信息,然后退出程序。

Python 内置的 logging 模块可以非常容易地记录错误信息:

import logging
def foo(s):
    return 10 / int(s)
def bar(s):
    return foo(s) * 2
def main():
    try:
        bar('0')
    except Exception as e:
        logging.exception(e)
main()
print('END')

这样就可以在抛出异常之后,程序仍旧可以按照预期的方式执行,而不会因为某一个异常而导致整个程序无法运行。并且,通过配置, logging 还可以把错误记录到日志文件里,方便事后排查。

因为错误是 class,捕获一个错误就是捕获到该 class 的一个实例。因此,错误并不是凭空产生的,而是有意创建并抛出的。Python 的内置函数会抛出很多类型的错误,但是,我们也可以抛出一个自定义的错误类型。

如果要抛出错误,首先根据需要,可以定义一个错误的 class,选择好继承关系,然后,用 raise 语句抛出一个错误的实例:

class FooError(ValueError):
    pass
def foo(s):
    n = int(s)
    if n==0:
        raise FooError('Invalid value: %s' % s)
    return 10 / n
foo('0')

在捕获异常时,我们也可以将异常捕获后继续抛出:

def foo(s):
    n = int(s)
    if n==0:
        raise ValueError('invalid value: %s' % s)
    return 10 / n
def bar():
    try:
        foo('0')
    except ValueError as e:
        print('ValueError!')
        raise  # raise 语句如果不带任何参数,则会将错误原样抛出
bar()

此外,还可以在捕获异常之后,将异常进行转换并再次抛出,但是,这种异常转换操作,应当考虑到逻辑的合理性。

# 调试

在编码或程序运行过程中,难免会出现很多错误,一个运行良好的程序,通常都需要经过一定的调试。在调试过程中,我们可以通过 print() 的方式将一些对象值输出到控制台查看,但这种方式并不推荐,因为对程序而言, print() 语句本身并无任何作用。因此,在通常的调试过程中,我们可以使用断言来辅助进行调试,凡是可以通过 print 来辅助查看的地方,都可以使用 assert 进行断言。

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

当断言语句判断为 False 时,就会抛出一个 AssertionError 错误。

如果在 Python 交互环境中执行程序,可以指定是否开启断言调试,默认开启,也可以通过如下方式关闭:

$ python -O err.py

除了使用断言,我们也可以使用 logging 的方式来记录错误信息:

import logging
logging.basicConfig(level=logging.INFO)  # 指定日志级别
s = '0'
n = int(s)
logging.info('n = %d' % n)
print(10 / n)

logging 允许指定记录信息的级别,主要有 debuginfowarningerror 等几个级别。

Python 日志级别:

级别日志函数描述
DEBUGlogging.debug()最低级别,追踪问题时使用。
INFOlogging.info()记录程序中一般事件的信息,或确认一切工作正常。
WARNINGlogging.warning()记录信息,用于警告。
ERRORlogging.error()用于记录程序报错信息。
CRITICALlogging.critical()最高级别,记录可能导致程序崩溃的错误。

另外,还可以使用 Python 的 pdb 调试器,让程序以单步方式运行,可以随时查看运行状态。

❗TODO 待完善

但一些功能较为强大的 IDE 工具,一般都包含有十分便捷的调试工具,使用这些 IDE 提供的调试工具,通常是最方便的。

# 单元测试

单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。

为了编写单元测试,我们需要引入 Python 自带的 unittest 模块,编写单元测试时使用的测试类,通常都是从 unittest.TestCase 继承而来,类中的方法以 test 开头,就标识为测试方法,反之,不以 test 开头的方法就不会被标识为测试方法,测试的时候也就不会被执行。

对每一类测试都需要编写一个 test_xxx() 方法。由于 unittest.TestCase 提供了很多内置的条件判断,我们只需要调用这些方法就可以断言输出是否是我们所期望的。最常用的断言就是 assertEqual()

self.assertEqual(abs(-1), 1)  # 断言返回结果与预期结果是否相等
with self.assertRaises(KeyError):  # 断言抛出预期类型的 Error
    value = d['empty']
with self.assertRaises(AttributeError):
    value = d.empty

要运行一个单元测试,只需要在当前 .py 文件末尾加上如下代码即可:

if __name__ == '__main__':
    unittest.main()

这样就可以将测试代码当做正常的 Python 脚本执行。

但如果在 Python 交互环境中,我们也可以加入参数 -m unittest 来运行单元测试:

$ python -m unittest main

如果要对每个单元测试方法的调用时机进行监控,可以使用 setUp()tearDown() 这两个特殊的方法来实现。这两个方法会分别在每一个测试方法调用前后执行。

def setUp(self):
    print('test start...')
def tearDown(self):
    print('test end...')

# 文档测试

Python 内置的文档测试(doctest)模块可以直接提取注释中的代码并执行测试。

❗TODO 待完善

# IO 编程

IO 在计算机中通常是指 Input/Output,也就是输入和输出。

在 IO 编程中,Stream(流)是一个很重要的概念。

操作 IO 的能力都是由操作系统提供的,每一种编程语言都会把操作系统提供的低级 C 接口封装起来方便使用,Python 也不例外。

一般来说,异步 IO 的复杂度远远高于同步 IO。

# 文件的读写

读写文件是最常见的 IO 操作。Python 内置了读写文件的函数,用法和 C 是兼容的。

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

要以读文件的模式打开一个文件对象,需要使用 Python 内置的 open() 函数,并传入文件名和标示符:

f = None
try:
    f = open('C:\\Users\\Administrator\\Desktop\\test.txt', 'r')
    print(f.buffer.read())
except FileNotFoundError as e:
    print(e)
finally:
    f.close()

其中,参数 r 表示这是一个读操作,如果文件不存在,就会抛出一个 IOError 错误,并告知文件不存在;调用 read() 方法可以读取文件中的内容,并返回字符串;而当文件读取完毕后,需要调用 close() 方法关闭文件。

文件使用完后必须关闭,因为文件对象会占用操作系统的资源,并且操作系统同一时间可打开的文件数量是有限的, close() 方法应当放在 finally 语句块中,保证其一定会执行。

我们可以在 finally 语句块中关闭文件,但有时候会显得比较麻烦,Python 为此也引入了一个 with 语句来帮助自动调用关闭文件的方法。

with open('C:\\Users\\Administrator\\Desktop\\test.txt', 'r') as f:
    buf = f.buffer.read()
    print(buf)

由于 read 方法默认会读取文件的全部内容,但如果文件本身较大,则应当通过 read(size) 来限定读取文件的大小,如果需要逐行读取,可以使用 readlines() 方法,这个方法的返回值时一个字符串数组。

如果需要读取一个二进制文件,比如图片、视频等,打开文件时就需要指定参数为 rb

# 打开图片文件
with open('C:\\Users\\Administrator\\Desktop\\img\\avatar.jpg', 'rb') as f:
    buf = f.read()
    print(buf)  # 输出十六进制的字节

如果读取时候的编码不对,就很有可能会导致乱码,因此,在打开文件时也可以指定读取文件所使用的编码。

with open('C:\\Users\\Administrator\\Desktop\\test.txt', 'r', encoding='utf-8', errors='ignore') as f:
    buf = f.buffer.readlines()
    print(buf)

同时,当读取文件中出现非法编码内容,可以通过指定 errors 参数进行处理。

写文件与读文件基本一样,只是在打开文件时指定参数为 wwb 即可:

with open('C:\\Users\\Administrator\\Desktop\\test.txt', 'w', encoding='gbk', errors='ignore') as f:
    f.write('How are you?')

可以反复调用 write() 来写入文件,但是写入完毕后务必要关闭文件。当我们写文件时,操作系统往往不会立刻把数据写入磁盘,而是放到内存缓存起来,空闲的时候再慢慢写入。只有调用关闭方法时,操作系统才保证把没有写入的数据全部写入磁盘。忘记调用关闭方法的后果是数据可能只写了一部分到磁盘,剩下的丢失了。所以,在读写文件时最后都使用 with 语句。

使用 a 参数,可以设置为追加模式,在原文件内容的基础上,追加新的内容。

with open('C:\\Users\\Administrator\\Desktop\\test.txt', 'a', encoding='gbk', errors='ignore') as f:
    f.write('\rI\'m fine, thank you!')

# StringIO 与 BytesIO

很多会后数据读写并不一定是操作文件,也有可能是在内存中进行读写, StringIO 就是用来读写内存中的字符串。

from io import StringIO
f1 = StringIO()
f1.write('Hello')
f1.write(' ')
f1.write('Mike!')
print(f1.getvalue())  # Hello Mike! # 获取写入后的字符串内容
f2 = StringIO('Hello Jack!\nHow are you?\nI\'m fine, thanks!')  # 也可以使用字符串初始化 StringIO
while True:
    s = f2.readline()
    if s == '':
        break
    print(s.strip())

同理,我们也可以使用 BytesIO 来操作内存中的字节数组。

from io import BytesIO
f1 = BytesIO()
f1.write('中文'.encode('utf-8'))  # 写入 bytes
print(f1.getvalue())  # b'\xe4\xb8\xad\xe6\x96\x87'
f2 = BytesIO(b'\xe4\xb8\xad\xe6\x96\x87')
print(f2.read())

# 操作文件和目录

使用 Python 提供的 os 模块,可以操作系统的文件和目录。

import os
print(os.name)  # nt 则表示 windows,posix 则表示 Linux,Unix 或 Mac Os X
print(os.uname())  # 获取更加详细的操作系统信息(不支持 windows)
print(os.environ)  # 查看操作系统中定义的环境变量
print(os.environ.get('JAVA_HOME', 'Invalid key!'))  # 获取某个具体的环境变量

操作文件和目录的函数一部分放在 os 模块中,一部分放在 os.path 模块中。

import os
print(os.path.abspath('.'))  # 查看当前文件的绝对路径
path = os.path.join('A:\\python', 'workspace')  # 目录路径
print(path)  # A:\python\workspace
print(os.path.split(path))  # ('A:\\', 'python')
print(os.path.split('/usr/chinmoku/test/file.txt'))  # ('/usr/chinmoku/test', 'file.txt')
print(os.path.splitext('/usr/chinmoku/test/file.txt'))  # ('/usr/chinmoku/test/file', '.txt') # 可以直接得到文件的扩展名
os.mkdir(path)  # 在指定位置创建目录
os.rmdir(path)  # 删除指定位置的目录

把两个路径合成一个时,不要直接拼字符串,而要通过 os.path.join() 函数,这样可以正确处理不同操作系统的路径分隔符。同理,拆分路径时,也应当通过 os.path.split() 函数进行拆分。

另外还有一些其他常用的文件操作:

import os
os.rename('test.txt', 'test.py')  # 文件重命名
os.remove('test.py')  # 删除文件
print([x for x in os.listdir('.') if os.path.isdir(x)])  # 过滤文件,列出当前目录下的所有目录
print([x for x in os.listdir('.') if os.path.isfile(x) and os.path.splitext(x)[1]=='.py'])  # 列出当前目录下的所有 .py 文件

# 序列化

在程序运行的过程中,所有的变量都是在内存中,可以随时修改,但是一旦程序结束,变量所占用的内存就被操作系统全部回收。如果在此期间没有将修改后的值存储到磁盘上,下次重新运行程序,变量又会被重新初始化。我们把变量从内存中变成可存储或传输的过程称之为序列化,在 Python 中叫 pickling ,在其他语言中也被称之为 serialization,marshalling,flattening 等等。

序列化之后,就可以把序列化后的内容写入磁盘,或者通过网络传输到别的机器上。反过来,把变量内容从序列化的对象重新读到内存里称之为反序列化,即 unpickling 。Python 提供了 pickle 模块来实现序列化。

import pickle
d = dict(name='Bob', age=20, score=88)
buf = pickle.dumps(d)  # 将任意对象转换为 bytes
print(buf)
with open('C:\\Users\\Administrator\\Desktop\\test.txt', 'wb') as f:
    # f.write (buf) # 向文件写入 bytes
    pickle.dump(d, f)  # 序列化并写入文件

读取被序列化的文件:

import pickle
with open('C:\\Users\\Administrator\\Desktop\\test.txt', 'rb') as f:
    res = pickle.load(f)
    print(res)  # {'name': 'Bob', 'age': 20, 'score': 88}

Python 也提供了对 JSON 数据格式的支持,我们可以很容易地将一个 Python 对象转换为 JSON 对象。

import json
d = dict(name='Bob', age=20, score=88)
s = json.dumps(d)  # 将 Python 类型序列化为 JSON 字符串
print(s)  # {"name": "Bob", "age": 20, "score": 88}
print(isinstance(s, dict))  # False
print(isinstance(s, str))  # True

同样,我们也可以将 JSON 字符串反序列化为 Python 对象。

import json
obj = json.loads('{"name": "Bob", "age": 20, "score": 88}')
print(obj)
print(isinstance(obj, str))  # False
print(isinstance(obj, dict))  # True

但对于一个 Python 类进行序列化操作则有所不同:

import json
class Student(object):
    def __init__(self, name, age, score):
        self.name = name
        self.age = age
        self.score = score
def student2dict(std):
    return {
        'name': std.name,
        'age': std.age,
        'score': std.score
    }
s = Student('Bob', 20, 88)
# print(json.dumps(s))  # TypeError: Object of type Student is not JSON serializable
print(json.dumps(s, default=student2dict))  # {"name": "Bob", "age": 20, "score": 88}
# 方法二:通常类的实例都有一个 __dict__ 属性,可以将实例转换为 dict,用来存储实例变量
print(json.dumps(s, default=lambda obj: obj.__dict__))  # {"name": "Bob", "age": 20, "score": 88}

并不是所有类的实例都有 __dict__ 属性,也有少数例外,例如定义了 __slots__ 的类。

将 JSON 字符串反序列化为 Python 类:

import json
class Student(object):
    def __init__(self, name, age, score):
        self.name = name
        self.age = age
        self.score = score
def dict2student(d):
    return Student(d['name'], d['age'], d['score'])
json_str = '{"name": "Bob", "age": 20, "score": 88}'
stu = json.loads(json_str, object_hook=dict2student)
print(isinstance(stu, str))  # False
print(isinstance(stu, Student))  # True

# 进程和线程

# 多进程

Unix/Linux 操作系统提供了一个 fork() 系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是 fork() 调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。

子进程永远返回 0,而父进程返回子进程的 ID。这样做的理由是,一个父进程可以 fork 出很多子进程,所以,父进程要记下每个子进程的 ID,而子进程只需要调用 getppid() 就可以拿到父进程的 ID。

import os
print('Process (%s) start...' % os.getpid())
pid = os.fork()  # Only works on Unix/Linux/Mac
if pid == 0:
    print('I am child process (%s) and my parent is %s.' % (os.getpid(), os.getppid()))
else:
    print('I (%s) just created a child process (%s).' % (os.getpid(), pid))
# Process (876) start...
# I (876) just created a child process (877).
# I am child process (877) and my parent is 876.

Python 在 Windows 环境中是无法调用 fork 函数的,但是这并不意味着 Python 无法在 Windows 环境下编写多进程服务,因为它提供了一个跨平台版本的多进程模块 multiprocessing ,这个模块提供了一个 Process 类来代表一个进程对象。

from multiprocessing import Process
import os
# 子进程要执行的代码
def run_proc(name):
    print('Run child process %s (%s)...' % (name, os.getpid()))
if __name__ == '__main__':
    print('Parent process %s.' % os.getpid())
    p = Process(target=run_proc, args=('test',))
    print('Child process will start.')
    p.start()
    p.join()  # join () 方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。
    print('Child process end.')
# Parent process 11284.
# Child process will start.
# Run child process test (6740)...
# Child process end.

创建子进程时,只需要传入一个执行函数和函数的参数,并创建一个 Process 实例,用 start() 方法启动即可。

但如果要启动大量的子进程,可以用进程池的方式批量创建子进程:

from multiprocessing import Pool
import os, time, random
def long_time_task(name):
    print('Run task-%s (%s)...' % (name, os.getpid()))
    start = time.time()
    time.sleep(random.random() * 3)
    end = time.time()
    print('Task-%s runs %0.2f seconds.' % (name, (end - start)))
if __name__ == '__main__':
    print('Parent process %s.' % os.getpid())
    p = Pool(4)  # 创建进程池并指定大小
    for i in range(5):
        p.apply_async(long_time_task, args=(i,))
    print('Waiting for all subprocesses done...')
    p.close()  # 调用 join () 之前必须先调用 close (),调用 close () 之后就不能继续添加新的 Process 了。
    p.join()
    print('All subprocesses done.')

Pool 默认大小是 CPU 的核数,但也可以传入参数指定进程数,如果实际运行的进程数超出进程池大小,超出的进程将等待进程池有空闲后才会执行。

很多时候,子进程并不是自身,而是一个外部进程。我们创建了子进程后,还需要控制子进程的输入和输出。 subprocess 模块可以让我们非常方便地启动一个子进程,然后控制其输入和输出。

import subprocess
print('$ nslookup www.python.org')
r = subprocess.call(['nslookup', 'www.baidu.com'])
print('Exit code:', r)  # Exit code: 0
# $ nslookup www.python.org
# Server:		192.168.1.1
# Address:	192.168.1.1
# 
# canonical name = dualstack.python.map.fastly.net
# Name:	python.map.fastly.net
# Addresses:  2a04:4e42:1a::223
# 	  151.101.228.223
# Aliases:  www.python.org
# 
# Exit code: 0

如果子进程需要输入,可以使用 communicate() 方法实现:

import subprocess
print('$ nslookup')
p = subprocess.Popen(['nslookup'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = p.communicate(b'set q=mx\npython.org\nexit\n')
print(output.decode('gbk'))
print('Exit code:', p.returncode)
# $ nslookup
# 默认服务器:  192.168.1.1
# Address:  192.168.1.1
# 
# > > 服务器:  192.168.1.1
# Address:  192.168.1.1
# 
# python.org   MX preference = 50, mail exchanger = mail.python.org
# > 
# Exit code: 0

这里的代码相当于进行了如下操作:

Python子进程输入操作

通过上面的操作,我们已经能够自如地创建进程,但进程之间是需要进行通信的,操作系统提供了很多机制来实现进程间的通信。Python 的 multiprocessing 模块包装了底层的机制,提供了 QueuePipes 等多种方式来交换数据。

from multiprocessing import Process, Queue
import os, time, random
# 写数据进程执行的代码:
def write(q):
    print('Process to write: %s' % os.getpid())
    for value in ['A', 'B', 'C']:
        print('Put %s to queue...' % value)
        q.put(value)
        time.sleep(random.random())
# 读数据进程执行的代码:
def read(q):
    print('Process to read: %s' % os.getpid())
    while True:
        value = q.get(True)
        print('Get %s from queue.' % value)
if __name__ == '__main__':
    # 父进程创建 Queue,并传给各个子进程:
    q = Queue()
    pw = Process(target=write, args=(q,))
    pr = Process(target=read, args=(q,))
    # 启动子进程 pw,写入:
    pw.start()
    # 启动子进程 pr,读取:
    pr.start()
    # 等待 pw 结束:
    pw.join()
    # pr 进程里是死循环,无法等待其结束,只能强行终止:
    pr.terminate()
# Process to write: 15180
# Put A to queue...
# Process to read: 18192
# Get A from queue.
# Put B to queue...
# Get B from queue.
# Put C to queue...
# Get C from queue.

在 Unix/Linux 下, multiprocessing 模块封装了 fork() 调用,使我们不需要关注 fork() 的细节。由于 Windows 不支持 fork 调用,因此, multiprocessing 需要 “模拟” 出 fork 的效果,父进程所有 Python 对象都必须通过 pickle 序列化再传到子进程去,所以,如果 multiprocessing 在 Windows 下调用失败了,要先考虑是不是 pickle 失败了。

# 多线程

Python 的线程是真正的 Posix Thread ,而不是模拟出来的线程。

Python 的标准库提供了两个模块: _threadthreading ,前者是低级模块,后者是高级模块,对前者进行了封装。

在 Python 中,启动一个线程就是把一个函数传入并创建 Thread 实例,然后调用 start() 开始执行:

import time, threading
# 新线程执行的代码:
def loop():
    print('thread %s is running...' % threading.current_thread().name)
    n = 0
    while n < 5:
        n = n + 1
        print('thread %s >>> %s' % (threading.current_thread().name, n))
        time.sleep(1)
    print('thread %s ended.' % threading.current_thread().name)
print('thread %s is running...[main]' % threading.current_thread().name)
t = threading.Thread(target=loop, name='LoopThread')
t.start()
t.join()
print('thread %s ended.' % threading.current_thread().name)

所有进程都会默认启动一个线程,我们称之为主线程,主线程又可以启动新的线程,Python threading 下的 current_thread() 函数会返回当前线程的实例。主线程实例名称默认为 MainThread ,子线程的名字可以在创建时指定。线程名称仅仅用于显示,没有实际意义,如果不指定子线程名称,Python 就自动给线程命名为 Thread-1Thread-2 ……

多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量。

在多线程环境中,线程之间的执行顺序并没有一定的规律,在不同的应用场景中,由于执行顺序的不同,极有可能产生与预期不符的执行结果。如果要确保执行结果的正确性,往往需要对共享操作进行加锁,这样就可以避免多个线程之间的修改冲突。在 Python 中,可以通过 threading.Lock() 来创建锁:

import threading
balance = 0
lock = threading.Lock()
def change_it(n):
    global balance
    balance = balance + n
    balance = balance - n
def run_thread(n):
    for i in range(100000):
        # 获取锁
        lock.acquire()
        try:
            change_it(n)
        finally:
            # 释放锁
            lock.release()
if __name__ == '__main__':
    t1 = threading.Thread(target=run_thread, args=(5,))
    t2 = threading.Thread(target=run_thread, args=(8,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(balance)

多个线程会对同一把锁产生竞争,竞争成功的线程则允许执行被锁定的操作,未竞争成功的线程则等待获得锁的线程释放锁。

关于锁的内容其实相当丰富,这里仅仅做最简要的说明。

在 Python 中可以使用多线程,但其实并不能有效理由计算机的多核。如果一定要通过多线程利用多核,那只能通过 C 扩展来实现。

不过,Python 虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个进程有各自独立的 GIL 锁,互不影响。

# ThreadLocal

在多线程环境下,相较于使用全局变量,一个线程使用自己的局部变量更加安全,因为局部变量只有自己可见,不会影响其他线程,而对全局变量的修改必须加锁进行。在多线程环境下,应当尽量减少使用全局变量。

但是,局部变量的使用也存在一些问题,例如函数在多层调用时,传递参数会相当麻烦,为了解决这一问题,Python 引入了 ThreadLocal 对象。

import threading
# 创建全局 ThreadLocal 对象:
local_school = threading.local()  # 不同线程存储在 ThreadLocal 对象中的属性互不影响
def process_student():
    # 获取当前线程关联的 student:
    std = local_school.student
    print('Hello, %s (in %s)' % (std, threading.current_thread().name))
def process_thread(name):
    # 绑定 ThreadLocal 的 student:
    local_school.student = name
    process_student()
if __name__ == '__main__':
    t1 = threading.Thread(target=process_thread, args=('Alice',), name='Thread-A')
    t2 = threading.Thread(target=process_thread, args=('Bob',), name='Thread-B')
    t1.start()
    t2.start()
    t1.join()
    t2.join()
# Hello, Alice (in Thread-A)
# Hello, Bob (in Thread-B)

可以将 ThreadLocal 类型的全局变量理解为一个 dict ,其内存根据不同线程,分别存放了不同线程下的对象信息,当再次从中获取值时,会自动判断当前是哪一个线程,并取出对应线程下的值。

ThreadLocal 最常用的地方就是为每个线程绑定一个数据库连接,HTTP 请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。

一个 ThreadLocal 变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰。

# 进程与线程对比

❗TODO 此部分概念与其他语言没有任何区别,暂时略去。

# 分布式线程

Python 的 multiprocessing 模块不但支持多进程,其中 managers 子模块还支持把多进程分布到多台机器上。一个服务进程可以作为调度者,依靠网络通信,可以将任务分布到其他多个进程中。由于 managers 模块封装很好,因此完全不必了解网络通信的细节,就可以很容易地编写分布式多进程程序。

在多进程章节中,我们已经能够让多个进程通过 Queue 在同一台主机上进行通讯。而如果需要在多个主机上实现进程之间的通讯,则需要 managers 模块将 Queue 通过网络暴露出去,其他主机的进程就可以访问这个 Queue 了。

创建并启动服务进程:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import random, time, queue
from multiprocessing.managers import BaseManager
# 发送任务队列
task_queue = queue.Queue()
# 接收结果队列
result_queue = queue.Queue()
# 继承 BaseManager
class QueueManager(BaseManager):
    pass
def return_task_queue():
    # global 用于函数内部,修改全局变量的值
    global task_queue
    return task_queue
def return_result_queue():
    global result_queue
    return result_queue
if __name__ == '__main__':
    # 将两个 Queue 注册到网络上,callable 参数关联 Queue 对象
    # !win10 中 callale 不对 lambda 匿名函数做处理
    QueueManager.register('get_task_queue', callable=return_task_queue)
    QueueManager.register('get_result_queue', callable=return_result_queue)
    # 绑定端口 5000,这 5000 怎么来的?两个文件中的端口一样就行!,设置验证码 abc
    # 通过 QueueManager 将 Queue 暴露出去
    manager = QueueManager(address=('127.0.0.1', 5000), authkey=b'abc')  # authkey,保证通讯不被其他程序恶意干扰
    manager.start()
    task = manager.get_task_queue()
    result = manager.get_result_queue()
    # 放 10 个任务进去
    for i in range(10):
        n = random.randint(0, 1000)
        print('Put task %d...' % n)
        # 将数据放到任务队列
        task.put(n)
    # 取任务执行结果
    print('Try get results...')
    for i in range(10):
        # 从结果队列中取结果
        # 等待 10 是因为计算需要时间
        r = result.get(timeout=10)
        print('Result: %s' % r)
    # 关闭
    manager.shutdown()
    print('master exit.')

创建并启动工作进程:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import time, sys, queue
from multiprocessing.managers import BaseManager
class QueueManager(BaseManager):
    pass
if __name__ == '__main__':
    QueueManager.register('get_task_queue')
    QueueManager.register('get_result_queue')
    server_addr = '127.0.0.1'
    print('Connect to server %s...' % server_addr)
    # 端口设置和 task_master.py 中一样
    m = QueueManager(address=(server_addr, 5000), authkey=b'abc')
    # 连接网络
    m.connect()
    task = m.get_task_queue()  # 如果不在同一台主机,必须通过 manager.get... 方式获得 Queue 接口
    result = m.get_result_queue()
    for i in range(10):
        try:
            # 接收任务队列中的数据
            n = task.get(timeout=1)
            print('Run task %d*%d' % (n, n))
            r = '%d*%d=%d' % (n, n, n * n)
            time.sleep(1)
            # 放进结果队列
            result.put(r)
        except queue.Queue.Empty:
            print('task queue is empty')
    print('work done')

依次启动服务进程和工作进程,得到如下结果:

image-20210909210504132

以上就是一个简单的 Master/Worker 模型。

Queue 其实是存储在服务进程中的,并使用 QueueManager 来进行管理,工作进程通过注册的队列名称查找匹配到指定的队列,并从队列中获得传输的值。

同时也需要注意,Queue 的作用是用来传递任务和接收结果,每个任务的描述数据量应当要尽量小。

# 正则表达式

Python 提供 re 模块,包含所有正则表达式的功能,但需要注意正则表达式在 Python 中的转义,推荐使用 r 前缀以便忽略转义。

import re
res1 = re.match(r'^\d{3}\-\d{3,8}$', '010-12345')
print(res1)  # <re.Match object; span=(0, 9), match='010-12345'>
res2 = re.match(r'^\d{3}\-\d{3,8}$', '010 12345')
print(res2)  # None
print('a b   c'.split(' '))  # ['a', 'b', '', '', 'c'] # 无法识别连续的空格
print(re.split(r'\s+', 'a b   c'))  # ['a', 'b', 'c']
phone = input('请输入手机号:')
if re.match(r'^(13[0-9]|14[5|7]|15[0|1|2|3|4|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$', phone):
    print('手机号验证通过!')
else:
    print('手机号格式错误!')

正则表达式能够帮助我们有效地对字符串进行格式验证,但对于使用场景的不同,有时候正则也并非是绝对的。

在 Python 中使用正则表达式时,解释器会进行如下操作:

  1. 编译正则表达式,如果正则表达式的字符串本身不合法,则会报错。

  2. 用编译后的正则表达式去匹配字符串。

# 常用内建模块

# datetime

datetime 是 Python 处理日期和时间的标准库。

from datetime import datetime, timedelta
now = datetime.now()
print(now)  # 2021-09-09 22:23:34.810674
print(type(now))  # <class 'datetime.datetime'>
dt = datetime(2021, 9, 9, 22, 30)  # 用指定日期时间创建 datetime
print(dt)  # 2021-09-09 22:30:00
print(dt.timestamp())  # 1631197800.0 # 日期转时间戳,单位:秒
t = 1631197800.0
print(datetime.fromtimestamp(t))  # 2021-09-09 22:30:00 # 时间戳转时间
print(datetime.utcfromtimestamp(t))  # 2021-09-09 14:30:00 # UTC 时间
# 时间格式转换
cday = datetime.strptime('2021-9-7 18:19:59', '%Y-%m-%d %H:%M:%S')  # 字符串转时间
print(cday)  # 2021-09-07 18:19:59
print(now.strftime('%a, %b %d %H:%M'))  # Thu, Sep 09 22:47 # 时间转字符串
# 日期加减
print(now)  # 2021-09-09 22:50:17.151719
print(now + timedelta(hours=10))  # 2021-09-10 08:50:17.151719
print(now - timedelta(days=1))  # 2021-09-08 22:50:17.151719
print(now + timedelta(days=2, hours=12))  # 2021-09-12 10:50:17.151719

# collections

collections 是 Python 内建的一个集合模块,提供了许多有用的集合类。

  1. namedtuple

    Python 提供了一个 namedtuple 函数,可以用来创建自定义的 tuple 对象,并且规定了 tuple 元素的个数,并可以用属性而不是索引来引用 tuple 的某个元素。

    from collections import namedtuple
    Point = namedtuple('Point', ['x', 'y'])
    p = Point(30, 125)
    print(p.y)  # 125
    print(isinstance(p, Point))  # True
    print(isinstance(p, tuple))  # True
    Circle = namedtuple('Circle', ['x', 'y', 'r'])
    c = Circle(4, 4, 5)
    print(c.r)  # 5

    使用 namedtuple 可以很方便地表示一些坐标、点或某些数据集。

  2. deque

    由于 list 是线性存储,所以它在按索引查找时很快,但删除和修改时很慢, deque 则是为了高效实现插入和删除操作的双向列表,适合用于队列和栈。

    from collections import deque
    q = deque(['a', 'b', 'c'])
    q.append('x')
    print(q)  # deque(['a', 'b', 'c', 'x'])
    q.appendleft('y')
    print(q)  # deque(['y', 'a', 'b', 'c', 'x'])

    deque 除了实现 list 的 append()pop() 外,还支持 appendleft()popleft() ,这样就可以非常高效地往头部添加或删除元素。

  3. defaultdict

    使用 dict 时,如果引用的 Key 不存在,就会抛出 KeyError 。如果希望 key 不存在时,返回一个默认值,就可以用 defaultdict

    from collections import defaultdict
    dd = defaultdict(lambda: 'N/A')
    dd['key1'] = 'abc'
    print(dd['key1'])  # abc
    print(dd['key2'])  # N/A

    除了返回默认值之外,defaultdict 和 dict 没有其他任何区别。

  4. OrderedDict

    使用 dict 时,Key 是无序的,如果要保持 Key 的顺序,可以用 OrderedDict

    from collections import OrderedDict
    od = OrderedDict([('a', 2), ('b', 3), ('c', 1)])
    print(od)  # OrderedDict ([('a', 2), ('b', 3), ('c', 1)]) # 有序

    注意: OrderedDict 的 Key 会按照插入的顺序排列,不是 Key 本身的排序。

  5. ChainMap

    ChainMap 可以把一组 dict 串起来并组成一个逻辑上的 dict 。ChainMap 本身也是一个 dict,但是查找的时候,会按照顺序在内部的 dict 依次查找。

    from collections import ChainMap
    import os, argparse
    # 构造缺省参数:
    defaults = {
        'color': 'red',
        'user': 'guest'
    }
    # 构造命令行参数:
    parser = argparse.ArgumentParser()
    parser.add_argument('-u', '--user')
    parser.add_argument('-c', '--color')
    namespace = parser.parse_args()
    command_line_args = {k: v for k, v in vars(namespace).items() if v}
    # 组合成 ChainMap:
    combined = ChainMap(command_line_args, os.environ, defaults)
    # 打印参数:
    print('color=%s' % combined['color'])
    print('user=%s' % combined['user'])

    运行结果:

    A:\pycharm-workspace\py-test>python main.py
    color=red
    user=guest
    A:\pycharm-workspace\py-test>python main.py -u Chimoku
    color=red
    user=Chimoku
  6. Counter

    Counter 是一个简单的计数器,实际上它也是 dict 的一个子类。

    # 使用 Counter 统计字符出现的个数
    from collections import Counter
    c = Counter()
    for ch in 'programming':
        c[ch] = c[ch] + 1
    print(c)  # Counter({'r': 2, 'g': 2, 'm': 2, 'p': 1, 'o': 1, 'a': 1, 'i': 1, 'n': 1})
    c.update("playing")
    print(c)  # Counter({'g': 3, 'p': 2, 'r': 2, 'a': 2, 'm': 2, 'i': 2, 'n': 2, 'o': 1, 'l': 1, 'y': 1})

# base64

Base64 是一种用 64 个字符来表示任意二进制数据的方法,Python 内置的 base64 可以直接进行 base64 的编解码:

import base64
en = base64.b64encode(b'binary\x00string')
print(en)  # b'YmluYXJ5AHN0cmluZw=='
de = base64.b64decode(b'YmluYXJ5AHN0cmluZw==')
print(de)  # b'binary\x00string'

由于标准的 Base64 编码后可能出现字符 +/ ,在 URL 中就不能直接作为参数,所以又有一种 url safe 的 base64 编码,其实就是把字符 +/ 分别变成 -_

import base64
en_byte = b'i\xb7\x1d\xfb\xef\xff'
en2 = base64.b64encode(en_byte)
print(en2)  # b'abcd++//'
uen = base64.urlsafe_b64encode(en_byte)
print(uen)  # b'abcd--__'
ude = base64.urlsafe_b64decode(uen)
print(ude == en_byte)  # True

Base64 适用于小段内容的编码,比如数字证书签名、Cookie 的内容等,但是,由于 = 字符也可能出现在 Base64 编码中,但 = 用在 URL、Cookie 里面会造成歧义,所以,很多 Base64 编码后会把 = 去掉。

Base64 是一种任意二进制到文本字符串的编码方法,常用于在 URL、Cookie、网页中传输少量二进制数据。

# struct

在 Python 中并没有专门用于处理字节的数据类型,但是由于 b'str' 可以用来表示字节,所以,Python 通常用二进制字符串来表示字节数组。

Python 提供了一个 struct 模块用来解决 bytes 和其他二进制数据类型之间的转换问题。struct 模块的 pack 函数把任意数据类型变成 bytes:

import struct
a = struct.pack('>I', 10240099)  # >I 是处理指令,
print(a)  # b'\x00\x9c@c'
un = struct.unpack('>IH', b'\xf0\xf0\xf0\xf0\x80\x80')
print(un)  # (4042322160, 32896)

其中, > 表示字节顺序是 big-endian,也就是网络序, I 表示 4 字节无符号整数。后面的参数个数要和处理指令一致。

unpack 则是将 bytes 转换为相应的数据类型。

尽管 Python 不适合编写底层操作字节流的代码,但在对性能要求不高的地方,利用 struct 将会提供很大的便利。

# hashlib

Python 的 hashlib 提供了常见的摘要算法,如 MD5,SHA1 等等。

摘要算法又称哈希算法、散列算法。它通过一个函数,把任意长度的数据转换为一个长度固定的数据串(通常用 16 进制的字符串表示)。

摘要算法就是通过摘要函数 f() 对任意长度的数据 data 计算出固定长度的摘要 digest ,目的是为了发现原始数据是否被人篡改过。

摘要算法之所以能指出数据是否被篡改过,就是因为摘要函数是一个单向函数,计算 f(data) 很容易,但通过 digest 反推 data 却非常困难。而且,对原始数据做一个 bit 的修改,都会导致计算出的摘要完全不同。

# MD5 摘要算法
import hashlib
md5 = hashlib.md5()
md5.update('how to use md5 in python hashlib?'.encode('utf-8'))
print(md5.hexdigest())  # d26a53750bc40b38b65a520292f69306
# 摘要算法支持将原始字符串进行分段计算,其计算结果不受影响
md5_2 = hashlib.md5()
md5_2.update('how to use md5 '.encode('utf-8'))
md5_2.update('in python hashlib?'.encode('utf-8'))
print(md5_2.hexdigest().__eq__(md5.hexdigest()))  # True

MD5 是最常见的摘要算法,速度很快,生成结果是固定的 128 bit 字节,通常用一个 32 位的 16 进制字符串表示。

另一种常见的摘要算法是 SHA1,调用 SHA1 和调用 MD5 完全类似:

import hashlib
sha1 = hashlib.sha1()
sha1.update('how to use sha1 in '.encode('utf-8'))
sha1.update('python hashlib?'.encode('utf-8'))
print(sha1.hexdigest())  # 2c76b57293ce30acef38d98f6046927161b46a44

SHA1 的结果是 160 bit 字节,通常用一个 40 位的 16 进制字符串表示。比 SHA1 更安全的算法是 SHA256 和 SHA512,不过越安全的算法不仅越慢,而且摘要长度更长。

对于两个完全不同的数据,它们通过摘要算法计算出的结果是有可能一样的,但是这种几率十分小。

MD5 算法经常应用在用户账号密码中,但是对于一些常规的高频的密码,通过 MD5 算法加密的结果也是固定的,为了保证用户的账号安全,可以在加密的时候对用户的密码进行加盐处理,这样能保证用户的密码通过加密后的结果不会与常规字符串加密后的结果相同。

摘要算法在很多地方都有广泛的应用。要注意摘要算法不是加密算法,不能用于加密(因为无法通过摘要反推明文),只能用于防篡改,但是它的单向计算特性决定了可以在不存储明文口令的情况下验证用户口令。

# hmac

Hmac(Keyed-Hashing for Message Authentication)通过一个标准算法,在计算哈希的过程中,将 key 混入计算过程中,和我们自定义的加 salt 算法不同,Hmac 算法针对所有哈希算法都通用,无论是 MD5 还是 SHA-1。采用 Hmac 替代我们自己的 salt 算法,可以使程序算法更标准化,也更安全。

import hmac
message = b'Hi, boy!'
key = b'randomkey'
h = hmac.new(key, message, digestmod='MD5')
h.update(b' how are you?')
r = h.hexdigest()
print(r)  # ae64cbf0742a13a1c8e2e3341779d2b3

hmac 和普通 hash 算法非常类似。hmac 输出的长度和原始哈希算法的长度一致。但注意传入的 key 和 message 都是 bytes 类型, str 类型需要首先编码为 bytes

# itertools

Python 的内建模块 itertools 提供了非常有用的用于操作迭代对象的函数。

import itertools
# 无限自然数序列
iter_num = itertools.count(1)
for n in iter_num:
    print(n)  # print Ctrl+C to stop
# 字符无限循环
iter_str = itertools.cycle('ABCD')
for n in iter_str:
    print(n)  # print Ctrl+C to stop
# 将指定内容重复指定次数
iter_repeat = itertools.repeat('AB', 10)
for n in iter_repeat:
    print(n)
# takewhile ():从无限序列中根据条件截取有限序列
iter_takewhile = itertools.count(1)
r = itertools.takewhile(lambda x: x <= 5, iter_takewhile)
print(list(r))  # [1, 2, 3, 4, 5]
# chain ():将多个迭代对象串联起来
chin_list = []
for c in itertools.chain('ABC', 'XYZ', 'O'):
    chin_list.append(c)
print(chin_list)  # ['A', 'B', 'C', 'X', 'Y', 'Z', 'O']
# groupby ():将迭代对象中连续的相同元素进行分组(可以指定规则)
list_group = []
for key, group in itertools.groupby('AaaBBbaAcC', lambda c: c.upper()):
    list_sub = [key, ''.join(list(group))]
    list_group.append(list_sub)
print(list_group)  # [['A', 'Aaa'], ['B', 'BBb'], ['A', 'aA'], ['C', 'cC']]

itertools 生成的迭代对象,其内部的元素只有在进行迭代的时候才会生成。

# contextlib

在 Python 中,在进行文件读写后,需要对显式关闭文件,但为了使用方便,也可以使用 with 语句进行简化。但实际上,with 语句不只适用于 IO 操作,而是适用于任何正确实现上下文管理的对象,而上下文管理是通过 __enter____exit__ 方法来实现的,如下:

class Query(object):
    def __init__(self, name):
        self.name = name
    def __enter__(self):
        print('Query start -->')
        return self
    def __exit__(self, exc_type, exc_value, traceback):
        if exc_type:
            print('---- [Something Error] ----')
        else:
            print('<-- Query end')
    def query(self):
        print('Query info about %s...' % self.name)
with Query('Baidu') as q:
    q.query()
# Query start -->
# Query info about Baidu...
# <-- Query end

对于需要进行上下文管理的对象,也可以使用 Python 的标准库 contextlib 进行简化书写:

from contextlib import contextmanager
class Query(object):
    def __init__(self, name):
        self.name = name
    def query(self):
        print('Query info about %s...' % self.name)
@contextmanager
def create_query(name):
    print('Query start -->')
    yield Query(name)  # 执行 with 语句块中的内容
    print('<-- Query end')
with create_query('Baidu') as q:
    q.query()
# Query start -->
# Query info about Baidu...
# <-- Query end

其中, yield 语句相当于调用执行了 with 语句块中的代码内容。yield 语句前后的代码,将分别在 with 语句块执行前后执行,它们之间的执行顺序为:

  1. with 语句执行 yield 之前的代码。

  2. yield 调用执行 with 语句块中的代码。

  3. 执行 yield 之后的代码。

如果一个对象没有实现上下文,那么它就不能用于 with 语句,但我们可以使用 closing() 来将普通对象变为上下文对象,例如:

from contextlib import closing
from urllib.request import urlopen
# 获取 python 官网 html 内容
with closing(urlopen('https://www.python.org')) as page:
    for line in page:
        print(line)

但其实,closing 函数在源码中的定义也很简单:

class closing(AbstractContextManager):
    def __init__(self, thing):
        self.thing = thing
    def __enter__(self):
        return self.thing
    def __exit__(self, *exc_info):
        self.thing.close()

它本质上也是定义了 __enter____exit__ 方法使对象实现了上下文管理。

# urllib

urllib 提供了一系列操作 URL 的功能,它可以模拟浏览器发送网络请求。

from contextlib import closing
from urllib import request
# 获取百度首页 html 内容
with closing(request.urlopen('https://www.baidu.com')) as f:
    data = f.read()
    print('Status:', f.status, f.reason)
    for k, v in f.getheaders():
        print('%s: %s' % (k, v))
    print('Data:', data.decode('utf-8'))

同时,在发送请求时,也可以指定请求头信息:

from contextlib import closing
from urllib import request
# 获取百度首页 html 内容(模拟 iPhone 的移动版)
req = request.Request('http://www.baidu.com/')
req.add_header('User-Agent', 'Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25')
with closing(request.urlopen(req)) as f:
    print('Status:', f.status, f.reason)
    for k, v in f.getheaders():
        print('%s: %s' % (k, v))
    print('Data:', f.read().decode('utf-8'))

如需模拟 post 请求,则需要将数据以 bytes 的形式传入,例如:

from contextlib import closing
from urllib import request, parse
print('Welcome to login xxx...')
email = input('Email: ')
passwd = input('Password: ')
login_data = parse.urlencode([
    ('username', email),
    ('password', passwd)
])
req = request.Request('https://www.youhost.com/login')
with closing(request.urlopen(req, data=login_data.encode('utf-8'))) as f:
    print('Status:', f.status, f.reason)
    for k, v in f.getheaders():
        print('%s: %s' % (k, v))
    print('Data:', f.read().decode('utf-8'))

# XML

操作 XML 有两种方法:DOM 和 SAX。DOM 会把整个 XML 读入内存,解析为树,因此占用内存大,解析慢,优点是可以任意遍历树的节点。SAX 是流模式,边读边解析,占用内存小,解析快,缺点是我们需要自己处理事件。

在 Python 中使用 SAX 解析 XML 非常简洁,它通常会产生三个事件:

  1. start_element:xml 元素开始标签。

  2. char_data:xml 元素环绕内容。

  3. end_element:xml 元素结束标签。

from xml.parsers.expat import ParserCreate
class DefaultSaxHandler(object):
    def start_element(self, name, attrs):
        print('sax:start_element: %s, attrs: %s' % (name, str(attrs)))
    def end_element(self, name):
        print('sax:end_element: %s' % name)
    def char_data(self, text):
        print('sax:char_data: %s' % text)
xml = r'''<?xml version="1.0"?>
<ol>
    <li><a href="/python">Python</a></li>
    <li><a href="/java">Java</a></li>
</ol>
'''
handler = DefaultSaxHandler()
parser = ParserCreate()
parser.StartElementHandler = handler.start_element
parser.EndElementHandler = handler.end_element
parser.CharacterDataHandler = handler.char_data
parser.Parse(xml)

相比 JSON 而言,XML 较为复杂,组装数据也很不方便,推荐优先使用 JSON。

# HTMLParser

HTML 本质上是 XML 的子集,但是 HTML 的语法没有 XML 那么严格,所以不能用标准的 DOM 或 SAX 来解析 HTML。Python 为此提供了 HTMLParser 来非常方便地解析 HTML。

from html.parser import HTMLParser
class MyHTMLParser(HTMLParser):
    def handle_starttag(self, tag, attrs):
        print('<%s>' % tag)
    def handle_endtag(self, tag):
        print('</%s>' % tag)
    def handle_startendtag(self, tag, attrs):
        print('<%s/>' % tag)
    def handle_data(self, data):
        print(data)
    def handle_comment(self, data):
        print('<!--', data, '-->')
    def handle_entityref(self, name):
        print('&%s;' % name)
    def handle_charref(self, name):
        print('&#%s;' % name)
parser = MyHTMLParser()
parser.feed('''<html>
<head></head>
<body>
<!-- test html parser -->
    <p>Some <a href=\"#\">html</a> HTML&nbsp;tutorial...<br>END</p>
</body></html>''')

feed() 方法可以多次调用,因此,对于一个 HTML 字符串,也可以进行分段处理。

# 常用第三方模块

基本上 Python 所有第三方模块都会在 PyPI - the Python Package Index 进行注册,需要使用时,直接通过 pip 进行安装即可。

# Pillow

PIL:Python Imaging Library。

  1. 安装 Pillow

    如果已经安装 Anaconda,那么 Pillow 可以直接使用,如果未安装 Anaconda,则需要执行命令 pip install pillow 进行安装。

  2. 使用 Pillow 操作图像

    缩小图像并进行模糊处理:

    from PIL import Image, ImageFilter
    # 打开一个 jpg 图像文件,注意是当前路径:
    im = Image.open('A:\\img\\test.jpg')
    # 获得图像尺寸:
    w, h = im.size
    print('Original image size: %sx%s' % (w, h))
    # 缩放到 50%:
    im.thumbnail((w // 2, h // 2))
    print('Resize image to: %sx%s' % (w // 2, h // 2))
    # 应用模糊滤镜:
    im2 = im.filter(ImageFilter.BLUR)
    # 把缩放后的图像用 jpeg 格式保存:
    im2.save('A:\\img\\test2.jpg', 'jpeg')

    除了缩放和模糊处理外,还可以进行如切片、旋转、滤镜、输出文字、调色板等一些列图像操作。

    生成验证码图片:

    from PIL import Image, ImageDraw, ImageFont, ImageFilter
    import random
    # 随机字母:
    def rndChar():
        return chr(random.randint(65, 90))
    # 随机颜色 1:
    def rndColor():
        return (random.randint(64, 255), random.randint(64, 255), random.randint(64, 255))
    # 随机颜色 2:
    def rndColor2():
        return (random.randint(32, 127), random.randint(32, 127), random.randint(32, 127))
    # 240 x 60:
    width = 60 * 4
    height = 60
    image = Image.new('RGB', (width, height), (255, 255, 255))
    # 创建 Font 对象:
    font = ImageFont.truetype('arial.ttf', 36)
    # 创建 Draw 对象:
    draw = ImageDraw.Draw(image)
    # 填充每个像素:
    for x in range(width):
        for y in range(height):
            draw.point((x, y), fill=rndColor())
    # 输出文字:
    for t in range(4):
        draw.text((60 * t + 10, 10), rndChar(), font=font, fill=rndColor2())
    # 模糊:
    image = image.filter(ImageFilter.BLUR)
    image.save('A:\\img\\captcha.jpg', 'jpeg')

    Python生成验证码

    更多好玩的功能,参考官方文档:https://pillow.readthedocs.org

# requests

相比于 urllib 模块,requests 模块在处理 URL 资源时更加方便。

如果已经安装 Anaconda,那么 requests 可以直接使用,如果未安装 Anaconda,则需要执行命令 pip install requests 进行安装。

常用方法示例:

import requests
# GET 请求
r = requests.get("http://www.baidu.com/s", params={'wd': 'python'})
print(r.url)  # https://www.baidu.com/s?wd=python
print(r.encoding)  # ISO-8859-1(requests 自动检测编码)
print(r.status_code)
print(r.text)
print(r.content)  # 获取响应的 bytes 内容
# 处理 json 内容
r2 = requests.get('https://v2.jinrishici.com/one.json')  # 今日诗词 API
print(r2.json())
# 设置请求头
r3 = requests.get("http://www.baidu.com/s", params={'wd': 'python'}, headers={'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit'})
print(r3.text)
# POST 请求(默认使用 application/x-www-form-urlencoded 编码)
r4 = requests.post('https://www.test.com/login', data={'email': 'abc@example.com', 'password': '123456'})
# POST 传递 JSON 数据
params = {'email': 'abc@example.com', 'password': '123456'}
r5 = requests.post('https://www.test.com/login', json=params)
# POST 上传文件
upload_files = {'file': open('A:\\xls\\test.xls', 'rb')}  # 注意使用 rb 读取,保证文件完整
r6 = requests.post('https://www.test.com/upload', files=upload_files)
print(r6.headers)  # 获取响应头信息
# Cookies 传送与接收、请求超时
cs = {'token': '123456'}
r7 = requests.get('https://www.test.com/testCookies', cookies=cs, timeout=2.5)  # 2.5 秒后超时
print(r.cookies['status'])

# chardet

chardet 主要用于检测编码。

如果已经安装 Anaconda,那么 chardet 可以直接使用,如果未安装 Anaconda,则需要执行命令 pip install chardet 进行安装。

常用方法示例:

import chardet as chardet
ce = chardet.detect(b'Hello, world!')
print(ce)  # {'encoding': 'ascii', 'confidence': 1.0, 'language': ''}
data = '一帷风动百虫绝,连月推山雪见归'.encode('gbk')
print(chardet.detect(data))  # {'encoding': 'GB2312', 'confidence': 0.99, 'language': 'Chinese'}
data = '一帷风动百虫绝,连月推山雪见归'.encode('utf-8')
print(chardet.detect(data))  # {'encoding': 'utf-8', 'confidence': 0.99, 'language': ''}
data = 'チンモクのブログ'.encode('euc-jp')
print(chardet.detect(data))  # {'encoding': 'EUC-JP', 'confidence': 0.99, 'language': 'Japanese'}

通过编码检测后,再进行内容解码,能够有效避免很多转码异常。

# psutil

psutil: process and system utilities,可以用于获取系统信息,并且支持跨平台使用。

如果已经安装 Anaconda,那么 psutil 可以直接使用,如果未安装 Anaconda,则需要执行命令 pip install psutil 进行安装。

常用方法示例:

import psutil
# 获取 CPU 逻辑数量
print(psutil.cpu_count())  # 8
# 获取 CPU 物理核心
print(psutil.cpu_count(logical=False))  # 8
# 统计 CPU 的用户/系统/空闲时间
print(psutil.cpu_times())  #
# 实现类似 top 命令的 CPU 使用率,每秒刷新一次
for x in range(10):
    print(psutil.cpu_percent(interval=1, percpu=True))
# 获取物理内存信息
print(psutil.virtual_memory())
# svmem(total=25701597184, available=17957904384, percent=30.1, used=7743692800, free=17957904384)
# 获取交换内存信息
print(psutil.swap_memory())
# sswap(total=29459693568, used=12058066944, free=17401626624, percent=40.9, sin=0, sout=0)
# 获取磁盘信息
print(psutil.disk_partitions())  # 磁盘分区信息
print(psutil.disk_usage('/'))  # 磁盘使用情况
print(psutil.disk_io_counters())  # 磁盘 IO
# 获取网络信息
print(psutil.net_io_counters())  # 获取网络读写字节/包的个数
print(psutil.net_if_addrs())  # 获取网络接口信息
print(psutil.net_if_stats())  # 获取网络接口状态
print(psutil.net_connections())  # 获取当前网络连接信息
# 获取进程信息
print(psutil.pids())  # 获取所有进程
p = psutil.Process(1300)  # 获取指定进程
print(p.name)  # 进程名称
print(p.cwd())  # 进程工作目录
print(p.cmdline())  # 进程启动的命令行
print(p.ppid())  # 父进程 ID
print(p.parent())  # 父进程
print(p.children())  # 子进程列表
print(p.status())  # 进程状态
print(p.create_time())  # 进程创建时间
print(p.cpu_times())  # 进程使用的 CPU 时间
print(p.memory_info())  # 进程使用的内存
print(p.connections())  # 进程相关的网络连接
print(p.num_threads())  # 进程的线程数量
print(p.threads())  # 进程的所有线程
print(p.environ())  # 进程环境变量
p.terminate()  # 结束进程

如在实际项目中需要查询相关功能,参考官方文档:https://github.com/giampaolo/psutil

# virtualenv

如果在同一台主机上,需要使用到多个不同的 Python 运行环境,这时就需要使用 virtualenv 对 Python 环境进行隔离。

安装 virtualenv:

$ pip3 install virtualenv

# 图形界面

Python 支持多种图形界面的第三方库,包括但不限于:

  • Tk

  • wxWidgets

  • Qt

  • GTK

此外,Python 也自带有支持 Tk 的 Tkinter ,无需安装任何包,就可以直接使用。

Tk 是一个图形库,支持多个操作系统,使用 Tcl 语言开发,它会调用操作系统提供的本地 GUI 接口,完成最终的 GUI。而 Tkinter 则对访问 Tk 的接口进行了封装。

Python图形界面

# 海龟绘图

绘制长方形:

# 导入 turtle 包的所有内容:
from turtle import *
# 设置笔刷宽度:
width(4)
# 前进:
forward(200)
# 右转 90 度:
right(90)
# 笔刷颜色:
pencolor('red')
forward(100)
right(90)
pencolor('green')
forward(200)
right(90)
pencolor('blue')
forward(100)
right(90)
# 调用 done () 使得窗口等待被关闭,否则将立刻关闭窗口
done()

结合 Python 的逻辑判断,可以绘制复杂的图像,例如五个五角星:

from turtle import *
def drawStar(x, y):
    pu()
    goto(x, y)
    pd()
    # set heading: 0
    seth(0)
    for i in range(5):
        fd(40)
        rt(144)
for x in range(0, 250, 50):
    drawStar(x, 0)
done()

使用递归还可以绘制复杂的图形,例如绘制一棵树:

from turtle import *
# 设置色彩模式是 RGB:
colormode(255)
lt(90)
lv = 14
l = 120
s = 45
width(lv)
# 初始化 RGB 颜色:
r = 0
g = 0
b = 0
pencolor(r, g, b)
penup()
bk(l)
pendown()
fd(l)
def draw_tree(l, level):
    global r, g, b
    # save the current pen width
    w = width()
    # narrow the pen width
    width(w * 3.0 / 4.0)
    # set color:
    r = r + 1
    g = g + 2
    b = b + 3
    pencolor(r % 200, g % 200, b % 200)
    l = 3.0 / 4.0 * l
    lt(s)
    fd(l)
    if level < lv:
        draw_tree(l, level + 1)
    bk(l)
    rt(2 * s)
    fd(l)
    if level < lv:
        draw_tree(l, level + 1)
    bk(l)
    lt(s)
    # restore the previous pen width
    width(w)
speed("fastest")
draw_tree(l, 4)
done()

运行最终显示效果:

Python海龟绘图-树

# 网络编程

# TCP/IP 简介

略。

# TCP 编程

Socket 是网络编程的一个抽象概念。通常我们用一个 Socket 表示打开了一个网络链接,而打开一个 Socket 则需要知道目标计算机的 IP、端口和协议类型。

创建一个基于 TCP 的 Socket 客户端连接:

# 导入 socket 库:
import socket
# 创建一个 socket:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # AF_INET 指 IPv4 协议,AF_INET6 指 IPv6 协议。SOCK_STREAM 指定使用面向流的 TCP 协议
# 建立连接:
s.connect(('www.test.com', 80))  # 参数为 tuple 类型
# 发送数据:
s.send(b'GET / HTTP/1.1\r\nHost: www.test.com\r\nConnection: close\r\n\r\n')
# 接收数据:
buffer = []
while True:
    # 每次最多接收 1k 字节:
    d = s.recv(1024)
    if d:
        buffer.append(d)
    else:
        break  # 当接收到空,退出接收
data = b''.join(buffer)
# 关闭连接:
s.close()
# 自定义处理数据
header, html = data.split(b'\r\n\r\n', 1)
print(header.decode('utf-8'))
# 把接收的数据写入文件:
with open('index.html', 'wb') as f:
    f.write(html)

服务器端通常会打开一个固定的端口,用于监听所有的客户端连接。对于大量的客户端连接,需要对连接的 Socket 进行识别,一个 Socket 依赖 4 项:服务器地址、服务器端口、客户端地址、客户端端口来唯一确定一个 Socket。此外,服务器端需要具备同时相应多个客户端的能力,所以,每个连接都需要一个新的进程或者新的线程来处理。

创建服务端:

import socket
import threading
from datetime import time
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 监听端口:
s.bind(('127.0.0.1', 9999))
# 监听端口并指定等待连接的最大数量
s.listen(5)
print('Waiting for connection...')
def tcplink(sock, addr):
    print('Accept new connection from %s:%s...' % addr)
    sock.send(b'Welcome!')
    # 持续监听客户端消息
    while True:
        data = sock.recv(1024)
        time.sleep(1)
        # 当客户端发送了 exit 时,断开连接
        if not data or data.decode('utf-8') == 'exit':
            break
        sock.send(('Hello, %s!' % data.decode('utf-8')).encode('utf-8'))
    sock.close()
    print('Connection from %s:%s closed.' % addr)
while True:
    # 接收一个新连接:
    sock, addr = s.accept()
    # 创建新线程来处理 TCP 连接:
    # 每个连接都必须创建新线程(或进程)来处理,否则,单线程在处理连接的过程中,无法接受其他客户端的连接
    t = threading.Thread(target=tcplink, args=(sock, addr))
    t.start()

可以创建客户端来对和上面的服务端进行通信:

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 建立连接:
s.connect(('127.0.0.1', 9999))
# 接收欢迎消息:
print(s.recv(1024).decode('utf-8'))
for data in [b'Li Bai', b'Du Fu', b'Ou Yangxiu']:
    # 发送数据:
    s.send(data)
    print(s.recv(1024).decode('utf-8'))
s.send(b'exit')
s.close()

注意:同一台主机,同一个端口,同一时间只能被一个 Socket 绑定。

# UDP 编程

TCP 是建立可靠连接,并且通信双方都可以以流的形式发送数据。相对 TCP,UDP 则是面向无连接的协议。

使用 UDP 协议时,不需要建立连接,只需要知道对方的 IP 和端口,就可以直接发数据包,但它不能保证接收方一定收到。

虽然用 UDP 传输数据不可靠,但它的优点是和 TCP 比,速度快,对于不要求可靠到达的数据,就可以使用 UDP 协议。

UDP 服务端示例:

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # SOCK_DGRAM 指定了这个 Socket 的类型是 UDP
# 绑定端口:
s.bind(('127.0.0.1', 9999))
print('Bind UDP on 9999...')
while True:
    # 接收数据:
    data, addr = s.recvfrom(1024)  # 返回数据和客户端的地址与端口
    print('Received from %s:%s.' % addr)
    s.sendto(b'Hello, %s!' % data, addr)

UDP 客户端示例:

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for data in [b'Michael', b'Tracy', b'Sarah']:
    # 发送数据:
    s.sendto(data, ('127.0.0.1', 9999))
    # 接收数据:
    print(s.recv(1024).decode('utf-8'))
s.close()

在使用 UDP 进行通信时,服务端并不需要调用 listen 方法进行监听,而是直接接收来自客户端的数据,客户端也不需要调用 connect 来建立连接,直接通过 sendto 方法发送数据即可。

UDP 与 TCP 最大的区别在于是否建立连接,也由此推导出 UDP 具有传输数据不可靠的特点。此外,UDP 和 TCP 的端口是互不冲突的,即它们可以同时使用同一个端口。

# 电子邮件

# SMTP 发送邮件

SMTP 是发送邮件的协议,可以使用它来发送纯文本邮件、HTML 邮件以及带附件的邮件。Python 对 SMTP 支持有 smtplibemail 两个模块,email 负责构造邮件,smtplib 负责发送邮件。

from email import encoders
from email.header import Header
from email.mime.text import MIMEText
from email.utils import parseaddr, formataddr
import smtplib
def _format_addr(s):
    name, addr = parseaddr(s)
    return formataddr((Header(name, 'utf-8').encode(), addr))
# 输入 Email 地址和口令:
from_addr = input('From: ')
password = input('Password: ')
# 输入收件人地址:
to_addr = input('To: ')
# 输入 SMTP 服务器地址:
smtp_server = input('SMTP server: ')
msg = MIMEText('君莫笑,我们蓝溪阁不是你的保姆!!!', 'plain', 'utf-8')
msg['From'] = _format_addr('蓝溪阁 <%s>' % from_addr)
msg['To'] = _format_addr('叶修 <%s>' % to_addr)
msg['Subject'] = Header('来自蓝溪阁的声明', 'utf-8').encode()
server = smtplib.SMTP(smtp_server, 25)  # SMTP 协议默认端口是 25
server.set_debuglevel(1)  # 打印出和 SMTP 服务器交互的所有信息
server.login(from_addr, password)  # 登录 SMTP 服务器
server.sendmail(from_addr, [to_addr], msg.as_string())
server.quit()

如果需要发送 html 内容,只需要传递 html 文本,并指定对应的类型参数即可:

msg = MIMEText('<html><body><h1>来自岚希阁某河的最新消息</h1>' +
    '<p>嘉世、霸图、蓝雨、蓝溪阁、薇草各大战队将在集结人马在<a href="http://www.python.org">这个地点</a>进行埋伏,野图BOSS是个陷进。</p>' +
    '</body></html>', 'html', 'utf-8')

# POP3 收取邮件

收取邮件就是编写一个 MUA 作为客户端,从 MDA 把邮件获取到用户的电脑或者手机上。收取邮件最常用的协议是 POP 协议,目前版本号是 3,俗称 POP3。

注意,POP3 协议收取的不是一个已经可以阅读的邮件本身,而是邮件的原始文本,还需要用 email 模块提供的各种类来解析原始文本,变成可阅读的邮件对象。

通过 POP3 获取最新的一封邮件内容:

import poplib
# 输入邮件地址,口令和 POP3 服务器地址:
from email.header import decode_header
from email.parser import Parser
from email.utils import parseaddr
email = input('Email: ')
password = input('Password: ')
pop3_server = input('POP3 server: ')
# 连接到 POP3 服务器:
server = poplib.POP3(pop3_server)
# 可以打开或关闭调试信息:
server.set_debuglevel(1)
# 可选:打印 POP3 服务器的欢迎文字:
print(server.getwelcome().decode('utf-8'))
# 身份认证:
server.user(email)
server.pass_(password)
# stat () 返回邮件数量和占用空间:
print('Messages: %s. Size: %s' % server.stat())
# list () 返回所有邮件的编号:
resp, mails, octets = server.list()
# 可以查看返回的列表类似 [b'1 82923', b'2 2184', ...]
print(mails)
# 获取最新一封邮件,注意索引号从 1 开始:
index = len(mails)
resp, lines, octets = server.retr(index)
# lines 存储了邮件的原始文本的每一行,
# 可以获得整个邮件的原始文本:
msg_content = b'\r\n'.join(lines).decode('utf-8')
# 稍后解析出邮件:
msg = Parser().parsestr(msg_content)
# 解析邮件内容
def decode_str(s):
    value, charset = decode_header(s)[0]
    if charset:
        value = value.decode(charset)
    return value
def guess_charset(msg):
    charset = msg.get_charset()
    if charset is None:
        content_type = msg.get('Content-Type', '').lower()
        pos = content_type.find('charset=')
        if pos >= 0:
            charset = content_type[pos + 8:].strip()
    return charset
# indent 用于缩进显示:
def print_info(msg, indent=0):
    if indent == 0:
        for header in ['From', 'To', 'Subject']:
            value = msg.get(header, '')
            if value:
                if header=='Subject':
                    value = decode_str(value)
                else:
                    hdr, addr = parseaddr(value)
                    name = decode_str(hdr)
                    value = u'%s <%s>' % (name, addr)
            print('%s%s: %s' % ('  ' * indent, header, value))
    if (msg.is_multipart()):
        parts = msg.get_payload()
        for n, part in enumerate(parts):
            print('%spart %s' % ('  ' * indent, n))
            print('%s--------------------' % ('  ' * indent))
            print_info(part, indent + 1)
    else:
        content_type = msg.get_content_type()
        if content_type=='text/plain' or content_type=='text/html':
            content = msg.get_payload(decode=True)
            charset = guess_charset(msg)
            if charset:
                content = content.decode(charset)
            print('%sText: %s' % ('  ' * indent, content + '...'))
        else:
            print('%sAttachment: %s' % ('  ' * indent, content_type))
print_info(msg)
# 可以根据邮件索引号直接从服务器删除邮件:
# server.dele(index)
# 关闭连接:
server.quit()

# 数据库访问

注意,相关的数据库环境,需要自行安装。

# 使用 SQLite

SQLite 是一种嵌入式数据库,它的数据库就是一个文件。由于 SQLite 本身是 C 语言开发的,而且体积很小,所以,经常被集成到各种应用程序中,甚至在 iOS 和 Android 应用中都可以集成。在 Python 中也内置了 SQLite3,可以直接使用。

import sqlite3
# 1. 创建连接(数据库文件是 test.db,如果文件不存在,会自动在当前目录创建)
conn = sqlite3.connect('test.db')
# 2. 创建一个游标 Cursor
cursor = conn.cursor()
# 3. 执行 SQL 语句(创建 user 表并添加数据)
cursor.execute('create table user (id varchar(20) primary key, name varchar(20))')
cursor.execute('insert into user (id, name) values (\'1\', \'Michael\')')
print(cursor.rowcount)  # 在执行增删改操作时,通过 rowcount 返回影响的行数,可用于判断执行是否符合预期
# 4. 关闭游标 Cursor
cursor.close()
# 5. 提交事务
conn.commit()
# 6. 关闭连接
conn.close()

查询数据与添加数据大同小异:

import sqlite3
conn = sqlite3.connect('test.db')
cursor = conn.cursor()
cursor.execute('select * from user where id=?', ('1',))
values = cursor.fetchall()  # 通过 fetchall 获取到 list 类型的结果集,其内的每个元素都是一个 tuple,对应一行记录
cursor.close()
conn.commit()
conn.close()
print(values)  # [('1', 'Michael')]

如果需要执行的 sql 语句中含有参数,则需要传入 tuple 类型的参数,并与 sql 语句中的 ? 占位符进行匹配,从而构造完整的 sql 语句。如需了解更多更细致的 SQLite 知识,请另行学习。

# 使用 MySQL

SQLite 的特点是轻量级、可嵌入,但不能承受高并发访问,适合桌面和移动应用。而 MySQL 是为服务器端设计的数据库,能承受高并发访问,同时占用的内存也远远大于 SQLite。

MySQL 安装,自行百度。

由于 MySQL 服务器以独立的进程运行,并通过网络对外服务,所以,需要支持 Python 的 MySQL 驱动来连接到 MySQL 服务器。使用如下命令安装驱动:

$ pip install mysql-connector-python --allow-external mysql-connector-python
$ pip install mysql-connector # 备选驱动

MySQL 连接示例:

import mysql.connector
# 创建 MySQL 表并添加数据记录
conn = mysql.connector.connect(user='root', password='password', database='test')
cursor = conn.cursor()
cursor.execute('create table user (id varchar(20) primary key, name varchar(20))')
# 注意 MySQL 的占位符是 % s
cursor.execute('insert into user (id, name) values (%s, %s)', ['1', 'Michael'])
print(cursor.rowcount)
conn.commit()  # 事务必须提交才能生效
cursor.close()
# 查询数据
cursor = conn.cursor()
cursor.execute('select * from user where id = %s', ('1',))
values = cursor.fetchall()
print(values)
cursor.close()
conn.close()

由于 Python 的 DB-API 定义都是通用的,所以,操作 MySQL 的数据库代码和 SQLite 类似。

# 使用 SQLAlchemy

SQLAlchemy 是 Python 中一个通过 ORM 操作数据库的框架。

SQLAlchemy 对象关系映射器提供了一种方法,用于将用户定义的 Python 类与数据库表相关联,并将这些类(对象)的实例与其对应表中的行相关联。它包括一个透明地同步对象及其相关行之间状态的所有变化的系统,称为工作单元,以及根据用户定义的类及其定义的彼此之间的关系表达数据库查询的系统。它可以让我们使用类和对象的方式操作数据库,从而从繁琐的 sql 语句中解脱出来。

安装 SQLAlchemy:

$ pip install sqlalchemy

SQLAlchemy 操作数据库示例:

from sqlalchemy import Column, String, create_engine, ForeignKey
from sqlalchemy.orm import sessionmaker, relationship
from sqlalchemy.ext.declarative import declarative_base
# 创建对象的基类:
Base = declarative_base()
# 定义 User 对象:
class User(Base):
    # 表的名字:
    __tablename__ = 'user'
    # 表的结构:
    id = Column(String(20), primary_key=True)
    name = Column(String(20))    # 一对多:
    books = relationship('Book')
class Book(Base):
    __tablename__ = 'book'
    id = Column(String(20), primary_key=True)
    name = Column(String(20))
    # “多” 的一方的 book 表是通过外键关联到 user 表的:
    user_id = Column(String(20), ForeignKey('user.id'))
# 初始化数据库连接:
engine = create_engine('mysql+mysqlconnector://root:password@localhost:3306/test')
# 创建 DBSession 类型:
DBSession = sessionmaker(bind=engine)
# ------- 新增数据 -------
# 创建 session 对象:
session = DBSession()
# 创建新 User 对象:
new_user = User(id='5', name='Bob')
# 添加到 session:
session.add(new_user)
# 提交即保存到数据库:
session.commit()
# 关闭 session:
session.close()
# ------- 查询数据 -------
# 创建 Session:
session = DBSession()
# 创建 Query 查询,filter 是 where 条件,最后调用 one () 返回唯一行,如果调用 all () 则返回所有行:
user = session.query(User).filter(User.id=='5').one()
# 打印类型和对象的 name 属性:
print('type:', type(user))
print('name:', user.name)
print('books:', user.books)
# 关闭 Session:
session.close()

# Web 开发

# HTTP 协议简介

略。

# HTML 简介

略。

# WSGI 接口

WSGI:Web Server Gateway Interface。

创建脚本文件 application.py

# application 函数需要由 WSGI 来调用
def start(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html')])  # Header 只能发送一次
    body = '<h1>Hello, %s!</h1>' % (environ['PATH_INFO'][1:] or 'web')
    return [body.encode('utf-8')]

在同级目录下创建 server.py

# 使用 Python 内置的 WSGI 服务器(不推荐,仅供开发测试时使用)
from wsgiref.simple_server import make_server
from application import start
# 创建一个服务器,IP 地址为空,端口是 8000,处理函数是 application:
httpd = make_server('', 8000, start)
print('Serving HTTP on port 8000...')
# 开始监听 HTTP 请求:
httpd.serve_forever()

启动 server.py 并在浏览器访问 http://localhost:8000 ,可以正常访问,说明 web 服务启动成功。

HTTP 请求的所有输入信息都可以通过 environ 获得,HTTP 响应的输出都可以通过 start_response() 加上函数返回值作为 Body。

# 使用 Web 框架

Python 的 web 框架较多,最为主流的主要有以下几款:

  1. Flask

  2. Django

  3. Tornado

  4. Twisted

知乎上有话题讨论这些框架:https://www.zhihu.com/question/20706333

这里演示如何使用 flask 框架来提供服务,其他框架的使用方法类似。

安装:

$ pip install flask

使用示例:

from flask import Flask
from flask import request
app = Flask(__name__)
@app.route('/', methods=['GET', 'POST'])
def home():
    return '<h1>Home</h1><div><a href="/signin">to login</a></div>'
@app.route('/signin', methods=['GET'])
def signin_form():
    return '''<form action="/signin" method="post">
              <p><input name="username"></p>
              <p><input name="password" type="password"></p>
              <p><button type="submit">Sign In</button></p>
              </form>'''
@app.route('/signin', methods=['POST'])
def signin():
    # 需要从 request 对象读取表单内容:
    if request.form['username'] == 'admin' and request.form['password'] == 'password':
        return '<h3>Hello, admin!</h3>'
    return '<h3>Bad username or password.</h3><br><p>redirect to <a href="/signin">login page</a></p>'
if __name__ == '__main__':
    app.run()

# 使用模板

在 web 开发中,MVC 是一种最常见的设计思想,Python 也同样支持 MVC 的设计模式。

新建 app.py

from flask import Flask, request, render_template
app = Flask(__name__)
@app.route('/', methods=['GET', 'POST'])
def home():
    return render_template('home.html')
@app.route('/signin', methods=['GET'])
def signin_form():
    return render_template('form.html')
@app.route('/signin', methods=['POST'])
def signin():
    username = request.form['username']
    password = request.form['password']
    if username=='admin' and password=='password':
        return render_template('signin-ok.html', username=username)
    return render_template('form.html', message='Bad username or password', username=username)
if __name__ == '__main__':
    app.run()

app.py 同级目录下创建文件夹 templates,并在其中添加 html 文件:

<!-- form.html -->
<html>
<head>
  <title>Please Sign In</title>
</head>
<body>
  
  <form action="/signin" method="post">
    <legend>Please sign in:</legend>
    <p><input name="username" placeholder="Username" value=""></p>
    <p><input name="password" placeholder="Password" type="password"></p>
    <p><button type="submit">Sign In</button></p>
  </form>
</body>
</html>
<!-- home.html -->
<html>
<head>
  <title>Home</title>
</head>
<body>
  <h1 style="font-style:italic">Home</h1>
</body>
</html>
<!-- signin-ok.html -->
<html>
<head>
  <title>Welcome, </title>
</head>
<body>
  <p>Welcome, !</p>
</body>
</html>

Flask 框架默认使用 jinja2 作为视图渲染模板。它使用 {{ name }} 进行占位,表示一个需要替换的变量,如果需要使用循环、条件判断等指令语句,在 Jinja2 中则是使用 {% ... %} 进行表示。

不同 web 框架指定使用的渲染模板各不相同,但它们基本上都是通过占位符进行匹配替换实现的模板动态生成,在原理上大同小异。

# 异步 IO

关于 IO 的理论叙述,我已经在文章 Java 基础知识大盘点的 IO 章节有过详细的整理,尽管语言不同,但是基本原理和概念是完全一致的。

# 协程

协程(Coroutine),又称微线程,纤程。

协程其实就是一个子程序,但与普通子程序不同,协程在执行过程中,其子程序内部是可以中断的,让出执行权,等到适当的时候再继续执行。

协程是单个线程执行,并非多线程,它的优势在于执行效率高,因为它是通过同一线程内部的多个子程序之间进行切换实现异步的,不会产生额外的线程开销。另外,协程由于只是单个线程,所以并不需要引入锁机制来写操作冲突。

为了充分发挥协程的执行效率,通常会将协程和多进程结合使用,以便能够利用多核 CPU。

协程示例:

def consumer():
    r = ''
    while True:
        n = yield r
        if not n:
            return
        print('[CONSUMER] Consuming %s...' % n)
        r = '200 OK'
def produce(c):
    c.send(None)  # 1. 在一个生成器函数未启动之前,是不能传递值进去的,需要执行 c.send (None) 或 next (c) 来返回生成器的第一个值
    n = 0
    while n < 5:
        n = n + 1
        print('[PRODUCER] Producing %s...' % n)
        r = c.send(n)  # 启动生成器,并传入参数,接着 yield 语句继续执行
        print('[PRODUCER] Consumer return: %s' % r)
    c.close()  # 结束生成器的生命周期
c = consumer()
produce(c)

这一段代码的执行顺序大致如下:

# 伪代码:
produce(consumer())  # 1. 将 consumer 函数作为参数传递给 produce 函数
c.send(None)  # 2. 初始化生成器函数,进入 consumer 函数
r = ''
# 进入 while True 循环
n = yield r  # 3. 此时 r 为空,n 未定义,跳出 consumer 函数,并标记当前跳出点(假设为 A 点),继续执行 c.send (None) 后续代码
n = 0
# 进入 while n < 5 循环(第一轮循环)
n = n + 1
print('[PRODUCER] Producing %s...' % n)  # 此时 n=1
r = c.send(n)  # 4. 再次进入生成器,查找上次标记的点 A,并从点 A 开始继续执行代码(此时 n=1)
# 判断 if not n
print('[CONSUMER] Consuming %s...' % n)  # 此时 n=1
r = '200 OK'
n = yield r  # 5. 此时 r='200 OK',n=1,跳出 consumer 函数,并标记当前跳出点(假设为 B 点),继续执行 c.send (n) 后续代码
print('[PRODUCER] Consumer return: %s' % r)  # 此时 r='200 OK'
# 进入第二轮循环
n = n + 1
print('[PRODUCER] Producing %s...' % n)  # 此时 n=2
r = c.send(n)  # 6. 再次进入生成器,查找上次标记的点 B,并从点 B 开始继续执行代码(此时 n=2)
# 判断 if not n
print('[CONSUMER] Consuming %s...' % n)  # 此时 n=2
# ...
# ...(按照这样的步骤循环,直到 while n < 5 条件不满足)
c.close()  # 结束生成器的生命周期

# asyncio

asyncio 的编程模型就是一个消息循环。从 asyncio 模块中直接获取一个 EventLoop 的引用,然后把需要执行的协程扔到 EventLoop 中执行,就实现了异步 IO。

import asyncio
import threading
async def hello():
    print('Hello! (%s)' % threading.currentThread())
    r = await asyncio.sleep(1)  # 异步调用 asyncio.sleep (1)
    print('Bye-bye! (%s)' % threading.currentThread())
# 获取 EventLoop:
loop = asyncio.get_event_loop()
tasks = [hello(), hello()]
# 执行 coroutine
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

对于这种异步操作,在 3.8 之前的 Python 旧版本中,其书写方式略有不同:

import threading
import asyncio
@asyncio.coroutine
def hello():
    print('Hello! (%s)' % threading.currentThread())
    yield from asyncio.sleep(1)  # 异步调用 asyncio.sleep (1)
    print('Bye-bye! (%s)' % threading.currentThread())
loop = asyncio.get_event_loop()
tasks = [hello(), hello()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

使用这种方式,我们可以进行很多异步 IO,例如网络资源的访问操作:

import asyncio
async def wget(host):
    print('wget %s...' % host)
    connect = asyncio.open_connection(host, 80)
    reader, writer = await connect
    header = 'GET / HTTP/1.0\r\nHost: %s\r\n\r\n' % host
    writer.write(header.encode('utf-8'))
    await writer.drain()
    while True:
        line = await reader.readline()
        if line == b'\r\n':
            break
        print('%s header > %s' % (host, line.decode('utf-8').rstrip()))
    # Ignore the body, close the socket
    writer.close()
loop = asyncio.get_event_loop()
tasks = [wget(host) for host in ['www.sina.com.cn', 'www.sohu.com', 'www.163.com']]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

由示例可见,通过异步 IO,我们可以实现一个线程通过 coroutine 来并发完成多个任务。

如果将 asyncio 用在服务器端,例如 Web 服务器,由于 HTTP 连接就是 IO 操作,因此可以用 单线程 + coroutine 实现多用户的高并发支持。

相关扩展内容:https://realpython.com/async-io-python

# aiohttp

asyncio 实现了 TCP、UDP、SSL 等协议,aiohttp 则是基于 asyncio 实现的 HTTP 框架。

安装 aiohttp :

$ pip install aiohttp

异步创建 TCP 服务:

import asyncio
from aiohttp import web
async def index(request):
    await asyncio.sleep(0.5)
    return web.Response(body=b'<h1>Index</h1>', content_type='text/html')
async def hello(request):
    await asyncio.sleep(0.5)
    text = '<h1>hello, %s!</h1>' % request.match_info['name']
    return web.Response(body=text.encode('utf-8'), content_type='text/html')
async def init():
    app = web.Application()
    app.router.add_get('/', index)
    app.router.add_get('/hello/{name}', hello)
    runner = web.AppRunner(app)
    await runner.setup()
    site = web.TCPSite(runner, '127.0.0.1', 8000)
    await site.start()
    print('Server started at http://127.0.0.1:8000...')
    return site
loop = asyncio.get_event_loop()
loop.run_until_complete(init())
loop.run_forever()

# 项目实践

# 参考学习项目

由于个人能力所限,这里的项目实战主要着重介绍 Python Web 相关项目,如后续有其他 Python 方向,将记录在单独的文章中。

由于手动编写项目实战篇幅较大,而且也费时很长,这里提供几个开源项目进行学习,看懂这些代码,并且能够根据自身需要,对这些代码进行符合自己业务需求的修改,如果你仔细梳理过这些开源项目,相信你对于常规的 Python 项目开发,都能够得心应手。

项目名官网备注
Django-Vue-Adminhttps://django-vue-admin.comWEB 应用,基于 RuoYi
Simple UIhttps://simpleui.72wo.com/simpleuiWEB 应用,博客系统

# 自主开发项目

学习完上面的基础知识,尝试用已掌握的知识开发一个自己的项目吧!

❗TODO 待完善

# 文末总结

跟着廖雪峰的官网教程过了一遍,除去实战部分,大约用了一个月的时间,总体来说感觉难度不是太大,大概是因为有三四年的 Java 开发经验垫底的缘故吧,一些知识上手感觉还是挺快的。但也有些部分是随意跳过了的,对于这些,我都是一旦梳理清了逻辑,就照搬了廖雪峰官网的代码作为记录。但以我自己的经验来看,这应该不是什么大问题,重要在于结合实际项目,零星的知识点太过于孤立,而且当接触了多种开发语言后,单独理解某一两个独立的知识点也是容易引起混淆的。最有效的方法还是学习了这些基础知识后,完完整整地应用到实际的项目中,这不仅可以帮助加深印象,也可以将各个知识点贯穿进行理解。而且在实际项目中,很多问题才会更加直观地呈现出来。

# 参考

  • 独立博客:廖雪峰的官方网站
閲覧数

*~( ̄▽ ̄)~[お茶]を一杯ください

チンモク WeChat 支払う

WeChat 支払う

チンモク Alipay

Alipay

チンモク PayPal

PayPal