![[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 |
布尔类型,表示真或假 | True , False |
|
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_000
和 10000000000
是完全一样的。
² Python还允许用 r''
表示 ''
内部的字符串默认不转义。
³ Python允许用 '''...'''
的格式表示多行内容。同样,它前面也可以加上 r
和 f
执行非转义和便捷格式化。
字符串
关于字符编码,可以阅读另一篇文章「 [[字符集和字符编码]] 」。
在最新的 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
的字符数,如果换成 bytes
,len()
函数就计算字节数。
🔔 在操作字符串时,我们经常遇到 str
和 bytes
的互相转换。为了避免乱码问题,应当始终坚持使用 UTF-8 编码对 str
和 bytes
进行转换。
UTF-8 编码把一个 Unicode 字符根据不同的数字大小编码成 1-6 个字节,常用的英文字母被编码成 1 个字节,汉字通常是 3 个字节,只有很生僻的字符才会被编码成 4-6 个字节。
如何输出格式化的字符串? 和 C 语言是一致的,用 %
实现。
|
|
还有?
使用字符串的 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() → 3 ,lst → [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') → 1 ,d.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') → 1 ,d.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) → 2 ,d → {'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
类型判断,如:
|
|
如果要对 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
列表生成式可以看成是循环生成列表的语法糖 🍬,如:
|
|
如果不想每个元素都转换呢?如何用条件过滤?
|
|
把 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
对象,但 list
、dict
、str
虽然是 Iterable
,却不是 Iterator
。
为什么呢?
这是因为 Python 的 Iterator
对象表示的是一个数据流,可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过 next()
函数实现按需计算下一个数据,所以 Iterator
的计算是惰性的,只有在需要返回下一个数据时它才会计算。
当然,如果要把 list
、dict
、str
等 Iterable
变成 Iterator
可以使用 iter()
函数,如:
>>> isinstance(iter([]), Iterator)
True
>>> isinstance(iter('abc'), Iterator)
True
函数
在上个章节中,我们提到了定义中包含 yield
关键字的函数是一个 generator 函数,那么具体什么是函数呢?
函数就是最基本的一种代码抽象的方式。看看它是怎么定义的,如下:
def 函数名(参数):
函数体
return 返回值 # 若没有 return 语句,默认返回 None
如果想定义一个什么事也不做的空函数,可以用 pass
语句:
|
|
调用函数时,如果参数个数不对,Python 解释器会自动检查出来,并抛出 TypeError
。但是如果参数类型不对,Python 解释器就无法帮我们检查。
数据类型检查可以用内置函数 isinstance()
实现。
|
|
函数可以返回多个值吗? 答案是肯定的。但真的是多个值吗?但其实这只是一种假象,Python 函数返回的仍然是单一值(元组罢了👻)。
函数式编程
函数式编程 就是一种抽象程度很高的编程范式。
纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。
函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!
Python 对函数式编程提供部分支持。由于 Python 允许使用变量,因此,Python 不是纯函数式编程语言。
高阶函数
一个函数可以接收另一个函数作为参数,这种函数就称之为高阶函数。
编写高阶函数,就是让函数的参数能够接收别的函数。下面我们就看几个经典的高阶函数。
❶ map
map()
函数接收两个参数,一个是函数,一个是 Iterable
,map
将传入的函数依次作用到序列的每个元素,并把结果作为新的 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
是一个 Iterator
,Iterator
是惰性序列,因此通过 list()
函数让它把整个序列都计算出来并返回一个 list。
你可能会想,不需要 map()
函数,写一个循环,也可以计算出结果,如下:
|
|
看,事实上 map()
只是把运算规则抽象了。让你一眼就能看明白 “是把 f(x) 作用在 list 的每一个元素并把结果生成一个新的 list ” 。
❷ reduce
reduce
把一个函数作用在一个序列 [x1, x2, x3, ...]
上,这个函数必须接收两个参数,reduce
把结果继续和序列的下一个元素做累积计算,其效果就是:
|
|
❸ 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 函数返回的结果进行排序。
返回函数
高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。
来实现一个可变参数的求和。通常情况下,求和的函数是这样定义的,如下:
|
|
但是,如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办?
可以不返回求和的结果,而是返回求和的函数。
|
|
在这个例子中,我们在函数 lazy_sum
中又定义了函数 sum
,并且,内部函数 sum
可以引用外部函数 lazy_sum
的参数和局部变量,当 lazy_sum
返回函数 sum
时,相关参数和变量都保存在返回的函数中,这种称为 “闭包(Closure)” 的程序结构拥有极大的威力。
注意,当我们调用 lazy_sum()
时,每次调用都会返回一个新的函数,即使传入相同的参数。
🔔 返回闭包时牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量。
|
|
什么情况?全部都是 9
!原因就在于返回的函数引用了变量 i
,但它并非立刻执行。 等到3个函数都返回时,它们所引用的变量 i
已经变成了 3
,因此最终结果为 9
。
如果一定要引用循环变量怎么办?
方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变。
|
|
💡 使用闭包时,对外层变量赋值前,需要先使用 nonlocal 声明该变量不是当前函数的局部变量。
使用闭包,就是内层函数引用了外层函数的局部变量。如果只是读外层变量的值,我们会发现返回的闭包函数调用一切正常。
|
|
但是,如果对外层变量赋值,由于Python解释器会把 x
当作函数 fn()
的局部变量,它会报错。
|
|
原因是 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) 就是一个返回函数的高阶函数。 我们先来定义一个打印日志的装饰器,如下:
|
|
现在我们有一个装饰器 - log()
了,如何使用它呢?借助 Python 的@语法,把 decorator 置于函数的定义处即可。
|
|
现在,我们再来调用一下 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 的写法如下:
|
|
如果,装饰器本身需要传入参数呢? 那就需要 编写一个返回 decorator 的高阶函数。 比如,要自定义 log 的文本:
|
|
现在,我们来调用一个 now()
,如下:
>>> now()
执行函数 now():
2025-01-02
偏函数
所谓偏函数,就是把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。
functools.partial
就是做这个的。
|
|
它就是就相当于替我们实现了下面这样一个函数:
|
|
仅此而已。
模块
在 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 运行环境。
|
|
:: 跟
node_modules
是一样一样的。
附录
python 中获取时间和格式化
|
|
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
参数指定镜像源。
|
|
方法 2:永久设置镜像源
如果你希望永久使用某个镜像源,可以通过修改 pip
的配置文件来实现。
2.1 修改全局配置文件
在终端中运行以下命令,将镜像源写入 pip
的全局配置文件:
|
|
2.2 手动编辑配置文件
-
找到
pip
的配置文件:- Windows:
C:\Users\<你的用户名>\pip\pip.ini
- macOS/Linux:
~/.pip/pip.conf
- Windows:
-
如果没有配置文件,可以手动创建。
-
在配置文件中添加以下内容:
|
|
方法 3:使用环境变量
你可以通过设置环境变量来指定 pip
的镜像源。在命令提示符中运行以下命令:
|
|
常用的国内镜像源
以下是一些常用的国内镜像源,你可以根据自己的需求选择:
镜像源名称 | 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]])。