2024-12-15    2025-01-11    10352 字  21 分钟
OLs

![[assets/Pasted image 20241215124829.png]]

官方文档请参阅 📖 Python 教程 — Python 3.13.1 文档

本语言的命名源自 BBC 的 “Monty Python 飞行马戏团”,与爬行动物无关(Python 原义为“蟒蛇”)。

基础

计算机编程语言和我们日常使用的自然语言有所不同,最大的区别就是,自然语言在不同的语境下有不同的理解,而计算机要根据编程语言执行任务,就必须保证编程语言写出的程序决不能有歧义。所以,任何一种编程语言都有自己的一套语法,编译器或者解释器就是负责把符合语法的程序代码转换成 CPU 能够执行的机器码,然后执行。

数据类型

> Python 中常用的数据类型

类别 数据类型 描述 示例
基本数据类型 int 整数类型,表示整数值 42-10
float 浮点数类型,表示带小数点的数值 3.14-0.001
bool 布尔类型,表示真或假 TrueFalse
str 字符串类型,表示文本数据 "hello"'Python'
复合数据类型 list 列表类型,有序且可变的集合 [1, 2, 3]['a', 'b', 'c']
tuple 元组类型,有序且不可变的集合 (1, 2, 3)('a', 'b', 'c')
set 集合类型,无序且不重复的集合 {1, 2, 3}{'a', 'b', 'c'}
dict 字典类型,键值对的集合 {'name': 'Alice', 'age': 25}
特殊数据类型 NoneType 空值类型,表示没有值 None
bytes 字节类型,表示二进制数据 b'hello'
bytearray 字节数组类型,可变的二进制数据 bytearray(b'hello')
range 范围类型,表示一个不可变的数字序列 range(0, 10)
自定义数据类型 class 类类型,用户自定义的数据结构 class MyClass: pass
object 对象类型,类的实例 obj = MyClass()
其他数据类型 complex 复数类型,表示复数 1 + 2j
enum.Enum 枚举类型,表示一组命名的常量 class Color(Enum): RED = 1
function 函数类型,表示可调用的代码块 def my_function(): pass

你可能不知道的:

¹ Python允许在数字中间以 _ 分隔,因此,写成 10_000_000_00010000000000 是完全一样的。

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

³ Python允许用 '''...''' 的格式表示多行内容。同样,它前面也可以加上 rf 执行非转义和便捷格式化。

字符串

关于字符编码,可以阅读另一篇文章「 [[字符集和字符编码]] 」。

在最新的 Python 3 版本中,字符串是以 Unicode 编码的。对于单个字符的编码,Python 提供了 ord() 函数获取字符的整数表示,chr() 函数把编码转换为对应的字符。

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

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

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

反过来,如果我们从网络或磁盘上读取了字节流,那么读到的数据就是 bytes。要把 bytes 变为 str,就需要用 decode() 方法。

>>> '中文'.encode('utf-8')
b'\xe4\xb8\xad\xe6\x96\x87'

>>> b'\xe4\xb8\xad\xe6\x96\x87'.decode('utf-8')
'中文'

len() 函数计算的是 str 的字符数,如果换成 byteslen() 函数就计算字节数。

🔔 在操作字符串时,我们经常遇到 strbytes 的互相转换。为了避免乱码问题,应当始终坚持使用 UTF-8 编码对 strbytes 进行转换。

UTF-8 编码把一个 Unicode 字符根据不同的数字大小编码成 1-6 个字节,常用的英文字母被编码成 1 个字节,汉字通常是 3 个字节,只有很生僻的字符才会被编码成 4-6 个字节。

如何输出格式化的字符串? 和 C 语言是一致的,用 % 实现。

1
print('%2d-%02d' % (3, 1)) # 3-01  

还有?

使用字符串的 format() 方法,它会用传入的参数依次替换字符串内的占位符 {0}{1} ……

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

🌟 使用以 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

:: 最后一种最 shuhu 👻

str 不能直接和整数比较,必须先把 str 转换成整数。Python提供了 int() 函数来完成这件事情,如果 int() 函数发现一个字符串并不是合法的数字时就会报错,程序就会退出。

:: 不像 JavaScript 那样在比较的时候可以自动转型。

> str 方法一览

方法 描述 示例
capitalize() 将字符串的第一个字符大写 "hello".capitalize() → "Hello"
casefold() 将字符串转换为小写,支持更多语言(如德语) "HELLO".casefold() → "hello"
center(width[, fillchar]) 返回居中后的字符串,width 为总宽度,fillchar 为填充字符(默认为空格) "hi".center(5, '-') → "--hi--"
count(sub[, start[, end]]) ⭐️返回子字符串 sub 在字符串中出现的次数,可选参数 start 和 end 指定范围 "hello".count('l') → 2
encode(encoding='utf-8', errors='strict') 将字符串编码为字节对象 "hello".encode() → b'hello'
endswith(suffix[, start[, end]]) 检查字符串是否以 suffix 结尾,可选参数 start 和 end 指定范围 "hello".endswith('o') → True
expandtabs(tabsize=8) 将字符串中的制表符(\t)替换为空格,tabsize 指定空格数 "hello\tworld".expandtabs(4) → "hello world"
find(sub[, start[, end]]) ⭐️返回子字符串 sub 第一次出现的索引,未找到返回 -1 "hello".find('l') → 2
format(*args, **kwargs) 格式化字符串,替换 {} 中的内容 "{} {}".format("Hello", "World") → "Hello World"
index(sub[, start[, end]]) ⭐️返回子字符串 sub 第一次出现的索引,未找到抛出 ValueError "hello".index('l') → 2
isalnum() 检查字符串是否只包含字母和数字 "hello123".isalnum() → True
isalpha() 检查字符串是否只包含字母 "hello".isalpha() → True
isascii() 检查字符串是否只包含 ASCII 字符 "hello".isascii() → True
isdecimal() 检查字符串是否只包含十进制数字 "123".isdecimal() → True
isdigit() 检查字符串是否只包含数字 "123".isdigit() → True
isidentifier() 检查字符串是否是有效的 Python 标识符 "hello".isidentifier() → True
islower() 检查字符串是否全部为小写 "hello".islower() → True
isnumeric() 检查字符串是否只包含数字字符 "123".isnumeric() → True
isprintable() 检查字符串是否全部为可打印字符 "hello".isprintable() → True
isspace() 检查字符串是否只包含空白字符 " ".isspace() → True
istitle() 检查字符串是否每个单词首字母大写 "Hello World".istitle() → True
isupper() 检查字符串是否全部为大写 "HELLO".isupper() → True
join(iterable) ⭐️将可迭代对象中的元素用字符串连接 ",".join(["a", "b", "c"]) → "a,b,c"
ljust(width[, fillchar]) 返回左对齐后的字符串,width 为总宽度,fillchar 为填充字符 "hi".ljust(5, '-') → "hi---"
lower() 将字符串转换为小写 "HELLO".lower() → "hello"
lstrip([chars]) ⭐️去除字符串左侧的空白字符或指定字符 " hello".lstrip() → "hello"
partition(sep) ⭐️将字符串按 sep 分割为三部分(分隔符前、分隔符、分隔符后) "hello world".partition(' ') → ('hello', ' ', 'world')
replace(old, new[, count]) 将字符串中的 old 替换为 new,可选参数 count 指定替换次数 "hello".replace('l', 'L') → "heLLo"
rfind(sub[, start[, end]]) ⭐️返回子字符串 sub 最后一次出现的索引,未找到返回 -1 "hello".rfind('l') → 3
rindex(sub[, start[, end]]) ⭐️返回子字符串 sub 最后一次出现的索引,未找到抛出 ValueError "hello".rindex('l') → 3
rjust(width[, fillchar]) 返回右对齐后的字符串,width 为总宽度,fillchar 为填充字符 "hi".rjust(5, '-') → "---hi"
rpartition(sep) 将字符串按 sep 从右分割为三部分 "hello world".rpartition(' ') → ('hello', ' ', 'world')
rsplit(sep=None, maxsplit=-1) 从右开始分割字符串,sep 为分隔符,maxsplit 为最大分割次数 "a,b,c".rsplit(',') → ['a', 'b', 'c']
rstrip([chars]) ⭐️去除字符串右侧的空白字符或指定字符 "hello ".rstrip() → "hello"
split(sep=None, maxsplit=-1) 分割字符串,sep 为分隔符,maxsplit 为最大分割次数 "a,b,c".split(',') → ['a', 'b', 'c']
splitlines([keepends]) ⭐️按行分割字符串,keepends 指定是否保留换行符 "hello\nworld".splitlines() → ['hello', 'world']
startswith(prefix[, start[, end]]) ⭐️检查字符串是否以 prefix 开头,可选参数 start 和 end 指定范围 "hello".startswith('he') → True
strip([chars]) ⭐️去除字符串两侧的空白字符或指定字符 " hello ".strip() → "hello"
swapcase() 将字符串中的大小写互换 "Hello".swapcase() → "hELLO"
title() 将字符串中每个单词的首字母大写 "hello world".title() → "Hello World"
translate(table) 根据映射表 table 转换字符串中的字符 "hello".translate(str.maketrans('el', 'EL')) → "hELLo"
upper() 将字符串转换为大写 "hello".upper() → "HELLO"
zfill(width) 在字符串左侧填充 0,直到字符串长度为 width "42".zfill(5) → "00042"

列表和元组

list 是一种有序的集合,可以随时添加和删除其中的元素。另一种有序列表叫元组:tuple。tuple 和 list 非常类似,但是 tuple 一旦初始化就不能修改。

🪧 也就是说,tuple 是个只读状态,什么 append 啦,pop 啦肯定都是不能用的了。只有 1 个元素的 tuple 定义时必须加一个逗号 , ,如 t = (1,)

> 列表 list 方法一览

方法 描述 示例
append(x) 在列表末尾添加元素 x lst = [1, 2]; lst.append(3) → [1, 2, 3]
extend(iterable) 将可迭代对象 iterable 中的所有元素添加到列表末尾 lst = [1, 2]; lst.extend([3, 4]) → [1, 2, 3, 4]
insert(i, x) 在索引 i 处插入元素 x lst = [1, 2]; lst.insert(1, 1.5) → [1, 1.5, 2]
remove(x) 删除列表中第一个值为 x 的元素,如果不存在则抛出 ValueError lst = [1, 2, 2]; lst.remove(2) → [1, 2]
pop([i]) 删除并返回索引 i 处的元素,如果未指定 i,则删除并返回最后一个元素 lst = [1, 2, 3]; lst.pop() → 3lst → [1, 2]
clear() 清空列表中的所有元素 lst = [1, 2]; lst.clear() → []
index(x[, start[, end]]) ⭐️ 返回第一个值为 x 的元素的索引,可选参数 start 和 end 指定搜索范围 lst = [1, 2, 3, 2]; lst.index(2) → 1
count(x) 返回值为 x 的元素在列表中出现的次数 lst = [1, 2, 2, 3]; lst.count(2) → 2
sort(key=None, reverse=False) 对列表进行排序,key 指定排序规则,reverse 控制是否降序 lst = [3, 1, 2]; lst.sort() → [1, 2, 3]
reverse() 反转列表中的元素顺序 lst = [1, 2, 3]; lst.reverse() → [3, 2, 1]
copy() 返回列表的浅拷贝 lst = [1, 2]; lst_copy = lst.copy() → lst_copy 是 [1, 2] 的副本

分支和循环

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

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

字典和集合

Python 内置了字典:dict 的支持,dict 全称 dictionary,在其他语言中也称为 map,使用键-值(key-value)存储,具有极快的查找速度。

正确使用 dict 非常重要,需要牢记的第一条就是 dict 的 key 必须是不可变对象

这是因为dict根据key来计算value的存储位置,如果每次计算相同的key得出的结果不同,那dict内部就完全混乱了。

这个通过key计算位置的算法称为哈希算法(Hash)。

要保证hash的正确性,作为key的对象就不能变。在Python中,字符串、整数等都是不可变的,因此,可以放心地作为key。而list是可变的,就不能作为key。

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

> 字典 dict 的常用方法

方法 描述 示例
clear() 清空字典中的所有键值对 d = {'a': 1}; d.clear() → {}
copy() 返回字典的浅拷贝 d = {'a': 1}; d_copy = d.copy() → d_copy 是 {'a': 1} 的副本
fromkeys(seq[, value]) 创建一个新字典,seq 为键,value 为值(默认为 None dict.fromkeys(['a', 'b'], 1) → {'a': 1, 'b': 1}
get(key[, default]) 返回键 key 对应的值,如果键不存在则返回 default(⭐️ 默认为 None d = {'a': 1}; d.get('a') → 1d.get('b', 0) → 0
items() 返回字典中所有键值对的视图((key, value) 对) d = {'a': 1}; d.items() → dict_items([('a', 1)])
keys() 返回字典中所有键的视图 d = {'a': 1}; d.keys() → dict_keys(['a'])
values() 返回字典中所有值的视图 d = {'a': 1}; d.values() → dict_values([1])
pop(key[, default]) 删除并返回键 key 对应的值,如果键不存在且未提供 default 则抛出 KeyError d = {'a': 1}; d.pop('a') → 1d.pop('b', 0) → 0
popitem() 删除并返回字典中的最后一对键值对,如果字典为空则抛出 KeyError d = {'a': 1}; d.popitem() → ('a', 1)
setdefault(key[, default]) 如果键 key 存在则返回其值,否则插入 key 并设置值为 default(默认为 None d = {'a': 1}; d.setdefault('b', 2) → 2d → {'a': 1, 'b': 2}
update([other]) 将字典 other 中的键值对更新到当前字典中 d = {'a': 1}; d.update({'b': 2}) → {'a': 1, 'b': 2}
__contains__(key) ⭐️检查字典中是否包含键 key(通常用 in 操作符) d = {'a': 1}; 'a' in d → True

切片、迭代、生成器

在 Python 中,代码不是越多越好,而是越少越好。代码不是越复杂越好,而是越简单越好。

❶ 切片

取一个 str、list 或 tuple 的部分元素是非常常见的操作。对这种经常取指定索引范围的操作,用循环十分繁琐,因此,Python 提供了切片(Slice)操作符。

操作 描述 示例
sequence[start:stop] 从 start 到 stop-1 的子序列 "hello"[1:4] → "ell"
sequence[start:] 从 start 到序列末尾的子序列 "hello"[2:] → "llo"
sequence[:stop] 从序列开头到 stop-1 的子序列 "hello"[:3] → "hel"
sequence[::step] 从序列开头到末尾,按 step 步长提取子序列 "hello"[::2] → "hlo"
sequence[::-1] 反转序列 "hello"[::-1] → "olleh"
sequence[start:stop:step] 从 start 到 stop-1,按 step 步长提取子序列 "hello"[1:5:2] → "el"

❷ 迭代

Python 中只要是可迭代对象,无论有无下标,都可以迭代。它是通过 for ... in 来完成的,而很多语言比如 C 语言,迭代 list 是通过下标完成的。

默认情况下,dict 迭代的是 key。如果要迭代 value,可以用 for value in d.values(),如果要同时迭代 key 和 value,可以用 for k, v in d.items()

如何判断一个对象是可迭代对象呢? 方法是通过 collections.abc 模块的 Iterable 类型判断,如:

1
2
3
4
from collections.abc import Iterable

isinstance('abc', Iterable) # → True
isinstance(123, Iterable)   # → False

如果要对 list 实现类似 C 那样的下标循环怎么办?👻

Python 内置的 enumerate 函数可以把一个 list 变成索引-元素对,这样就可以在 for 循环中同时迭代索引和元素本身。

>>> for i, value in enumerate(['A', 'B', 'C']):
...     print(i, value)
...
0 A
1 B
2 C

❸ 列表生成式

i.e.List Comprehensions

列表生成式可以看成是循环生成列表的语法糖 🍬,如:

1
2
3
4
5
6
7
list = list(range(0, 9))     # [0, 1, 2, 3, 4, 5, 6, 7, 8]
L = [x * x for x in list]    # [0, 1, 4, 9, 16, 25, 36, 49, 64]

# 等同于
L = []
for x in list:
    L.append(x * x)

如果不想每个元素都转换呢?如何用条件过滤?

1
2
3
4
5
6
7
8
9
L = [x * x for x in list if x % 2 == 0] # [0, 4, 16, 36, 64]

# 等同于
for x in list:
    if x % 2 == 0:
        L.append(x * x)
        
L = [x * x if x % 2 for x in list]        # ❌❓
L = [x * x if x % 2 else 0 for x in list] # ✔️

if 写在 for 前面必须加 else,否则报错。 为什么呢?

这是因为 for 前面的部分是一个表达式,它必须根据 x 计算出一个结果。因此,考察表达式:x if x % 2 == 0,它无法根据 x 计算出结果,因为缺少 else,必须加上 else

❹ 生成器

通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。那么,可不可以在循环的过程中不断推算出后续的元素呢? 这样就不必创建完整的 list,从而节省大量的空间。

一边循环一边计算的机制,称为生成器(generator) 。那么,如何创建一个 generator 呢?

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

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

可以通过 next() 函数获得 generator 的下一个返回值,但正确的方法是使用 for 循环。

² 如果一个函数定义中包含 yield 关键字,那么这个函数就不再是一个普通函数,而是一个 generator 函数,调用一个 generator 函数将返回一个 generator 。

想要了解更多关于生成器的内容,可以阅读另一篇 「 [[生成器]] 」原理都是相通的。

❺ 迭代器

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

🪧 生成器都是 Iterator 对象,但 listdictstr 虽然是 Iterable,却不是 Iterator

为什么呢?

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

当然,如果要把 listdictstrIterable 变成 Iterator 可以使用 iter() 函数,如:

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

函数

在上个章节中,我们提到了定义中包含 yield 关键字的函数是一个 generator 函数,那么具体什么是函数呢?

函数就是最基本的一种代码抽象的方式。看看它是怎么定义的,如下:

def 函数名(参数):
    函数体
    return 返回值    # 若没有 return 语句,默认返回 None

如果想定义一个什么事也不做的空函数,可以用 pass 语句:

1
2
def nop():
    pass

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

数据类型检查可以用内置函数 isinstance() 实现。

1
2
3
4
5
6
7
def my_abs(x):
    if not isinstance(x, (int, float)):      # 类型检查
        raise TypeError('bad operand type')
    if x >= 0:
        return x
    else:
        return -x

函数可以返回多个值吗? 答案是肯定的。但真的是多个值吗?但其实这只是一种假象,Python 函数返回的仍然是单一值(元组罢了👻)。

函数式编程

函数式编程 就是一种抽象程度很高的编程范式。

纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。

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

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

高阶函数

一个函数可以接收另一个函数作为参数,这种函数就称之为高阶函数。

编写高阶函数,就是让函数的参数能够接收别的函数。下面我们就看几个经典的高阶函数。

map

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

>>> def f(x):
...     return x * x
...
>>> r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])  # r 是一个迭代器
>>> list(r)
[1, 4, 9, 16, 25, 36, 49, 64, 81]

map() 传入的第一个参数是 f,即函数对象本身。

由于结果 r 是一个 IteratorIterator 是惰性序列,因此通过 list() 函数让它把整个序列都计算出来并返回一个 list。

你可能会想,不需要 map() 函数,写一个循环,也可以计算出结果,如下:

1
2
3
4
L = []
for n in [1, 2, 3, 4, 5, 6, 7, 8, 9]:
    L.append(f(n))
print(L)

看,事实上 map() 只是把运算规则抽象了。让你一眼就能看明白 “是把 f(x) 作用在 list 的每一个元素并把结果生成一个新的 list ” 。

reduce

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

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

filter

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

可见用 filter() 这个高阶函数,关键在于正确实现一个“筛选”函数。

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

sorted

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

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

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

sorted() 函数也是一个高阶函数,它还可以接收一个 key 函数来实现自定义的排序,例如按绝对值大小排序:

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

key 指定的函数将作用于 list 的每一个元素上,并根据 key 函数返回的结果进行排序。

返回函数

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

来实现一个可变参数的求和。通常情况下,求和的函数是这样定义的,如下:

1
2
3
4
5
6
7
def calc_sum(*args):
    ax = 0
    for n in args:
        ax = ax + n
    return ax
    
calc_sum(1,2,3,4,5)  # → 15

但是,如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办?

可以不返回求和的结果,而是返回求和的函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def lazy_sum(*args):
    def sum():
        ax = 0
        for n in args:
            ax = ax + n
        return ax
    return sum
    
f = lazy_sum(1,2,3,4,5) # → <function lazy_sum.<locals>.sum at 0x101c6ed90>
f()                     # → 15

在这个例子中,我们在函数 lazy_sum 中又定义了函数 sum,并且,内部函数 sum 可以引用外部函数 lazy_sum 的参数和局部变量,当 lazy_sum 返回函数 sum 时,相关参数和变量都保存在返回的函数中,这种称为 “闭包(Closure)” 的程序结构拥有极大的威力。

注意,当我们调用 lazy_sum() 时,每次调用都会返回一个新的函数,即使传入相同的参数。

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def count():
    fs = []
    for i in range(1, 4):
        def f():
             return i*i
        fs.append(f)
    return fs

f1, f2, f3 = count()

f1() # → 9
f2() # → 9
f3() # → 9

什么情况?全部都是 9!原因就在于返回的函数引用了变量 i,但它并非立刻执行。 等到3个函数都返回时,它们所引用的变量 i 已经变成了 3,因此最终结果为 9

如果一定要引用循环变量怎么办?

方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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() # → 1
f2() # → 4
f3() # → 9

💡 使用闭包时,对外层变量赋值前,需要先使用 nonlocal 声明该变量不是当前函数的局部变量。

使用闭包,就是内层函数引用了外层函数的局部变量。如果只是读外层变量的值,我们会发现返回的闭包函数调用一切正常。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def inc():
    x = 0
    def fn():
        # 仅读取x的值:
        return x + 1
    return fn

f = inc()
print(f()) # 1
print(f()) # 1

但是,如果对外层变量赋值,由于Python解释器会把 x 当作函数 fn() 的局部变量,它会报错。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def inc():
    x = 0
    def fn():
        # nonlocal x
        x = x + 1
        return x
    return fn

f = inc()
print(f()) # 1
print(f()) # 2

原因是 x 作为局部变量并没有初始化,直接计算 x+1 是不行的。但我们其实是想引用 inc() 函数内部的 x,所以需要在 fn() 函数内部加一个 nonlocal x 的声明。加上这个声明后,解释器把 fn()x 看作外层函数的局部变量,它已经被初始化了,可以正确计算 x+1

匿名函数

lambda x: x * x 就是一个匿名函数,关键字 lambda 表示匿名函数,冒号前面的 x 表示函数参数。

它没有函数名,所以不用担心命名冲突。只有一个表达式,不用 return ,返回值就是表达式的结果。

装饰器

本质上,装饰器(decorator) 就是一个返回函数的高阶函数。

:: 其实就是代理。

假设,我们有一个 now 函数,如下:

def now():
    print('2025-01-02')
    
now()         # → 2025-01-02
now.__name__  # → 'now'

现在我们要增强 now() 的功能,如在函数调用前后打印日志,但又不希望修改 now() 函数的定义。怎么办呢?

通过装饰器!这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。

本质上,装饰器(decorator) 就是一个返回函数的高阶函数。 我们先来定义一个打印日志的装饰器,如下:

1
2
3
4
5
def log(func):
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper

现在我们有一个装饰器 - log() 了,如何使用它呢?借助 Python 的@语法,把 decorator 置于函数的定义处即可。

1
2
3
@log
def now():
    print('2024-6-1')

现在,我们再来调用一下 now() 函数,在调用函数之前就会输出调用的函数名这条日志了。

>>> now()
call now():
2024-6-1

>>> now.__name__
wrapper    # ❓ 变成 wrapper 了

@log 放到 now() 函数的定义处,相当于执行了语句:now = log(now)

看,现在调用的 now 不再是原来的 now 了,而是 log 函数返回的同名函数。不信你看,函数的 __name__ 属性变成了 wrapper

所以,需要把原始函数的 __name__ 等属性复制到 wrapper() 函数中,否则,有些依赖函数签名的代码执行就会出错。

不需要编写 wrapper.__name__ = func.__name__ 这样的代码,Python 内置的 functools.wraps 就是干这个事的,所以,一个完整的 decorator 的写法如下:

1
2
3
4
5
6
7
8
import functools

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

如果,装饰器本身需要传入参数呢? 那就需要 编写一个返回 decorator 的高阶函数。 比如,要自定义 log 的文本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import functools

# 定义 log
def log(text):
    def decorator(func):
       @functools.wraps(func)
       def wrapper(*args, (**kw)):
           print('%s %s():' % (text, func.__name__))
           return func(*args, **kw)
        return wrapper
    return decorator
    
# 使用 log
@log('执行函数')
def now():
    print('2025-01-02)

现在,我们来调用一个 now() ,如下:

>>> now()
执行函数 now():
2025-01-02

偏函数

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

functools.partial 就是做这个的。

1
2
3
4
import functools

int2 = functools.partial(int, base=2)
int2('1000000')  # → 64

它就是就相当于替我们实现了下面这样一个函数:

1
2
def int2(x, base=2):
  return int(x, base)

仅此而已。

模块

在 Python 中,一个 .py 文件就称之为一个模块(Module)

使用模块,大大提高了代码的可维护性,还可以函数名和变量名冲突。为了避免模块名冲突,Python 又引入了按目录来组织模块的方法,称为包(Package)

引入了包以后,只要顶层的包名不与别人冲突,那所有模块都不会与别人冲突。

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

每一个包目录下面都会有一个 __init__.py 的文件,这个文件是必须存在的,否则,Python就把这个目录当成普通目录,而不是一个包。

__init__.py 可以是空文件,也可以有 Python 代码,因为 __init__.py 本身就是一个模块,而它的模块名就是 mycompany

abc.py 模块的名字就变成了 mycompany.abc,类似的,xyz.py 的模块名变成了 mycompany.xyz

类似的,可以有多级目录,组成多级层次的包结构。

常用内建模块

venv

默认情况下,所有第三方的包都会被 pip 安装到Python3的 site-packages 目录下。

如果要同时开发多个应用程序,应用 A 需要 jinja 2.7,而应用 B 需要 jinja 2.6 怎么办?

这种情况下,每个应用可能需要各自拥有一套“独立”的 Python 运行环境。venv 就是用来为一个应用创建一套“隔离”的 Python 运行环境。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# ① 创建项目目录
jack@jk:/d/wp$ mkdir pyvenv
jack@jk:/d/wp$ cd pyvenv/
# ② 创建一个独立的 Python 运行环境
jack@jk:/d/wp/pyvenv$ python3 -m venv .
jack@jk:/d/wp/pyvenv$ ls
Include  Lib  Scripts  pyvenv.cfg
# ③ 激活 venv 环境
jack@jk:/d/wp/pyvenv$ source Scripts/activate

# ④ 激活后命令提示符多了 (pyvenv) 前缀
# ..下面可正常安装各种第三方包
# ..安装的包都被安装到当前 pyvenv 这个环境下
# ../d/wp/pyvenv/Lib/site-packages 目录中
(pyvenv) jack@jk:/d/wp/pyvenv$ pip3 install jinja2
Collecting jinja2
...
Successfully installed MarkupSafe-3.0.2 jinja2-3.1.5
(pyvenv) jack@jk:/d/wp/pyvenv$ python
Python 3.13.1 (tags/v3.13.1:0671451, Dec  3 2024, 19:06:28) [MSC v.1942 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import jinja2
>>> exit()
# ⑤ 退出当前的 pyvenv 环境
(pyvenv) jack@jk:/d/wp/pyvenv$ cd Scripts/
(pyvenv) jack@jk:/d/wp/pyvenv/Scripts$ deactivate

:: 跟 node_modules 是一样一样的。

附录

python 中获取时间和格式化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from datetime import datetime

# 获取当前日期
current_date = datetime.now().date()

# 格式化日期
formatted_date = current_date.strftime("%Y年%m月%d日")
print("格式化后的日期:", formatted_date)

# 其他格式
formatted_date_2 = current_date.strftime("%A, %d %B %Y")
print("另一种格式:", formatted_date_2)

# » 格式化后的日期: 2023年10月05日
# » 另一种格式: Thursday, 05 October 2023

pyenv 版本管理

首先,通过 Powershell 安装 pyenv-win ,如下:

Invoke-WebRequest -UseBasicParsing -Uri "https://raw.githubusercontent.com/pyenv-win/pyenv-win/master/pyenv-win/install-pyenv-win.ps1" -OutFile "./install-pyenv-win.ps1"; &"./install-pyenv-win.ps1"

然后,重新打开终端,运行 pyenv --version 检查是否安装成功。注意,你总是可能通过 pyenv help 了解更多。

更多详情参考 ➭ pyenv-win installation

PyPI 换源

pypi | 镜像站使用帮助 | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror

在使用 Python 的 pip 安装包时,默认的包下载源是国外的 PyPI(Python Package Index)服务器,可能会因为网络问题导致下载速度慢或失败。为了提高下载速度,可以将 pip 的镜像源设置为国内的镜像站点。

以下是设置 pip 镜像的几种方法:

方法 1:临时使用镜像源

如果你只想临时使用某个镜像源,可以在使用 pip 命令时添加 -i 参数指定镜像源。

1
2
# 用法 pip install <package_name> -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install requests -i https://pypi.tuna.tsinghua.edu.cn/simple

方法 2:永久设置镜像源

如果你希望永久使用某个镜像源,可以通过修改 pip 的配置文件来实现。

2.1 修改全局配置文件

在终端中运行以下命令,将镜像源写入 pip 的全局配置文件:

1
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

2.2 手动编辑配置文件

  1. 找到 pip 的配置文件:

    • WindowsC:\Users\<你的用户名>\pip\pip.ini
    • macOS/Linux~/.pip/pip.conf
  2. 如果没有配置文件,可以手动创建。

  3. 在配置文件中添加以下内容:

1
2
[global]
index-url = https://pypi.tuna.tsinghua.edu.cn/simple

方法 3:使用环境变量

你可以通过设置环境变量来指定 pip 的镜像源。在命令提示符中运行以下命令:

1
2
3
4
5
# Windows
set PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple

# macOS/Linux
export PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple

常用的国内镜像源

以下是一些常用的国内镜像源,你可以根据自己的需求选择:

镜像源名称 URL
清华大学 https://pypi.tuna.tsinghua.edu.cn/simple
阿里云 https://mirrors.aliyun.com/pypi/simple/
中国科技大学 https://pypi.mirrors.ustc.edu.cn/simple/
华为云 https://repo.huaweicloud.com/repository/pypi/simple

总结

  • 临时使用:使用 -i 参数。
  • 永久使用:修改 pip 配置文件或设置环境变量。
  • 推荐镜像源:清华大学、阿里云、中国科技大学等。

通过设置镜像源,可以显著提高 pip 的下载速度,避免因网络问题导致的安装失败。

切片

可以这样理解切片,索引指向的是字符之间,第一个字符的左侧标为 0,最后一个字符的右侧标为 n ,n 是字符串长度。例如:

 +---+---+---+---+---+---+
 | P | y | t | h | o | n |
 +---+---+---+---+---+---+
 0   1   2   3   4   5   6
-6  -5  -4  -3  -2  -1

斐波那契数列

> 以斐波那契数为边的正方形拼成的近似的黄金矩形 (1:1.618)

![[assets/Pasted image 20241223155202.png|325]]

在数学上,斐波那契数是以递归的方法来定义:

![[assets/Pasted image 20241223155423.png|200]]

用白话文来说,就是斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。首几个斐波那契数是:

1、 1、 2、 3、 5、 8、 13、 21、 34、 55、 89、 144、 233、 377、 610、 987……

特别指出:0 不是第一项,而是第零项( ![[assets/Pasted image 20241223155936.png]])。