「笔记」Python 杂记
写在前面
自用向随意整理。
使用 python 版本 3.9.12。
主要参考 Python教程 - 廖雪峰的官方网站 与 Python 内置注释。
基础
杂项
- Python 对缩进敏感,不同的代码块使用缩进进行标记。
- 空语句
pass
。 - 单行注释:
#
,多行注释为三个单引号:''' something useful or useless '''
r'...'
表示内部字符串默认不转义。- Python 运算自带高精度。
- 除法
/
计算结果是浮点数,即使是两个整数恰好整除,结果也是浮点数。实现整数除法可使用//
,商会向下取整。 - Bool 类型对应的常量为
True
和False
。 - 单引号与双引号等价,因为无论字面串有多少字符都会被解释为
str
类型。 input()
的返回值类型为str
,如果想要读入数字则需进行类型转换,如:a = int(input())
。- 格式化的三种方式:
'Hi, %s, you have $%d.' % ('Michael', 1000000)
'Hello, {0}, 成绩提升了 {1:.1f}%'.format('小明', 17.125)
,传入的参数依次替换字符串内的占位符。print(f'The area of a circle with radius {r} is {s:.2f}')
,r
和s
是两个变量,且s
必须是浮点型的变量。- 使用变量的值指定输出格式:
困扰我许久的问题终于解决了!在写 cpp 的时候遇到这种诡异的需求都还要自己手写输出= =a = 2 s = 1.2345 print(f'%.{a}lf' % 1.2345) print(f'{s:.{a}f}')
- 没有自增和自减运算符。原因可参考:浅谈:为什么python没有自增运算符? - 腾讯云开发者社区。
- 与 C 非常不同的一点:赋值语句总是建立对象的引用值,而不是复制对象。python 变量更像是指针,而不是数据存储区域。
输出为:a = [1, 2, 3] b = a a[0] = 2 print(a) print(b) a = [3, 2, 1] print(a) print(b)
如果想要创建独立于某个对象的副本需要使用[2, 2, 3] [2, 2, 3] [3, 2, 1] [2, 2, 3]
copy()
函数:
输出为:a = [1, 2, 3] b = a.copy() a[0] = 2 print(a) print(b)
[2, 2, 3] [1, 2, 3]
集合数据类型
list
:[]
。.append(x)
、.pop()
、.insert(pos, x)
。
tuple
:()
,每个元素指向永远不变,但元素的值可以改变。- 例:
([1,2], [3,4])
中两个 list 可以改变。 - 一个元素的 tuple:
(x,)
。 - 多个变量赋值可以这样:
a, b, c = 1, 2, 3
,本质上是创建了t = (1, 2, 3)
,然后使a = t[0]
,b = t[1]
,c = t[2]
。
- 例:
dict
:{key : value}
,建立 key 到 value 的映射关系。d[key] = value
、d.pop(key)
。- 建立映射关系时 key 必须是 hashable 的,即不可变的数据结构(如 str、tuple、objects)。
d.get(x, y)
:d 中不存在 x 这个 key 时返回 y,否则返回 x 对应的 value。- 空间换时间,大数据范围下查找插入效率高于 list。
set
:set([key])
,不可重 key 集。- 创建时提供一个 list。
- 自动去重并排序。
s.add(key)
,s.remove(key)
。- 多个 set 间可做数学上的
|
、&
运算。
- 对于不变对象来说,调用对象自身的任意方法不会改变对象自身的内容,而是会创建新的对象并返回。这样,就保证了不可变对象本身永远是不可变的。例:
a = 'abc
,b = a.replace('a', 'A')
,对象a
仍为'abc'
。 enumerate
:用于方便地同时获取可枚举对象中的索引值以及索引值对应的列表中的元素。
输出为:a = ['a', 'b', 'c'] b = 'abc' for i, x in enumerate(a): print(i, x) for i, x in enumerate(b): print(i, x)
0 a 1 b 2 c 0 a 1 b 2 c
可变对象与不可变对象
-
可变对象:对象指向的内存中的值会改变,当更改这个变量的时候,还是指向原来内存中的值,并且在原来的内存值进行原地修改,并没有开辟新的内存(list、dict、set)。
-
不可变对象:对象所指向的内存中的值不能被改变,当改变这个变量的时候,原来指向的内存中的值不变,变量不再指向原来的值,而是开辟一块新的内存,变量指向新的内存(int、float、str、tuple、bool、None)。
函数
定义与调用
- 函数名其实就是指向一个函数对象的引用,完全可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”。
- 参数个数不对将抛出
TypeError
,参数类型不对无法自动检查,程序仍会执行,可自定义数据类型检查:def my_abs(x): if not isinstance(x, (int, float)): raise TypeError('bad operand type') if x >= 0: return x else: return -x
- 执行完没有返回值时自动
return None
。 - 返回值实际上是一个 tuple,因此可返回多个值。
参数
- 参数定义顺序必须为:必选参数(如
a
)、默认参数(如b = 1
)、可变参数(如*c
)、命名关键字参数(如d
、d = 2
)和关键字参数(如**e
)。- 如:
def f1(a, b, c=0, *args, **kw):
。 - 又如:
def f2(a, b, c=0, *, d, **kw):
。 - 不存在可变参数时需要用
*,
来占位。
- 如:
- 位置参数:实参按照位置顺序依次赋给位置参数。
- 默认参数:如
def power(x, n = 2):
,没有实参则取默认值。- 如果默认参数是一个可变对象:如果在调用该函数时改变默认参数的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的了。如:
输出为:def add_end(L = []): L.append('END') return L print(add_end()) print(add_end())
['END'] ['END', 'END']
- 原因:与 C 不同,函数及其局部变量都有固定的内存空间。
- 一个改进的例子,令默认参数指向不可变对象,在函数体中再赋默认值:
def add_end(L = None): if L is None: L = [] L.append('END') return L
- 定义默认参数要牢记一点:默认参数必须指向不变对象。
- 如果默认参数是一个可变对象:如果在调用该函数时改变默认参数的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的了。如:
- 可变参数:
def calc(*numbers):
,允许传入任意个参数(包括 0 个),将传入的参数顺次组装成一个 tuple。- 如果要传入一个 list 或者 tuple,实参前加
*
:calc(*nums)
。
- 如果要传入一个 list 或者 tuple,实参前加
- 关键字参数:
def person(name, age, **kw):
,允许传入任意个key=value
的参数对(包括 0 个),将传入的参数顺次组装成一个 dict。- 如果要传入一个 dict:
person('Aliemo', 24, **extra)
。
- 如果要传入一个 dict:
- 命名关键字参数:
def person(name, age, *, city, job):
,使关键字参数的 key 为给定值。- 与位置参数不同的是,调用时必须传入参数名,否则会被解释为位置参数。调用方式:
person('Jack', 24, city='Beijing', job='Engineer')
- 可以有缺省值:
def person(name, age, *, city='Beijing', job):
- 与位置参数不同的是,调用时必须传入参数名,否则会被解释为位置参数。调用方式:
- 对于任意函数,都可以通过类似
func(*args, **kw)
的形式调用,其中args
是一个 tuple,kw
是一个 dict。args
与kw
中的元素会按照参数定义的顺序传入。
文档字符串
- 在函数定义的代码块的开头输入的字符串,用于对函数的功能进行说明。
- 可以通过
help
函数来进行访问,以便于代码的阅读和调试:
输出为:def f3(x): ''' Return the cube of the argument. Args: x(int) Returns: int: the cube of x. ''' return x*x*x help(f3) print(f3(-2))
Help on function f3 in module __main__: f3(x) Return the cube of the argument. Args: x(int) Returns: int: the cube of x. -8
- 许多 IDE 支持文档字符串的快速浏览,比如在我的 vscode 上,只需要把鼠标移动到该函数名上:
真是太方便啦!
高级特性
切片
我还年轻,不想看 V,2022.10.8 upd 一下,昨晚京糖的瓜直接给我看麻了,还是少玩点网吧家人们。- 用于快速取出 list、tuple 和 字符串的指定子集。
- 对于一个 list:
L = [...]
:L[0:3]
:取出从索引 0 到索引 2 对应的元素,并组成一个 list。L[:3]
:功能同上,第一个空默认为 0。L[-2:-1]
:倒数第 1 个元素的索引为 -1。取出倒数第 2 个元素,并组成一个 list。L[-2:]
:功能同上,第二个空默认为 -1。L[:10:2]
:前十个数,每隔两个取一个数。
- 对于一个 tuple,操作同上,结果是一个 tuple。
- 对于一个字符串,操作同上,结果是一个字符串。
迭代
- 用
for
遍历一个可迭代(Iterable)对象的过程,称为迭代。 - 迭代一个 dict:
for key in d:
,迭代 key。for value in d.values()
,迭代 value。for k,v in d.items()
,迭代整体,k 为 key,v 为 value。或者可以写成:for i in d.items()
,此时i
是一个由 key 和 value 组成的 tuple。
for
中可以引用多个变量,如:for x, y in [(1, 1), (2, 4), (3, 9)]:
列表生成式
-
简单而强大地创建 list 的方式。
-
简单的示例:
[x * x for x in range(1, 11)]
:1~10 的平方。[m + n for m in 'ABC' for n in 'XYZ']
:两字符串中的字母两两组合生成的字符串。[s.lower() for s in L]
:将L
中所有字符串 s 中的字母都变为小写。
-
使用
if
和else
进行筛选。- 当
if
写在for
之后,不能出现else
。表示先生成所有的元素,再筛出符合条件的元素。如:[x * x for x in range(1, 11) if x % 2 == 0]
:1~10 中偶数的平方。 - 当
if
写在for
之前时,必须搭配else
使用。表示进行迭代同时进行判断,若if
成立则按照正常的方式生成元素,否则按照else
之后的方式生成元素。如:[x if x % 2 == 0 else -x for x in range(1, 11)] == [-1, 2, -3, 4, -5, 6, -7, 8, -9, 10]
。
- 当
生成器(Generator)
-
给定一个序列推算的算法,使得可以通过循环不断推算出后续元素。这种一边循环一边计算的机制,称为生成器:generator。
-
创建方法 1:将列表生成式的
[]
改为()
。- 如:
g = (x * x for x in range(1, 11))
。 - 初始时
g
没有值。调用next
函数可使g
生成下一个元素,next
函数的返回值为g
生成出的当前元素。没有更多元素时抛出错误StopIteration
。 - 可使用
for
迭代:for n in g:
迭代完成后g
生成完了所有元素,再调用next(g)
时将报错。
- 如:
-
更复杂的创建方法:将现有有返回值的函数改为生成器。
- 使用
yield
返回下一个元素。变成 generator 的函数,在每次调用next()
的时候执行,遇到yield
语句返回,再次执行时从上次返回的yield
语句处继续执行。 - 一个简单的例子:
def func(): for x in range(1, 11): yield x * x g = func()
- 一个复杂的例子,生成指定长度的斐波那契数列:
def fib(n): a, b = 0, 1 for i in range(1, n + 1): yield b a, b = b, a + b g = fib(10) #生成前十项
- 调用 generator 函数
func()
会创建一个 generator 对象,多次调用 generator 函数会创建多个相互独立的 generator。- 多次调用
next(func())
的值均为这个 generator 的第一个值。 - 可以通过对一个对象重复调用函数来重置这个对象,但这样会造成时间效率的降低,可在枚举生成器的元素时顺便将元素存入 list 中空间换时间。
- 多次调用
- 使用
-
一个更复杂的例子,输出指定行数的杨辉三角:
def F(max): n = [1] for i in range(0, max): yield n l = len(n) n = [1] + [n[i] + n[i + 1] for i in range(0, l - 1)] + [1] f = F(10) for i in f: print(i)
迭代器(Iterator)
-
可以直接作用于
for
的对象统称为可迭代(Iterable)对象。 -
可以使用
isinstance()
判断一个对象是否是 Iterable 对象。>>> from collections.abc import Iterable >>> isinstance([], Iterable) True >>> isinstance({}, Iterable) True >>> isinstance('abc', Iterable) True >>> isinstance((x for x in range(10)), Iterable) True >>> isinstance(100, Iterable) False
-
可以被
next()
调用并不断返回下一个值的对象称为迭代器:Iterator。类似地可以使用isinstance()
判断:isinstance((x for x in range(10)), Iterator)
-
生成器都是 Iterator,list、dict、str 是 Iterable 的,但不是 Iterator。
-
可使用
iter()
将 Iterable 转化为 Iterator。如:iter([1, 2])
。 -
可能会有这样的疑问,为什么 list、dict、str 等数据类型不是 Iterator?
这是因为 Python 的 Iterator 对象表示的是一个数据流,Iterator 对象可以被next()
函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration
错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()
函数实现按需计算下一个数据,所以 Iterator 的计算是惰性的,只有在需要返回下一个数据时它才会计算。
Iterator 甚至可以表示一个无限大的数据流,例如全体自然数。而使用 list 是永远不可能存储全体自然数的。 -
list()
函数:将传入的 Iterator 遍历一遍,将其中所有元素存入一个 list 并返回。调用后的 Iterator 生成完了所有元素。def func(): for x in range(1, 11): yield x * x g = list(func()) print(g)
输出为:
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
。
函数式编程
Python 的函数有一些奇技淫巧:
- 变量可以指向函数:如令
f = abs()
,调用f(x)
与调用abs(x)
等价。 - 函数名也是变量:举个有些怪的例子,可以令
abs = max
,之后就可以用abs
取最大值了,不过除非喝了假酒不会有人这么写的。 - 可以将函数名作为参数传入函数,如:
def f(x): return x * x def g(x, y, h): return h(x) + h(y) print(g(1, 2, f))
- 一个能够接收其它函数作为参数的函数称为高阶函数。
- 函数式编程就是指这种高度抽象的编程范式。
- 感觉比 C 牛逼了十倍甚至九倍。
map 与 reduce
- map:传入一个函数
f
与不少于一个list,list
的个数与f
的参数个数相同。之后依次将每个 list 相同位置的元素作为参数同时传入f
,按传入的顺序创建一个结果为函数f
返回值的 Iterator。- 一个简单的例子,其结果为 1~9 的平方:
def f(x): return x * x r = map(f, range(1, 10)) print(list(r))
- 另一个简单的例子,将数字转化为对应的字符
r = map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9]) print(list(r))
- 另一个简单的例子,取出三个字符串相同位置的字符并重新拼接。其输出结果为
'adg'
、'beh'
、'cfi'
:def f(x, y, z): return x + y + z r = map(f, 'abc', 'def', 'ghi') print(list(r))
- 一个简单的例子,其结果为 1~9 的平方:
- reduce:传入一个两个参数的函数
f
与一个 Iterable 对象。之后按照顺序遍历 Iterable 对象,先计算出前两个元素调用f
得到的结果,然后把结果作为参数继续和 Iterable 对象的下一个元素做累计计算,最后得到一个元素作为结果。- 使用前需要在源代码开头添加:
from functools import reduce
。import
表示导入模块,模块的概念将在后文中补充。 - 直观地理解:
reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)
- 一个没什么实际用处的例子,数列求和:
def my_sum(x, y): return x + y r = reduce(my_sum, [1, 2, 3, 4]) print(r)
- 一个也没有没什么实际用处的例子,翻转字符串:
def my_reverse(x, y): return y + x r = reduce(my_reverse, 'abcdef') print(r)
- 使用前需要在源代码开头添加:
- 一个综合的运用,将数字字符串转化为对应的数字:
from functools import reduce DIGIT = {'0':0, '1':1, '2':2, '3':3, '4':4, '5':5, '6':6, '7':7, '8':8, '9':9} def d(x): return DIGIT[x] def f(x, y): return 10 * x + y r = reduce(f, map(d, '114514')) print(r)
filter
-
用于筛选一个 Iterable 对象,并返回一个能计算出了筛选后的对象的 Iterator。
-
接收一个返回值为 True 或者 False 的函数和一个对象,
filter()
把传入的函数依次作用于每个元素。如果某个元素对应的函数值返回值为True
则在新的 Iterator 中保留该元素,否则丢弃该元素。 -
一个非常简单的例子,筛出奇数:
def is_odd(n): return n % 2 == 1 r = list(filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15])) print(r) # 结果: [1, 5, 9, 15]
-
一个稍微复杂一点的例子,删去 list 里的非整数:
def judge(x): return isinstance(x, int) x = ['a', '', (), iter([1]), 1, 5] r = filter(judge, x) print(list(r))
-
对代表自然数的 Iterator 进行筛选,求小于 10 的所有的奇数:
def is_odd(n): return n % 2 == 1 def Natural_number(): x = 0 while True: yield x x += 1 x = Natural_number() r = filter(is_odd, x) for i in r: if i <= 10: print(i) else: break
-
是的你没有看错,可以对无限长度的 Iterator 进行筛选。
filter
的筛选与 Iterator 的生成都是懒惰的,使用fliter
相当于给生成添加了判断条件,通过这可以实现上述的无限长度序列的筛选。 -
需要注意的是,如果筛选一个生成器后要通过
for
进行某个范围内的遍历,要防止筛选后无法正常使用判断条件。 -
实现埃氏筛:
def number(): #构造从 2 开始的整数列 x = 2 while True: yield x x += 1 def solve(p): #筛数 return lambda x: x % p def primes(): num = number() while True: p = next(num) yield p num = filter(solve(p), num) r = primes() for i in r: if i <= 100: #输出小于 100 的 print(i) else: break
-
上述代码中
filter
筛选函数中,为了能够传入一个参数,因此使用了lambda
匿名函数,其详细用法将在后文补充。 -
对上述代码的一些疑问:为什么一定要使用匿名函数?可不可以把
p
定义成全局变量,把solve
和primes
两个函数写成下述形式?p = 1 def solve(x): #筛数 return x % p def primes(): num = number() while True: global p p = next(num) yield p num = filter(solve, num)
答案是否定的。这样修改后发现输出了 2~100 中的每个自然数。怎么会是呢?
-
前文提到,
filter
的筛选与 Iterator 的生成都是懒惰的,使用fliter
相当于给生成添加了判断条件。把这个过程详细地形容一下:对于某个 Iterator,对它进行的每次fliter
后都会另外开辟一段内存空间,存下传入的判断函数的信息;而在生成 Iterator 的下一个元素时,才会依次将新生成的元素依次传入存储的所有判断函数中进行判断来决定是否保留该元素。 -
因此,当我们使用最初的正确写法时,每次
fliter
后储存的函数信息中包含了此次用于埃氏筛的数p
,因此在将新生成的元素依次传入函数中判断时p
的值均没有发生改变,可以保证判断的正确性;而当采用修改后的写法时,传入判断函数中判断时读取的p
发生改变,均为当前的全局变量的值,其值等于新生成的元素 -1,则判断失效。
sorted
- 传入一个 list 与一个实现映射的函数
key=...
。key
指定的函数将作用于 list 的每一个元素上,并根据key
函数返回的结果,按照对应类型的排序方法进行升序排序。 - 一个简单的例子,升序排序:
sorted([36, 5, -12, 9, -21])
- 一个也很简单的例子,降序排序:
def f(x): return -x sorted([36, 5, -12, 9, -21], key = f)
- 上面那种写法太原始了,可以直接传入第三个参数
reverse
:sorted([36, 5, -12, 9, -21], reverse = True)
- 可以直接进行字符串字典序排序:
s = sorted(['abc', 'abd', 'Abc', 'AbD'])
- 一个有点复杂的例子,将 list 中整数放在前面,字符串放中间,list 放在最后,同一种类型按照在 list 中原来的顺序排序:
def f(x): if isinstance(x, int): return 0 elif isinstance(x, str): return 1 elif isinstance(x, list): return 2 x = [[1], 4, "114514", [2], -21] s = sorted(x, key = f)
函数作为返回值
- 闭包:能够读取其他函数内部变量的函数。
- 将函数作为返回值相当于返回了一个闭包,可以引用外部函数的参数和局部变量,但是不能修改。这也是 C 与 Python 不同的一点,C 的函数运行结束后局部变量会被立刻释放,py 却会一直保存。
- 一个简单的例子,懒惰求和:
def calc_sum(*args): def sum(): ax = 0 for n in args: ax = ax + n return ax return sum f = calc_sum(1, 2, 3, 4, 5) print(f())
- 解释一下运行顺序:首次调用
calc_sum
时会直接返回sum
函数,再次调用f
时会首先进入calc_sum
,引用曾经传入的参数,然后直接跳转到sum
并计算。 - 需要注意的是,每次调用
calc_sum
时都会创建一个新的副本,也就是说每次返回函数都是不同的,即有:>>> f1 = lazy_sum(1, 3, 5, 7, 9) >>> f2 = lazy_sum(1, 3, 5, 7, 9) >>> 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()) print(f2()) print(f3())
1 4 9
的输出,但很遗憾,实际的输出是9 9 9
。为什么呢?
考虑运行的顺序:第一次调用count()
时,我们通过循环构建了一个存有三个f
的list
,循环变量i
增加到 3 后循环停止,之后将这个list
中三个元素分别赋值给了f1
、f2
和f3
。当我们调用它们时,首先进入了count
函数,然后进入了我们在循环中创建的f
。但在f
中计算i * i
时,引用的循环变量i
的值是循环结束时的3
,而非在循环中创建函数时的值,因此三次调用输出都是 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
lambda
简化,详细用法将在后文补充。 nonlocal
关键字:用于需要在闭包内修改外部参数局部变量的情况。使用nonlocal
标记后,外部变量即可被修改。- 一个例子:
此时每次调用def createCounter(): x = 0 def counter(): nonlocal x x = x + 1 return x return counter f = createCounter()
f()
时即可令x + 1
,并返回x
当前的值。 nolocal
和global
关键字的区别:global
标识该变量是全局变量,而nonlocal
标识该变量是上一级函数中的局部变量,上一级函数中不存在该局部变量则报错,因此只可用于嵌套函数中。
匿名函数 lambda
- 使用
lambda
可以简便地定义一个简单函数。 - 一个非常简单的例子:
等价于:f = lambda x: x * x print(f(2)) #输出 4
def f(x): return x * x print(f(2))
lambda
允许传入多个参数:f = lambda x, y: x + y print(f(1, 2))
- 可以看出,
lambda
的限制就是只能有一个表达式,不用写return
,返回值就是该表达式的结果。使用lambda
的好处是不需要考虑函数名,避免了函数名冲突,如果需要多次使用可以将其赋给一个变量,与直接定义相比节省了代码量。 - 同样,也可以把匿名函数作为返回值返回:
def build(x, y): return lambda: x * x + y * y f = build(1, 2) print(f()) #输出为 5
lambda
最能大显身手的地方还是在与高级函数的嵌套中:map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8]) reduce(lambda x, y: x + y, [1, 2, 3, 4]) sorted([-4, 1, -3, 4, 5], key = lambda x: abs(x))
- 使用
lambda
可以在filter
筛选函数中传入其他参数:def f(p): #筛出 3 的倍数 return lambda x: x % p == 0 r = filter(f(3), [2, 3, 4, 5, 6])
装饰器(Decorator)
-
这里廖雪峰老师讲的很好但是比较劝退新人,另外参考了:python @符号 - linyb。
-
一种在不改变原函数定义的要求下,为原函数增加额外功能的写法。
-
首先介绍
@
函数修饰符:作用是为现有函数增加额外的功能,常用于插入日志、性能测试、事务处理等。创建函数修饰符的规则如下:- 修饰符是一个函数。
- 修饰符取被修饰函数为参数。
- 修饰符返回一个新函数。
- 修饰符维护被维护函数的签名。
-
一个
@
函数修饰符的例子:def fa(func): print('%s() is defined' % func.__name__) return func @fa def fb(): print(1) fb() fb() fb()
输出结果为:
fb is defined #在定义 fb 后输出 1 #第一次调用 fb 时输出 1 #第二次 1 #第三次
解释一下,这样使用
@
符号,等价于在定义fb
后紧跟着增添这样一条语句:fb = fa(fb)
。而在fa
函数中进行了一次输出操作后就原样返回了参数func
,因此fb
的功能没有改变。 -
这里仅介绍了
@
的简单用法,其他的用法会在后文进行补充。 -
一个装饰器的例子:
def log(func): def wrapper(*args, **kw): print('call %s():' % func.__name__) return func(*args, **kw) #返回函数值 return wrapper #返回函数名称 @log def now(): print("114514") now()
输出结果为:
call now(): #调用 now() 时输出 114514
此例子中我们定义了装饰器
log
,它是一个以函数为返回值的高阶函数,其作用是输出我们调用的函数的日志。我们通过@
函数修饰符将log
置于now
前,等价于我们在now
定义后紧跟了一行:now = log(now)
。
新定义的wrapper()
函数的参数定义是(*args, **kw)
,因此,wrapper()
函数可以接受任意参数的调用。在wrapper()
函数内,首先打印日志,再紧接着调用原始函数。于是now
函数在原有功能未被影响的情况下,新增了一个输出日志的功能。可以发现,本质上,decorator 就是一个返回函数的高阶函数。
-
由于
log()
是一个 decorator,返回一个函数,所以,原来的now()
函数仍然存在,只是现在同名的now
变量指向了新的函数,于是调用now()
将执行新函数,即在log()
函数中返回的wrapper()
函数。 -
如果 decorator 本身需要传入参数,则需要再多一层嵌套:
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('execute') def now(): print('114514') now()
@log('execute')
等价于:now = log('execute')(now)
,首先执行了log('execute')
语句,返回了函数decorator
,然后now
作为参数调用了该函数,返回值最终是wrapper
函数。 -
上面的写法仍然存在一点小问题。经过装饰后,由于
now
函数被替换成了wrapper
,调用now.__name__
的返回值变成了wrapper
。因此我们还需要将now
的一些属性复制给wrapper
。只需要调用 Python 内置的functools.wraps
即可,修改后的装饰器写法如下: -
不传入参数:
import functools def log(func): @functools.wraps(func) def wrapper(*args, **kw): print('call %s():' % func.__name__) return func(*args, **kw) return wrapper
-
传入参数:
import functools def log(text): def decorator(func): @functools.wraps(func) def wrapper(*args, **kw): print('%s %s():' % (text, func.__name__)) return func(*args, **kw) return wrapper return decorator
偏函数
- 用于固定函数的可变参数和关键字参数,从而简化函数调用的一种写法。
- 以下是一个将 n 进制的数字串转化为 10 进制对应整数的程序:
如果我们想要转化大量的某已知进制串,可以写一个含有默认参数的函数:s = input('string:') n = int(input('base:')) print(int(s, base = n))
输出为:def int2(s, b = 2): return int(s, base = b) s = '10' print(int2(s)) print(int2(s, 3))
这就是传统的偏函数的写法。2 3
- 我们可以使用
functools.partial
来简化偏函数的定义。使用前需要添加:import functools
,以下是上述例子的修改版本。
输出为:import functools int2 = functools.partial(int, base = 2) s = '10' print(int2(s)) print(int2(s, base = 3)) #这里必须是命名关键字参数的形式
2 3
- 为什么调用时传入
base
必须是命名关键字形式呢?详见下文。 - 让我们更加详细地解释一下:
functools.partial
可以接受三个参数:函数、可变参数*args
和关键字参数**kw
。 - 一个简单的例子:为 max 函数添加固定的比较对象:
参数import functools my_max = functools.partial(max, 0) print(my_max(-1, -2, -3))
0
会被添加到参数列表的最左侧,上述代码等价于:def my_max(*args): return max(0, *args) print(my_max(-1, -2, -3))
- 因此在上上个例子中,如果我们调用时使用了
print(int2(s, 3))
,相当于调用了int(s, 3, base = 2)
,实际参数3
并不会赋值给形式参数base
,则会报错:int() takes at most 2 arguments (3 given)
,表示传入了过多的参数。
模块(Module)
- 如果学过 C:头文件!
- 一个 .py 文件就是一个模块,可以通过
import
关键字导入模块在其他程序中使用模块内的代码。 - 使用模块可以把一个大型工程拆分后逐个编写和测试,大大提高了代码的可维护性。还可以引用其他人已完成的模块来简化自己的代码。还可以避免函数名和变量名冲突。相同名字的函数和变量完全可以分别存在不同的模块中,因此,我们自己在编写模块时,不必考虑名字会与其他模块冲突。
- 创建自己的模块时,要注意:
- 模块名要遵循 Python 变量命名规范,不要使用中文、特殊字符;
- 模块名不要和系统模块名冲突,最好先查看系统是否已存在该模块,检查方法是在 Python 交互环境执行
import abc
,若成功则说明系统存在此模块。
- 以下是模块的一种标准模板:
1、2 行是标准注释,第 1 行注释可以让这个文件直接在#!/usr/bin/env python3 # -*- coding: utf-8 -*- ' a test module ' __author__ = 'Luckyblock'
Unix/Linux/Mac
上运行,第 2 行注释表示 .py 文件本身使用标准 UTF-8 编码;第 4 行是一个字符串,表示模块的文档注释,任何模块代码的第一个字符串都被视为模块的文档注释,可以通过变量__doc__
访问;第 6 行使用__author__
变量把作者写进去,这样当公开源代码后别人就可以瞻仰我们的大名;
当然也可以全部删掉不写,但是,按标准办事肯定没错。 - 为了避免模块名冲突,Python 引入了按照目录组织模块的方法,称为包(Package),即通过文件目录区分不同的模块。只需在导入模块前加上所在目录即可。
- 比如以下的目录结构:
mycompany
├─ web
│ ├─ init.py
│ ├─ utils.py
│ └─ www.py
├─ init.py
└─ utils.py - 每一个包目录下面都会有一个
__init__.py
的文件,这个文件是必须存在的,否则,Python 就把这个目录当成普通目录,而不是一个包。__init__.py
本身就是一个模块,它可以是空文件,也可以有 Python 代码。 - 目录
mycompany
下的__init__.py
的模块名是mycompany
,文件www.py
的模块名是mycompany.web.www
,两个文件utils.py
的模块名分别是mycompany.utils
和mycompany.web.utils
。mycompany.web
也是一个模块,它对应的文件是该目录下的__init__.py
。 - 例如,如果想要导入
web
目录下的www.py
,只需要这样写:import mycompany.web.www
。
使用模块
- 以下是一个名为
my_module.py
的模块:
在同目录的其他代码中我们可以这样导入该模块,来恳请 A 老板 v 我们 50 吃疯狂星期四:#!/usr/bin/env python3 # -*- coding: utf-8 -*- ' a test module ' __author__ = 'Luckyblock' def Aliemo(): print('fgAliemo fkxq4 vw50') print('Aliemo is a fg')
输出结果为:import my_module my_module.Aliemo()
Aliemo is a fg fgAliemo fkxq4 vw50
- 可以发现,当我们导入了模块时,首先会执行完模块内的代码,因此上述例子中首先输出了
Aliemo is a fg
。之后我们就有了一个与模块名同名的变量来指向这个模块,通过这个变量就可以访问模块的所有功能。 - 模块名太长了怎么办?可以这样重命名指向模块的变量:
import my_module as my my.Aliemo()
- 也可以仅导入某个模块中的某个函数或变量:
当然也可以重命名变量:from my_module import Aliemo Aliemo()
from my_module import Aliemo as A A()
- 再举个例子,以下仍是一个名为
my_module
的模块:
当我们直接运行这个模块时,输出为#!/usr/bin/env python3 # -*- coding: utf-8 -*- ' a test module ' __author__ = 'Luckyblock' def Aliemo(): print('fgAliemo fkxq4 vw50') if __name__ == '__main__': Aliemo()
fgAliemo fkxq4 vw50
。但如果在其他代码中仅导入该模块则没有输出。因为当直接运行一份代码时,变量__name__
的值才会被赋为__main__
从而执行一次Aliemo()
。但如果是被导入的话,__name__
的值则会被赋为文件名,可以测试一下,如果我们在导入该模块后这样:
输出为:import my_module print(my_module.__name__)
my_module
。
我们可以利用这个特性使模块在单独执行时多执行一些语句,来进行模块的运行测试。 - 再举一个导入包中的模块的例子,比如有一个名为
my_module.py
的模块存放于本目录下的 package 文件夹中,即有:
work_station
├─ package
│ ├─ init.py
│ └─ my_module.py
└─ test.py
当我们需要在test.py
中导入该模块时,最直接的写法是:
前面提到,包也是一个模块,对应目录中的import package.my_module
__init__.py
,所以import package
等价于import package.__init__
。所以当我们想要导入一个包中的多个模块时,可以这样修改__init__.py
:
导入时仅需#!/usr/bin/env python3 # -*- coding: utf-8 -*- ' a test module ' __author__ = 'Luckyblock' import package.my_module #这里写要导入的其他模块
import package
即可。按需求修改__init__.py
就可以可以像这样起到控制导入模块的作用。
作用域
- 有时我们想要封装模块中的一些函数和变量,使它们隐藏起来不能在模块外部使用,即使它们的作用域局限在模块内部。然而 Python 中并不提供 private 与 pubilc 这类的关键字,我们只能通过习惯性的函数和变量的命名习惯来标记它们的作用域。
- 正常的函数和变量名是公开的(public),可以被直接引用,比如:
abc,x123,PI
等。 - 类似
__xxx__
这样的变量是特殊变量,可以被直接引用,但是有特殊用途,比如上面的__author__
,__name__
就是特殊变量,按照标准模块书写方式定义的文档注释也可以用特殊变量__doc__
访问,我们自己的变量一般不要用这种变量名。 - 类似
_xxx
和__xxx
这样的函数或变量就是非公开的(private),不应该被直接引用。 - 书写模块时,一般将外部不需要引用的函数全部定义成 private,只有外部需要引用的函数才定义为 public。这样可以增加模块的可维护性。调用时我们仅需关注 pubilc 对象,而不必在意 private 对象的细节,从而简化了调用模块的难度。
- 举个例子,我们可以将上文中写的埃氏筛这样封装起来:
看起来似乎多此一举,但是当他人导入这个模块时仅需关注#!/usr/bin/env python3 # -*- coding: utf-8 -*- ' a test module ' __author__ = 'Luckyblock' def _number(): #构造从 2 开始的整数列 x = 2 while True: yield x x += 1 def _solve(p): #将 Iterator 中 p 的倍数删去 return lambda x: x % p def primes(): num = _number() while True: p = next(num) yield p num = filter(_solve(p), num)
primes
中的算法即可,生成数列和筛选的细节可以通过阅读注释理解,不需要花费额外的精力。
安装第三方模块
- 首先安装包管理工具 pip,安装 Python 时勾选对应选项
pip
和Add python.exe to Path
即可。之后就可以在 cmd 里运行 pip 了。 - 一般来说,第三方库都会在 Python 官方的 pypi.python.org 网站注册,要安装一个第三方库,必须先知道该库的名称,可以在官网或者 pypi 上搜索,比如Pillow 的名称叫 Pillow ,因此,安装 Pillow 的命令就是:
pip install Pillow
。 - 不过这样一个一个安装确实麻烦,可以下载常用模块的整合包
Anaconda
。国内用户推荐去 THU 的镜像站下载:Index of anaconda 清华大学开源软件镜像站,有梯子的大神可以去 Anaconda 官网 下载。
模块及其他文件的搜索路径
- 当我们试图加载一个模块时,Python会在指定的路径下搜索对应的 .py 文件,找不到则会报错。
- 在我们未指定目录的默认情况下,Python解释器会搜索当前目录、所有已安装的内置模块和第三方模块,搜索路径存放在
sys
模块的path
变量中,比如在我的电脑上就会有:>>> import sys >>> sys.path ['', 'D:\\anaconda3\\python39.zip', 'D:\\anaconda3\\DLLs', 'D:\\anaconda3\\lib', 'D:\\anaconda3', 'D:\\anaconda3\\lib\\site-packages', 'D:\\anaconda3\\lib\\site-packages\\win32', 'D:\\anaconda3\\lib\\site-packages\\win32\\lib', 'D:\\anaconda3\\lib\\site-packages\\Pythonwin'] >>>
- 可以直接修改这个变量的值来添加自己的搜索目录,比如:
不过这样只会在代码运行时生效,运行结束后就会失效。>>> import sys >>> sys.path.append('/Users/michael/my_py_scripts')
- 如果要长期使用可以设置环境变量。
面向对象编程
- 这个中文翻译有些侮辱性,我暂且蒙在鼓里。
- 面向对象编程——Object Oriented Programming,简称 OOP,是一种程序设计思想。OOP 把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数。
- 关于面向过程与面向对象的区别:
- 面向过程的程序设计把计算机程序视为一系列用于处理输入数据的命令集合,即一组函数的顺序执行,可以形象地理解为一个有入口(数据输入)和出口(由数据决定的行为)的黑匣子。为了简化程序设计,面向过程把函数继续切分为子函数,即把大块函数通过切割成小块函数来降低系统的复杂度。
- 而面向对象的程序设计,首选思考的不是程序的执行流程,而是把计算机程序视为一组对象的集合。对于每一种数据类型都视为一个对象,每个对象都有一些该对象对应的行为,而且每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。
- 我这里仅结合廖雪峰老师的文章记录了自己的理解,他给出了一个代码的例子,详情阅读:面向对象编程 - 廖雪峰的官方网站。如果需要更形象的例子请自行百度。
- 面向对象的设计思想是从自然界中来的,因为在自然界中,类(Class)和实例(Instance)的概念是很自然的。类是一种抽象概念,而实例则是一个个具体的类。所以,面向对象的设计思想是抽象出类,根据类创建实例。
- 每一个类都有一些用于操作这种类型的特有的关联函数,称之为对象的方法。给对象发消息就是调用对象对应的关联函数。
- 因此面向对象的抽象程度又比函数要高,因为一个类既包含数据,又包含操作数据的方法。
- 面向过程的好处是对于一个有具体功能的代码,按照解决问题的过程进行编程难度较低,并简化了阅读的难度。面向对象的好处是将功能与对象绑定了起来,使得同一对象可以在不同的代码里使用,从而提高了代码的复用性。两者各有好坏,没有绝对的优劣之分。
- 数据封装、继承和多态是面向对象的三大特点,后文会进行详细的介绍。
类(class)和实例(Instance)
- 面向对象最重要的概念就是类(class)和实例(Instance)。类是抽象的模板,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。
- 举个简单的例子,以下的代码定义了一个简单的类,并创建了一个实例:
定义类时括号里需要传入一个类名,表示新定义的类从哪个类继承而来,对继承的解释会在下文详细展开。现在仅需知道如果不需要从某个已定义的类中继承,则括号内填入class Oier(object): pass Aliemo = Oier()
object
类即可。定义实例后,我们便可以对实例进行一些操作:
可以动态地修改实例的属性,这比作为静态语言的 C 的 struct 灵活了十倍甚至九倍。Aliemo.name = "Aliemo" Aliemo.money = "Inf" print(Aliemo.name, Aliemo.money)
- 如果想要简单地初始化一个实例,可以定义一个特殊的
__init__
方法,就可以在创建实例时便将属性绑定到实例上:
在类的方法的定义中,class Oier(object): def __init__(self, name, money): self.name = name self.money = money Aliemo = Oier("Aliemo", "Inf") print(Aliemo.name, Aliemo.money)
self
是指向调用这个方法的实例的特殊参数。__init__
方法的第一个参数必须是self
。类的方法本质上就是函数,写法可以非常灵活,详情参考上文中关于函数的内容。
有了__init__
方法之后创建实例时就不能传入空的参数了,必须和上述例子类似地传入与__init__
方法匹配的参数,但self
不需要传,Python 解释器自己会把它传进去。 - 这里需要注意!如上文所述,当实例调用方法时,Python 解释器自己会把实例变量作为参数传进去,所以我们在定义方法时,就算是实现与实例无关的功能,也必须要写
self
参数,并把self
参数写在括号最前面。如果我们写出了以下的代码:
就会有以下错误信息出现,意思是我们调用该方法时传入了 1 个参数,但是该方法的定义里没有参数。class Aliemo(object): def Thursday(): print("Aliemo have many money, fkxq4, vw50!") a = Aliemo() a.Thursday()
TypeError: Aliemo.Thursday() takes 0 positional arguments but 1 was given
- 再举个稍复杂的例子,以下的代码定义了一个名为
Aliemo
的类,并定义了这个类的一些方法:class Aliemo(object): def __init__(self, name = "Aliemo", money = "Inf"): self.name = name self.money = money def Thursday(self): print("%s have %s money, fkxq4, vw50!" % \ (self.name, self.money)) Kersen = Aliemo("Kersen", "many many") Kersen.Thursday()
数据封装
- 对于上述例子,我们给
Kersen
实例绑定了两个属性name
与money
,但当我们想要实现需要这两个属性的功能Thursday()
时,我们没有选择直接在主体部分print("%s have %s money, fkxq4, vw50!" % (Kersen.name, Kersen.money))
,而是在类中定义了访问数据的函数,这样就把数据和处理数据的逻辑“封装”起来了。 - 这样从外部看
Aliemo
类时仅需知道创建实例所需的参数,以及调用方法的方式即可,不需要知道内部实现的细节。不仅更方便,而且更安全。
访问限制
- 在模块那一章的作用域部分中,我们提到了可以使用变量前加双下划线的方式来将一个对象定义为
private
的。在类的定义中,我们也可以用这样的方法来禁止一个对象在外部代码中被访问和修改。 - 举个简单的例子:
此时,class Aliemo(object): def __init__(self, name, money): self.__name = name self.__money = money def Thursday(self): print("%s have %s money, fkxq4, vw50!" % \ (self.__name, self.__money)) Kersen = Aliemo("Kersen", "many many")
Kersen.Thursday()
仍是合法的,但是print(Kersen.__name)
将会直接报错。 - 如果需要让外部代码获取或修改实例的属性怎么办?可以新增专门用来访问属性的方法,如:
不过这样看起来好麻烦,为什么不直接像原来那样直接访问修改呢?因为这样新定义方法的同时可以进行参数的检查,来避免无效的参数,如:class Aliemo(object): def get_name(self): return self.__name def set_name(self, value): self.__name = value
class Aliemo(object): def set_name(self, value): if isinstance(value, str): self.__name = value else : print("False!")
- 然而,让人失望的是,上面这种定义
private
对象的方法只是一种障眼法。不能直接访问__name
是因为Python 解释器对外把__name
变量改成了_Aliemo__name
,所以,仍然可以通过_Aliemo__name
来访问__name
变量:
不过这种写法有种明知有法还要去试法的感觉,就像先辈知道雷普后辈非常的屑但还是要去一样,非常的野蛮,强烈不推荐这么做。Python 本身没有任何机制阻止你干坏事,一切全靠自觉。Kersen._Aliemo__name = 114514
还有就是因为不同版本的 Python 解释器可能会把__name
改成不同的变量名,如果这样的话,就会出现下面提到的错误: - 如果我们头铁地想要在外部代码中修改
private
对象,并且还不知道上面的替换规则,写出了像这样的屑代码时:
然后我们发现这份代码是可以运行的,于是就想当然地认为Kersen.__name = "田所浩二"
private
对象已经被修改了——然而通过上面的讲解我们得知,这实质上是给实例绑定了一个新的属性__name
,而非修改了实例中的private
对象。这种错误对初学者来说是非常隐蔽的,请务必注意。
继承与多态
- 还记得我们上文留的坑吗?
- 当我们定义一个 class 的时候,可以从某个现有的 class 继承,新的 class 称为子类(Subclass),而被继承的 class 称为基类、父类或超类(Base class、Super class)。
- 举个例子:
在上述例子中,class Oier(object): def __init__(self, name, money): self.name = name self.money = money def print_name(self): print(self.name) class Aliemo(Oier): def print_money(self): print(self.money) Kersen = Aliemo("Kersen", "many many") Kersen.print_name() Kersen.print_money()
Oier
是Aliemo
的父类,反过来,Aliemo
是Oier
的子类。可以发现,当Aliemo
继承了Oier
之后,就自动拥有了Oier
的所有属性和方法,并且也可以增加新的方法。 - 当子类和父类存在相同名称的方法时,子类的方法会覆盖父类的方法,在代码运行时总会调用子类的方法。我们称这种特性为多态,以下是一个简单的例子:
class Aliemo(Oier): def print_name(self): print(self.name, "is a fg")
- 多态有什么好处呢?假设现在有这样的一个函数:
对于任意def print_oier(oier): oier.print_name()
Oier
的子类,它们都有print_name
方法,因此它们都可以被传入这个函数并正常运行。对于一个变量,我们只需要知道它的父类,无需确切地知道它的子类型,就可以放心地调用对应的方法了——这就是多态真正的威力:调用方只管调用,不管细节。 - 上述例子体现出了多态的“开闭”原则:
- 对扩展开放:允许新增子类;
- 对修改封闭:不需要修改依赖特定类类型的函数,如
print_oier()
。
- “鸭子类型”
对于静态语言来说,我们需要在传入函数参数时指定形式参数的类型,则我们仅可以传入指定类型或子类的实例,否则将无法调用函数。然而对于 Python 这样的动态语言,并不需要指定传入的类型;正如我们刚才举出的例子,仅需保证传入的对象有特定的属性和方法即可调用函数。
这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。 - 上一节访问限制中我们提到,当在定义类时为类绑定
private
对象属性时,在对象的名称实际上会被解释为_(类名)__(对象名)
,仅在类的定义内部使用__(对象内部)
可以访问该对象。但是在父类里绑定的属性,在子类里会变成什么样呢?可以做个实验:
然后发现报错了,错误信息是:class Oier(object): def __init__(self, name): self.__name = name class Aliemo(Oier): def print_name(self): print(self.__name) Kersen = Aliemo("Kersen") Kersen.print_name()
AttributeError: 'Aliemo' object has no attribute '_Aliemo__name'. Did you mean: '_Oier__name'?
,可以发现,因为__init__
方法由父类继承而来,在父类的方法中绑定的private
对象仍会被解释为_(父类名)__(对象名)
,而在子类中__(对象名)
指向的却是_(子类名)__(对象名)
。
为了验证这个结论,我们可以再做个实验。我们在子类里修改__init__
方法,但把输出的方法放到父类里面:
发现还是会报错,但是错误信息变成了:class Oier(object): def __init__(self, name): self.__name = name def print_name(self): print(self.__name) class Aliemo(Oier): def __init__(self, name): self.__name = name Kersen = Aliemo("Kersen") Kersen.print_name()
AttributeError: 'Aliemo' object has no attribute '_Oier__name'. Did you mean: '_Aliemo__name'?
哈哈,和上面正好反了。
原因显然易见,print_name
方法由父类继承而来,所以函数内的__name
会被解释为_Oier__name
而非子类的__init__
初始化的的_Aliemo__name
。 - 看来如果想要在子类里使用父类的
private
对象,就只能修改所有方法的定义了吗?2022.10.10 暂时还不清楚,留个坑先。 - 2022.10.21 回来填坑了,使用单下划线来定义
private
对象即可。
获取对象信息
-
这一节的名字也有些侮辱性,我暂且蒙在鼓里。
-
函数
type(x)
可以接受一个实例x
,并返回参数对应的类型:>>> type(123) <class 'int'> >>> type('str') <class 'str'> >>> type(None) <type(None) 'NoneType'> >>> type(abs) <class 'builtin_function_or_method'> >>> type(a) #a = Oier() <class '__main__.Oier'>
-
注意返回值是一个类型,所以你甚至可以这样写,非常的神奇:
class Oier(object): pass a = Oier() b = type(a) () #b 是 Oier 的一个实例 b.name = 114514
-
如果要在条件判断语句里使用该函数判断参数是否为某特定的类,则需要这样写:
>>> type(123) == type(456) True >>> type(123) == int True >>> type('abc') == type('123') True >>> type('abc') == str True >>> type('abc') == type(123) False
-
如果要判断一个对象是否是函数怎么办?可以使用
types
模块中定义的常量:>>> import types >>> def fn(): ... pass ... >>> type(fn) == types.FunctionType True >>> type(abs) == types.BuiltinFunctionType True >>> type(lambda x: x) == types.LambdaType True >>> type((x for x in range(10))) == types.GeneratorType True
-
我们注意到,使用
type
函数返回的是该实例在定义时对应的类,无法判断一个实例是否是某个父类,举个例子:class Oier(object): pass class Aliemo(Oier): pass a = Aliemo() print(type(a) == Oier)
输出为
False
,所以我们需要一些功能更强大的函数: -
isinstance()
函数可以接受一个实例与一个类的 tuple,并判断该实例是否是这个 tuple 中的类,或者是这些类的子类。 -
举个例子:
>>> isinstance('a', str) True >>> isinstance(123, int) True >>> isinstance(b'a', bytes) True
-
再举个例子:
class Oier(object): pass class Aliemo(Oier): pass a = Aliemo() print(isinstance(a, Oier) and isinstance(a, Aliemo)) print(isinstance(a, (Oier, Aliemo)))
输出结果为两个
True
。 -
如果要获取一个类的所有属性和方法,可以使用
dir()
函数,它接受一个类名或者一个对象,返回一个包含字符串的 list。 -
比如获得一个 str 对象的所有属性和方法:
>>> dir(str) ['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isascii', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isuffix', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
-
类似
__xxx__
的属性和方法在 Python 中都是有特殊用途的,比如__len__()
方法返回对象的长度。调用len()
函数获取一个对象的长度,在函数内部是通过调用该对象的__len__()
方法实现的。因此下面的代码是等价的:>>> len('ABC') 3 >>> 'ABC'.__len__() 3
虽然这样有些怪,我们也可以给自己定义的类添加
__len__()
方法,使得len()
函数可以接受对应的对象。class Oier(object): def __len__(self): return 15 Aliemo = Oier() print(len(Aliemo))
需要注意
len()
函数的返回值是int
型的,如果我们定义的__len__()
方法返回值与其不一致则会报错。 -
下文中“定制类”一节将会更加详细地介绍此类形如
__xxx__
的属性和方法的用法。 -
使用函数
hasattr()
、setattr()
以及getattr()
,可以分别判断对象是否存在某个属性或方法、为对象设置属性和获取一个对象的属性。 -
举个稍复杂的例子,比如当我们有:
class Oier(object): def __init__(self): self.name = "Aliemo" def fkxq4(self): print("vw50") Aliemo = Oier()
-
hasattr()
可以接受一个对象和一个字符串,判断该对象是否存在名为该字符串的属性。print(hasattr(Aliemo, 'name')) print(hasattr(Aliemo, 'money'))
输出为
True
和False
。 -
setattr()
可以接受一个对象、一个对应属性名的字符串和一个值,将对象该属性设置为对应的值。如果当前不存在对应属性,则会新建一个对应的属性。setattr(Aliemo, 'name', 'Kersen') setattr(Aliemo, 'money', 'Inf') print(Aliemo.name, Aliemo.money)
输出为
Kersen Inf
。 -
getattr()
函数可以接受一个对象,一个对应属性名的字符串和一个default
参数,来获取对象对应属性的值。如果不存在对应属性时,如果没有设置default
参数则会抛出AttributeError
的错误,否则返回default
参数。print(getattr(Aliemo, 'name')) print(getattr(Aliemo, 'money', 0)) print(getattr(Aliemo, 'money'))
前两行的输出为:
Aliemo
和0
,第三行抛出了错误。 -
当然,也可以使用上述函数对方法进行判断、获取。
print(hasattr(Aliemo, 'fkxq4')) b = getattr(Aliemo, 'fkxq4') b()
-
最不可思议的是,配合
lambda
时setattr()
函数可以为对象设置新的方法……不过这种写法属实有些脑回路清奇,我自己瞎试出来的,非常不推荐使用。而且与直接在类中定义方法不同, lambda 中不可以设置参数self
,不知道为什么,留个坑先。setattr(Aliemo, 'fkxq5', lambda : print("vw5k")) Aliemo.fkxq5()
-
虽然我们有了能抛出错误的
getattr
,但当我们明确知道一个对象存在某属性时,应当直接调用对应属性。当且仅当只有在不知道对象信息的时候,我们才会试图获取对象信息。
实例属性和类属性
- 前面提到,我们可以通过
__init__
函数为每一个对象初始化属性。但是这样初始化的属性,对于每个对象而言是相互独立的,修改与访问都互不影响。如果我们想要为每个对象添加一个公共的属性,使得修改和访问的是同一个属性,应当怎么办呢? - 答案是可以直接为类绑定属性,如:
输出分别为class Oier(object): name = "Aliemo" a = Oier() print(a.name) Oier.name = "Kersen" print(a.name)
Aliemo
和Kersen
。 - 但当我们在实例中又绑定了一个同名属性时,类的属性会被覆盖,调用属性时将指向实例的属性。然而当我们删除实例的属性后,调用同名属性又将指向类的属性。
输出为:a = Oier() a.name = "Kersen" print(a.name) del a.name print(a.name)
Kersen
和Aliemo
。 - 我们可以理解为,类的属性为实例的属性提供了一个默认的值。然而我们应当避免这种为实例和类绑定同名属性的写法,因为我们在调用到期望之外的属性时将不会出现错误信息,从而导致一些难以发现的错误。
- 以下是一点胡思乱想:我们发现可以为类绑定属性,由此是否可以认为类本身也是一个实例呢?不妨来做一点实验:
发现输出是正常的class Oier(object): name = "Aliemo" def fkxq4(): print("vw50") Oier.fkxq4()
vw50
,而且此时不可以在函数定义中传入self
参数……感觉非常的神奇,暂时解释不了为什么会是这样,再留个坑。
面向对象高级编程
__slots__
__slots__
是一个 tuple 类型的属性,用于限制某个类的实例可以绑定的属性。使用方法为给该属性赋一个由属性名组成的 tuple。- 举个例子:
报错为:class Oier(object): __slots__ = ('name', 'money') a = Oier() a.name = "Aliemo" a.money = "Inf" a.length = "1cm"
AttributeError: 'Oier' object has no attribute 'length'
。 - 需要注意
__slots__
定义的属性仅对当前类的实例起作用,如果在子类中未定义__slots__
时子类可绑定的属性是不受限制的。但如果在子类中定义__slots__
,那么子类可绑定的属性就是自身的__slots__
加上父类的__slots__
。
@property
- 上文中访问限制一节中给出了这样一个例子:
在这个例子中,为了保护参数和进行参数的检查,我们新增专门用来访问和修改属性的方法。虽然这样增加了代码的安全性,但同时也增加了编写代码的复杂度。这,对 Python 简洁又强大的特性是一种教科书般的亵渎!!!class Aliemo(object): def __init__(self, name, money): self._name = name self._money = money def get_name(self): return self._name def set_name(self, value): if isinstance(value, str): self._name = value else : print("False!")
- 此时,我们便可以使用
@property
装饰器来简化代码,它可以把调用方法简化为调用属性。由于@property
内部实现比较复杂,此处仅讨论如何使用。 - 我们通过一个实际的例子进行说明:
使用class Aliemo(object): def __init__(self, name, money): self._name = name self._money = money @property #装饰访问属性的方法 def name(self): return self._name @name.setter #装饰修改属性的方法,注意该装饰器和该方法的命名 def name(self, value): if isinstance(value, str): self._name = value else : print("False!") a = Aliemo("Boss.A", "Inf") a.name = "Kersen" print(a.name)
@property
装饰用于访问属性的方法,即可在外部代码中直接访问该属性,而不需调用对应的方法。更具体的,代码中的a.name
被装饰成了之前的例子中的a.get_name()
,a.name=...
被装饰成了a.set_name(...)
,这样做既简化了代码又保留了检查参数的功能。
需要注意,访问属性和修改属性的方法需要是同名的,还有属性的方法名不要和实例变量重名,下文中将给出为什么不要这样做的解答。 - 仅使用
@property
装饰用于访问属性的方法可以定义只读属性。以下是一个尝试修改只读属性的例子:
错误信息为:class Aliemo(object): def __init__(self, name, money): self._name = name self._money = money @property def name(self): return self._name a = Aliemo("Boss.A", "Inf") a.name = "Kersen"
AttributeError: can't set attribute 'name'
。 - 再次重申:属性的方法名不要和实例变量重名。例如,以下的代码是错误的:
错误信息为:class Aliemo(object): def __init__(self, name, money): self._name = name self._money = money @property def name(self): return self.name a = Aliemo("Boss.A", "Inf") print(a.name)
居然爆栈了,这是怎么会是呢?[Previous line repeated 996 more times] RecursionError: maximum recursion depth exceeded
因为调用a.name
时,首先转换为方法调用,在执行return self.name
时,因为self
也是一个实例,访问self
的属性又转换为方法调用,从而造成了无限递归,最终导致了栈溢出报错。
多重继承
- 如题,定义类时可以使该类从继承多个父类的属性与方法。如果不同的父类里有同名方法,则继承传参时顺序靠前的父类的方法。
- 举个应该比较简明易懂的例子:
输出结果为:class Oier(object): def __init__(self, name): self._name = name class Arknights(object): def __init__(self, level): self._level = level def Print(self): print(1) class Setu(object): def __init__(self, number): self._number = number def Print(self): print(2) class Aliemo(Oier, Arknights, Setu): pass a = Aliemo("yu__xuan") print(dir(a)) a.Print()
可以发现,虽然三个父类都有['Print', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_name'] 1
__init__
方法,但是Aliemo
仅继承了Oier
的方法获得了_name
属性。更直观地,Print
方法的输出是1
,说明该方法来自Ariknights
而非Setu
。 - 多重继承的使用方法如上所示,仅需把父类按所需顺序排列在括号内即可。
定制类
-
为一个自定义类定义形如
__xxx__
的特殊方法,可以实现一些非常方便的功能,如上文中的__slots__
、__len__
。以下仅简单记录几个例子,详见 Python 的官方文档。 -
__str__
:一个返回值必须为str
类型的方法,规定了实例转化为str
类型的值,例如:class Aliemo(object): def __init__(self, name): self._name = name def __str__(self): return ("I'm nb %s" % (self._name)) print(Aliemo("lkp")) x = Aliemo("lkp") print(x) a = str(x) print(a)
输出为:
I'm nb lkp I'm nb lkp I'm nb lkp
这样打印出来的实例,不但好看,而且可以检查实例内部重要的数据。
-
__repr__
:一个返回值必须为str
类型的方法,规定了在交互式解释器中直接调用实例时转化为str
类型后的值,例如:>>> class Aliemo(object): ... def __init__(self, name): ... self._name = name ... def __str__(self): ... return ("I'm nb %s" % (self._name)) ... __repr__ = __str__ ... >>> x = Aliemo("lkp") >>> x I'm nb lkp >>>
上述写法是我们常常采用的偷懒的方式。
和
__str__
的区别:__str__()
返回用户看到的字符串,而__repr__()
返回程序开发者看到的字符串,也就是说__repr__()
是为调试服务的。 -
__iter__
和__next__
:使实例可以实现Iterator
的迭代功能。将这个实例用于迭代时,__iter__
方法将返回一个有__next__
方法的对象,之后 python 解释器会不断调用返回的对象的__next__
方法获取迭代的值,直至抛出StopIteration
停止迭代。举例:class Fib(object): def __init__(self): self.a, self.b = 0, 1 def __iter__(self): return self # 实例本身就是迭代对象,故返回自己 def __next__(self): self.a, self.b = self.b, self.a + self.b # 计算下一个值 if self.a > 10: # 退出循环的条件 raise StopIteration() return self.a # 返回下一个值 print(list(Fib())) for i in Fib(): print(i, end = ' ')
输出为:
[1, 1, 2, 3, 5, 8] 1 1 2 3 5 8
-
__getitem__
:使实例可以实现类似list
的取出指定位置的元素和切片的功能,举例:实现取指定位置的元素:
class Fib(object): def __getitem__(self, n): a, b = 0, 1 for i in range(1, n): a, b = b, a + b return b f = Fib() print(f[10])
在此基础上实现切片。切片的类型是
slice
,因此我们可以对传入的参数进行判断:class Fib(object): def __getitem__(self, n): a, b = 0, 1 if isinstance(n, int): for i in range(1, n): a, b = b, a + b return b if isinstance(n, slice): start = n.start stop = n.stop step = n.step ret = [] for i in range(1, start): a, b = b, a + b for i in range(start, stop + 1): if (i - start) % step == 0: ret.append(b) a, b = b, a + b return ret f = Fib() print(f[3:10:1]) print(f[3:10:2])
输出为:
[2, 3, 5, 8, 13, 21, 34, 55] [2, 5, 13, 34]
不过上述的程序还是有些简陋,比如无法对负数下标进行处理等。
此外,如果把对象看成 dict,
__getitem__()
的参数也可能是一个可以作key
的object
,例如str
。与__getitem__()
对应,还有__setitem__()
方法,用于把对象视作 list 或 dict 来对集合赋值;此外还有__delitem__()
方法,用于删除某个元素。 -
通过上面的方法,我们自己定义的类表现得和 Python 自带的 list、tuple、dict 没什么区别,这完全归功于动态语言的“鸭子类型”,仅需带有某个接口即可,不需要强制继承某个接口。
-
__getattr__
:用于处理调用的类的方法和属性不存在的情况,可以动态地返回属性和方法,例:class Aliemo(object): def __init__(self, name): self._name = name def __getattr__(self, attr): if attr == 'money': return 114514 if attr == 'fkxq4': return lambda: print("fkxq4, vw50!") else: raise AttributeError('\'Aliemo\' object has no attribute \'%s\'' % attr) x = Aliemo("lkp") print(x._name) print(x.money) x.fkxq4() print(x.sb)
输出为:
lkp 114514 fkxq4, vw50! Traceback (most recent call last): File "c:\Users\Luckyblock233\Desktop\work_station\test.py", line 15, in <module> print(x.sb) File "c:\Users\Luckyblock233\Desktop\work_station\test.py", line 10, in __getattr__ raise AttributeError('\'Aliemo\' object has no attribute \'%s\'' % attr) AttributeError: 'Aliemo' object has no attribute 'sb'
这种完全动态调用的特性可以针对完全动态的情况作调用,如我们可以根据动态的 API 调用创建动态的 URL 生成:
class Chain(object): def __init__(self, path=''): self._path = path def __getattr__(self, path): return Chain('%s/%s' % (self._path, path)) def __str__(self): return self._path __repr__ = __str__ print(Chain("kokodayo").a.b.c)
输出为:
kokodayo/a/b/c
。 -
__call__
:可以实现对实例的直接调用,例:class Aliemo(object): def __init__(self, name): self._name = name def __call__(self, rank): print(self._name, "is No.%d nb" % (rank)) a = Aliemo("lkp") a(1)
输出为:
lkp is No.1 nb
。这个例子展示了函数和实例之间的统一,我们完全可以把函数和实例看做是一莲托生的关系。
可以通过
callable()
函数判断某个对象能否被调用,能被调用的对象就是一个Callable
对象:>>> callable(Aliemo) True >>> callable(max) True >>> callable([1, 2, 3]) False >>> callable(None) False >>> callable('str') False
枚举类
- Python 中不存在单独的常量类型。尽管我们可以通过用大写字母作为变量名标记一个对象为常量,但我们始终无法阻止对象被修改。如果我们想要定义一系列常量的话,更好的方法是使用枚举(Enum)类。
- 使用前需要:
from enum import Enum
。 - 如果我们要定义一系列描述月份的常量,可以这么写:
输出为:from enum import Enum Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')) print(type(Month), Month) print(type(Month.Jan), Month.Jan) print(type(Month['Jan']), Month['Jan']) print(type(Month.Jan.value), Month.Jan.value) print(type(Month.__members__), Month.__members__) for name, member in Month.__members__.items(): print(name, member, member.name, member.value)
我们获得了一个<class 'enum.EnumMeta'> <enum 'Month'> <enum 'Month'> Month.Jan <enum 'Month'> Month.Jan <class 'int'> 1 {'Jan': <Month.Jan: 1>, 'Feb': <Month.Feb: 2>, 'Mar': <Month.Mar: 3>, 'Apr': <Month.Apr: 4>, 'May': <Month.May: 5>, 'Jun': <Month.Jun: 6>, 'Jul': <Month.Jul: 7>, 'Aug': <Month.Aug: 8>, 'Sep': <Month.Sep: 9>, 'Oct': <Month.Oct: 10>, 'Nov': <Month.Nov: 11>, 'Dec': <Month.Dec: 12>} Jan Month.Jan Jan 1 Feb Month.Feb Feb 2 Mar Month.Mar Mar 3 Apr Month.Apr Apr 4 May Month.May May 5 Jun Month.Jun Jun 6 Jul Month.Jul Jul 7 Aug Month.Aug Aug 8 Sep Month.Sep Sep 9 Oct Month.Oct Oct 10 Nov Month.Nov Nov 11 Dec Month.Dec Dec 12
Month
类型的枚举类。Month
枚举类的__members__
是一个mappingproxy
(只读字典)类型的对象,建立了常量名到枚举类型的映射。我们可以通过Month.xxx
或Month['xxx']
来引用一个常量,调用name
属性获取常量的名称,调用value
属性获取常量的值。value
属性是自动赋给成员的int
常量,默认从1
开始计数,按照创建时的顺序依次给成员赋值。 - 由于枚举类型中的映射是一一对应的,我们还可以通过
value
的值反过来引用常量:
输出为:print(type(Month(1))) print(Month(1), Month(1).name, Month(1).value)
<enum 'Month'> Month.Jan Jan 1
- 如果需要调整
value
的值则可以从Enum
派生出自定义类:
输出为:from enum import Enum, unique @unique class TouhouProject(Enum): Reimu = 1 # Marisa = 1 Rumia = 10 Cirno = 9 Utsuho = 6 for name, member in TouhouProject.__members__.items(): print(name, member, member.value)
Reimu TouhouProject.Reimu 1 Rumia TouhouProject.Rumia 10 Cirno TouhouProject.Cirno 9 Utsuho TouhouProject.Utsuho 6
@unique
装饰器可以检查枚举中有无重复值,如果有则报错。使用前需要从enum
模块中导入unique
。
错误、调试、测试
- 错误包括 bug、不合法用户行为和异常。
- 通过单步执行代码等方法,跟踪程序的进行来检查程序是否正确工作的行为叫做调试。
- 测试就是字面意思。
错误处理
- 使用 Python 的
try...except...finally...
机制可以识别一个代码块运行时的错误类型并产生特定的反应,之后跳过代码块的未执行部分,继续执行程序。 - 先用一个简单的例子说明:
输出结果为:try: print('try...') r = 10 / 0 print('result:', r) except ZeroDivisionError as e: print('except:', e) print('END')
当执行到try... except: division by zero END
try
中的10/0
部分时抛出了ZeroDivisionError
的错误,与except
语句所识别的类型一致而被捕获,于是10/0
之后的语句被跳过,而except
之后的语句被执行了。expect
后的as
可有可无,它可以将错误信息以字符串的形式赋值给了后面的变量。如果except
之后没有具体的错误类型,那么一切错误都会被捕获。 finally
语句会在所有情况下的try
正常结束后进行,无论抛出了错误被expect
捕获后,还是try
是正常地执行完所有语句(甚至包括return
和exit
语句)结束后,还是。举个例子:
输出为:def Div(x): try: print('try...') r = 10 / x print('result:', r) except ZeroDivisionError as e: print('ZeroDivisionError:', e) finally: print('finally...') Div(0) Div(1)
关于try... ZeroDivisionError: division by zero finally... try... result: 10.0 finally...
try
中有return
和exit
的例子:
输出为:def test(): try: print("try") # return exit(0) except Exception as e_: print("except") finally: print("finally") print("after finally") test()
try finally
- 但是需要注意的是,如果
try
中抛出了except
无法捕获的错误,那么程序仍然会非正常终止,但是会先执行完finally
中的内容再终止程序。如:
终端输出为:try: print('try...') r = 10 / 'a' print('result:', r) except ZeroDivisionError as e: print('except:', e) finally: print('finally...') print('END')
try... finally... Traceback (most recent call last): File "c:\Users\Luckyblock233\Desktop\work_station\test.py", line 3, in <module> r = 10 / 'a' TypeError: unsupported operand type(s) for /: 'int' and 'str'
- 举一个更复杂的例子:
输出结果为:def Div(x, y): try: print('try...') r = x / y print('result:', r) except TypeError as e: print('TypeError', e) except ZeroDivisionError as e: print('ZeroDivisionError:', e) else: print('OK!') Div(10, 0) Div(10, 'a') Div(10, 1)
从例子中我们可以发现,允许有多个try... ZeroDivisionError: division by zero try... TypeError unsupported operand type(s) for /: 'int' and 'str' try... result: 10.0 OK!
except
存在以捕获多种不同的错误,多个except
的检查顺序是从上到下的。还可以在所有的except
之后添加else
语句,当没有可以被except
捕获的错误时,try
后的语句执行完后将会执行else
之后的语句,此时程序将不会非正常终止。 - Python 的错误类型本质上是类,因此错误之间也存在继承关系。所有的错误都继承自
BaseException
。常见的错误类型和继承关系可以查阅:https://docs.python.org/3/library/exceptions.html#exception-hierarchy。 - 举个例子:
上述代码中第二个try: foo() except ValueError as e: print('ValueError') except UnicodeError as e: print('UnicodeError')
except
永远也捕获不到UnicodeError
。因为UnicodeError
是ValueError
的子类,就算抛出了UnicodeError
也会被第一个except
捕获。 - 使用
try...except
捕获错误还有一个巨大的好处,就是可以跨越多层调用捕获错误。举个直观的例子:
输出为:def Div(x): r = 10 / x def Do(x): return Div(x) def test(x): try: Do(x) except Exception as e: print('Error:', e) test(0)
因此我们只要在合适的层次去捕获错误即可,不需要在所有可能出错的地方都尝试捕获错误,这样大大降低了编写错误处理的难度。Error: division by zero
- 当我们的代码在多层调用中抛出错误时,如果错误没有被捕获,它就会一直往上抛,最后被 Python 解释器捕获,打印一串很长的错误信息,然后程序退出。解读错误信息是定位错误的关键。我们从上往下可以看到整个错误的调用函数链。
- 举个例子:
错误信息为:def Div(x): return 10 / x def Do(x): return Div(x) def test(x): Do(x) test(0)
Traceback (most recent call last): File "c:\Users\Luckyblock233\Desktop\work_station\test.py", line 10, in <module> test(0) File "c:\Users\Luckyblock233\Desktop\work_station\test.py", line 8, in test Do(x) File "c:\Users\Luckyblock233\Desktop\work_station\test.py", line 5, in Do return Div(x) File "c:\Users\Luckyblock233\Desktop\work_station\test.py", line 2, in Div return 10 / x ZeroDivisionError: division by zero
- 让我们来分析一下这个错误信息:
第 1 行Traceback (most recent call last):
告诉我们以下是错误的跟踪信息,之后的两行表示在test
函数中出错,并且是在test
函数里调用Do
函数时出错。第 4 行表示是Do
函数的return Div(x)
语句出了问题,第 5 行表示是Div
函数中的return 10 / x
语句出错,并且在最后一行告知了我们错误的准确类型,至此我们找到了错误源头。 - 可以发现错误信息在被给出时呈一个栈的形式,我们称之为异常栈。出错的时候,一定要分析错误的调用栈信息,才能定位错误的准确位置。
- 虽然使用
try...except
可以忽略代码中的错误并继续运行代码,但我们丢失了对 debug 来说非常重要的异常栈。当我们既想保留异常栈的错误信息,又想忽略错误使代码继续执行时,应当怎么做呢?
Python 提供了使用起来非常简单的logging
模块来记录错误信息,举个例子:
终端输出为:import logging def div(): return 10 / 0 def test(): try: div() except Exception as e: logging.exception(e) test() print('END')
ERROR:root:division by zero Traceback (most recent call last): File "c:\Users\Luckyblock233\Desktop\work_station\test.py", line 8, in test div() File "c:\Users\Luckyblock233\Desktop\work_station\test.py", line 4, in div return 10 / 0 ZeroDivisionError: division by zero END
- 上文中提到,Python 中的错误都是
class
。因此我们可以自定义错误类型,也可以自定义何时应当抛出错误。 - 使用
raise
可以抛出指定的错误和错误信息。使用方法为:raise (错误名)((错误信息))
。举个简单的例子:
终端输出为:class NoMoneyError(Exception): pass def fkxq4(money): if money < 50: raise NoMoneyError('only have %d' % money) else: print('oishii') fkxq4(49)
可以发现异常栈中跟踪到了我们自定义的错误类型Traceback (most recent call last): File "c:\Users\Luckyblock233\Desktop\work_station\test.py", line 10, in <module> fkxq4(49) File "c:\Users\Luckyblock233\Desktop\work_station\test.py", line 6, in fkxq4 raise NoMoneyError('only have %d' % money) __main__.NoMoneyError: only have 49
NoMoneyError
并输出了自定义的信息。但只有在必要的时候我们才需要自定义错误类型,大多数情况下我们要尽量使用 Python 内置的错误类型(比如ValueError
,TypeError
)。 - 使用
raise
时错误名和错误信息都是可有可无的。当程序正常运行时调用不带参数的raise
则会返回RuntimeError: No active exception to reraise
。
如果在expect
捕获错误后调用不带参数的raise
则会返回expect
捕获的错误。举个例子:
终端输出为:try: 10 / 0 except ZeroDivisionError as e: print('Error:', e) raise
可能会有人感到疑惑:既然我们已经在Error!: division by zero Traceback (most recent call last): File "c:\Users\Luckyblock233\Desktop\work_station\test.py", line 2, in <module> 10 / 0 ZeroDivisionError: division by zero
except
中捕获了错误,又何必把它raise
出去呢?岂不是多此一举?
虽然这样看起来有些繁琐,但这种错误处理方法不但没错,而且相当常见。在多层调用中,捕获错误的目的只是记录一下以便于后续追踪。但当前函数没有处理该错误的能力,所以应该把错误继续向上抛出,直到被有一个能处理该错误的函数接受为止。 - 此外,在
except
中raise
一个Error
,还可以把一种类型的错误转化成另一种类型。举个例子:
终端输出为:try: 10 / 0 except ZeroDivisionError: raise ValueError('input error!')
可以发现原有的错误和我们Traceback (most recent call last): File "c:\Users\Luckyblock233\Desktop\work_station\test.py", line 2, in <module> 10 / 0 ZeroDivisionError: division by zero During handling of the above exception, another exception occurred: Traceback (most recent call last): File "c:\Users\Luckyblock233\Desktop\work_station\test.py", line 4, in <module> raise ValueError('input error!') ValueError: input error!
raise
的错误都会出现在错误信息中。但在使用这种写法时要注意错误的转化要符合逻辑。比如,我们不应当将一个IOError
转换成毫不相干的ValueError
。 - 对于某些特殊的对象,我们可以使用
with
关键字来简化try…except…finally
的写法。但是例子有些难找,等到 IO 编程一节再展开说明吧。
调试
- 输出变量法。
- 使用 IDE。
- 断言(assert)用于检查运行过程中某个表达式的结果是否为
True
,若为False
则抛出AssertionError
。举个例子:
终端输出为:a = 0 assert a != 0, 'a == 0' print(10 / a)
Traceback (most recent call last): File "c:\Users\Luckyblock233\Desktop\work_station\test.py", line 2, in <module> assert a != 0, 'a == 0' AssertionError: a == 0
- 然而
assert
会掩盖真实的错误信息,此时我们可以在启动 Python 解释器时添加-O
参数(注意是英文大写字母O
)来关闭assert
。如:$ python -O test.py
logging
可以在不抛出错误的情况下输出自定义信息,并且可以将信息输出到文件中。使用前需要:import logging
。
logging
输出的信息有四个级别,等级自高到低为:debug
、info
、warning
、error
。使用logging
输出信息时必需要指明这条信息的级别,如:
需要注意,如果仅仅按照上面那样写信息是不会输出的。如果要查看不高于某个等级的信息,则需要在logging.debug('1') logging.warning('2') logging.error('3') logging.info('4')
import logging
之后加上一句:logging.basicConfig(level=logging.(...))
,其中(...)
填入字母大写的对应等级。举个例子:
终端输出为:import logging logging.basicConfig(level = logging.DEBUG) s = '0' n = int(s) logging.debug('debuging...') logging.info('n = %d' % n) print(10 / n)
DEBUG:root:debuging... INFO:root:n = 0 Traceback (most recent call last): File "c:\Users\Luckyblock233\Desktop\work_station\test.py", line 7, in <module> print(10 / n) ZeroDivisionError: division by zero
logging
的另一个好处是通过简单的配置,一条语句可以同时输出到不同的地方,比如console
和文件。- 最后祝大家修 1 个 bug 能少 1 个 bug!
单元测试
- 放一个经典笑话:
一个测试工程师走进一家酒吧,要了一杯啤酒 一个测试工程师走进一家酒吧,要了一杯咖啡 一个测试工程师走进一家酒吧,要了 0.7 杯啤酒 一个测试工程师走进一家酒吧,要了 -1 杯啤酒 一个测试工程师走进一家酒吧,要了 2^32 杯啤酒 一个测试工程师走进一家酒吧,要了一杯洗脚水 一个测试工程师走进一家酒吧,要了一杯蜥蜴 一个测试工程师走进一家酒吧,要了一份 asdfQwer@24dg!&*(@ 一个测试工程师走进一家酒吧,什么也没要 一个测试工程师走进家酒吧,又走出去又从窗户进来又从后门出去从下水道钻进来 一个测试工程师走进家酒吧,又走出去又进来又出去又进来又出去,最后在外面把老板打了一顿 一个测试工程师走进一 一个测试工程师走进一家酒吧,要了一杯烫烫烫的锟斤拷 一个测试工程师走进一家酒吧,要了 NaN 杯 Null 1T 测试工程师冲进一家酒吧,要了 500T 啤酒咖啡洗脚水野猫狼牙棒奶茶 1T 测试工程师把酒吧拆了 一个测试工程师化装成老板走进一家酒吧,要了 500 杯啤酒,并且不付钱 一万个测试工程师在酒吧外呼啸而过 一个测试工程师走进一家酒吧,要了一杯啤酒‘;DROPTABLE酒吧 测试工程师们满意地离开了酒吧 然后一名顾客点了一份炒饭,酒吧炸了 作者:李清和 链接:https://www.zhihu.com/question/39441398/answer/1636650160 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
- 将一些测试程序正确性的测试用例用例放到一个测试模块里,就能构成一个完整的单元测试。之后当我们需要修改某份已经通过测试的代码时,只需要再跑一遍单元测试即可快速检查代码的正确性。
- 以下是我们自定义的一个
Store
类,用于进行简单的商店管理。这份代码的文件名为store.py
,它将在下文作为我们的测试对象:from functools import reduce class Store(object): class InvaildItem(Exception): pass class NotEnoughError(Exception): pass def Check(self, type, **kw): #检查操作物品是否合法 for name, number in kw.items(): if (not isinstance(number, int)): raise self.InvaildItem('Item \'%s\' is invaild.' % name) if (type == 'Del'): if (not name in self.items): raise KeyError('No %s in Store.' % name) if (self.items[name] < number): raise self.NotEnoughError('Need %d %s but only have %d %s.'\ % (number, name, self.items[name], name)) def __init__(self, store_name): self.store_name = store_name; self.items = {} def Add(self, **kw): #新增 kw 的物品 self.Check('Add', **kw) for name,number in kw.items(): if (name in self.items.items()): self.items[name] += number else : self.items[name] = number def Del(self, **kw): #删除 kw 的物品 self.Check('Del', **kw) for name, number in kw.items(): self.items[name] -= number if (not self.items[name]): del self.items[name] def List(self): #列出当前所有库存 print(reduce(lambda x, y: x + y, ['-' for i in range(0, 20)])) for name, number in self.items.items(): print('\'%s\':%d' % (name, number)) if (not self.items): print('No items in %s\'s.' % self.store_name) print(reduce(lambda x, y: x + y, ['-' for i in range(0, 20)]))
- 编写单元测试模块时,我们需要引入 Python 自带的
unittest
模块,和我们需要测试的部分。 - 我们需要编写的单元测试部分是一个继承自
unittest.TestCase
的类,类中包含多个测试样例。定义测试样例的途径是声明名称为test_(...)
格式的方法,它们会在测试时被执行,若名称不满足上述格式不会在测试时被执行。如果我们要对Store
的每一个方法进行测试的话,大致框架如下:import unittest from store import Store class TestStore(unittest.TestCase): def test_Init(self): pass def test_Add(self): pass def test_Del(self): pass def test_List(self): pass
- 在每一个测试方法中,我们需要使用
unittest.TestCase
提供的条件判读方法来断言测试结果是否是我们所期望的。常用的方法有以下几种形式:assertequal()
:断言两个参数的值是否相等。如:self.assertEqual(abs(-1), 1) # 断言函数返回的结果与1相等
。assertTrue()
与assertFalse()
判断参数中的表达式是否为真\假。assertRaises()
:断言某个代码块抛出的错误与期望类型是否相同。如:
这段代码将会判断with self.assertRaises(KeyError): d = {} value = d['a']
with
以下的代码块执行过程中抛出的错误是否与指定错误类型相同。但为什么with
语句可以这么写?暂时还不能完全理解,留个坑先。- 2023.3.28 来填坑了。见下文“常用内建模块”中的“contextlib”章节。
- 当上述语句判断结果为假时,该测试方法将会停止执行并在终端中输出该测试样例的错误信息。其他的之后再补充。
- 然后我们就可以写出一个很丑很丑的单元测试了:
import unittest from store import Store class TestStore(unittest.TestCase): def test_Init(self): a = Store('a') self.assertEqual(a.store_name, 'a') self.assertEqual(a.items, {}) def test_Add(self): a = Store('a') a.Add(b = 1) self.assertEqual(a.items['b'], 1) with self.assertRaises(a.InvaildItem): a.Add(c = '1') def test_Del(self): a = Store('a') a.Add(b = 1) with self.assertRaises(a.InvaildItem): a.Del(c = '1') with self.assertRaises(a.NotEnoughError): a.Del(b = 2) a.Del(b = 1) with self.assertRaises(KeyError): a.Del(b = 1) def test_List(self): #不知道该怎么写,留个坑先。 pass
- 那么要如何运行这个单元测试呢?一种方法是在单元测试代码尾部加上这样两行:
此时直接运行单元测试代码文件即可完成测试。终端输出为:if __name__ == '__main__': unittest.main()
.... ---------------------------------------------------------------------- Ran 4 tests in 0.001s OK
- 另一种方法是在命令行通过参数
-m unittest
直接运行单元测试:
这样可以一次批量运行很多单元测试,并且有很多工具可以自动来运行这些单元测试,推荐使用这种方法。$ python -m unittest mydict_test ..... ---------------------------------------------------------------------- Ran 4 tests in 0.000s OK
- 在单元测试中,我们可以编写两个特殊的方法:
setUp()
和tearDown()
。这两个方法会在每次调用测试方法之前和之后分别被执行。
有什么用呢?假设我们需要把测试结果输出到文件中时,就可以在setUp()
中打开文件输出,而在tearDown()
中关闭文件输出。当我们需要在测试时调用数据库时,也可以这样实现数据库的开闭。
简单演示一下使用方法:class TestStore(unittest.TestCase): def setUp(self): #打开文件输出 print('TestStore begin...') def tearDown(self): print('TestStore end...') #关闭文件输出
文档测试
- 上面一节的测试方法似乎有点硬核,有没有什么适合作者这种脑瘫和懒狗的测试方法呢?
- 答案是有的。
- Python 内置的“文档测试”(doctest)模块可以直接提取注释中的代码并执行测试。使用时仅需在文件末尾加上如下所示两行即可。举个简单的例子:
def average(values): """ Computes the arithmetic mean of a list of numbers. >>> print(average([20, 30, 70])) 40.0 """ return sum(values) / len(values) import doctest doctest.testmod()
- doctest 严格按照 Python 交互式命令行的输入和输出来判断测试结果是否正确。当测试结果符合期望时没有特殊的提醒,因此上述代码直接运行时是没有输出的。但当我们把
40.0
改成0
时,输出就变成了:
可以得到非常详尽的错误描述。********************************************************************** File "c:\Users\Luckyblock233\Desktop\work_station\test.py", line 5, in __main__.average Failed example: print(average([20, 30, 70])) Expected: 0 Got: 40.0 ********************************************************************** 1 items had failures: 1 of 1 in __main__.average ***Test Failed*** 1 failures.
- 使用过程中发现一个问题,文档测试部分只能写在定义的类或者函数中,如果直接写在外部代码里则会报错。暂时不知道怎么解释,留个坑先。
IO 编程
- IO 在计算机中指 Input/Output,也就是输入和输出。涉及到数据交换的地方,通常是磁盘、网络等,就需要 IO 接口。
- IO编程中,Stream(流)是一个很重要的概念,可以把流想象成一个水管,数据就是水管里的水,但是只能单向流动。Input Stream 就是数据从外面(磁盘、网络)流进内存,Output Stream 就是数据从内存流到外面去。
- 由于 CPU 和内存的速度远远高于外设的速度,所以,在 IO 编程中,就存在速度严重不匹配的问题。当遇到 CPU 输出数据速度高于磁盘写入速度时,有两种处理方案:
- 一是 CPU 暂停程序的执行,等待输出数据全部写入磁盘后再继续执行,这种模式叫做同步 IO。
- 二是 CPU 不等待磁盘而继续执行后续代码,磁盘在写入完毕后会通知 CPU 继续写入。
- 可以直观看出,异步 IO 的运行效率肯定优于同步 IO,但是异步 IO 的缺点是编写复杂度远大于同步 IO。
文件读写
- 首先,当前目录下有一个以
UTF-8
编码的文本文件1.txt
,它的内容如下:9-nine- Episode 2 is the second volume in a series of supernatural mystery games, with Episode 2 telling a story centered around Sora Niimi. The entries in the the 9-nine- series share the same setting and world, but each entry focuses on a different heroine. 9-nine- is a tale of the town of Shiromitsugawa, host to mysterious Artifacts and the superpowers they bestow on their Users. A tale of a brother and sister with a close bond, and also a murder mystery where they hunt down the culprit behind a series of supernatural murders. Each game has an independent story that can be enjoyed on its own. We hope you enjoy the experience that is the 9-nine- series.
- 使用
open
函数,传入文件名和标示符'r'
,即可打开一个文件并读取其中的内容。如:f = open('1.txt', 'r')
创建一个_io.TextIOWrapper
类型的对象f
,通过对该对象进行操作即可读取文件1.txt
的内容。 - 文件的搜索路径见上文模块章节,如果文件不存在则会抛出
IOError
错误。 - 打开文件后即可通过一些方法读取文件内容:
f.read()
可以一次性读取文件的所有内容,并返回一个储存文件内容的str
对象。
有些奇怪的是,多次调用f.read()
时,第二次及之后都不会有返回值。但当我们重新调用open
时,即可再次正常使用read
。为什么呢?- 可以认为,读取过程中有一个不断扫描文件内容的指针,称为
stream position
。当这个指针向后扫描时就会把扫描过的内容置入输入流中。当指针指向文件尾时则无法继续向后扫描。此时可以认为open
函数的功能是在文件头部创建一个指针,而直接调用read()
则会不断扫描文件直至文件尾。 - 调用
seek
方法可以使指针移动到指定的位置;调用tell
方法则会返回当前指针所在位置,如:
成功地输出了两次f = open('1.txt', 'r') print(f.read()) f.seek(0) print(f.read())
1.txt
的内容。 - 如果文件过大时,直接调用
read()
内存就爆了。此时我们可以反复调用read(size)
,每次最多读取size
个字节的内容。不能确定文件大小时这样做最保险。 - 另外,调用
readline()
可以每次读取一行内容并返回一个str
,调用readlines()
则会一次读取所有内容并返回由各行的str
组成的list
,行末的换行符会被保留。
想要得到不含行末空白字符(空格、制表符、换行符)的字符串可以对字符串调用strip
方法。如:调用s.strip()
即可返回不含行末空字符的字符串s
。
- 最后一步是调用
close
方法关闭文件,如:f.close()
。
文件使用完毕后必须关闭,因为文件对象会占用系统资源,且系统能同时打开的文件数量也是有限的。 - 文件读写时都有可能产生
IOError
,一旦出错就不会调用f.close()
。为了保证无论是否出错都能正确地关闭文件,我们可以使用try ... finally
:try: f = open('1.txt', 'r') print(f.read()) finally: if f: f.close()
- 对于
open()
创建的_io.TextIOWrapper
这种类特殊的类,我们还可以使用with
关键字简化try...finally
的写法。上述写法等价于:with open('1.txt', 'r') as f: print(f.read())
with
关键字只能用于_io.TextIOWrapper
这类特殊的类,它们都有两个特殊的方法:__enter__
和__exit__
。对于_io.TextIOWrapper
来说,其__enter__
方法即创建了对应的对象并将对象返回,__exit__
方法即关闭文件读入。
with
语句的实质是首先调用__enter__
方法,将__enter
中的返回值赋值给as
后的对象,然后执行with
语句下的代码块,最后调用__exit__
方法。 即使代码块中出现错误,也会调用__exit__
方法。在这个例子中,也就是会关闭文件流。 - 之前的例子默认都是读取文本文件,并且是 UTF-8 编码的文本文件。读取二进制文件(图片、视频等)需要用
'rb'
模式,如:f = open('SoraNiimi.jpg', 'rb')
。 - 读取非 UTF-8 编码的文本文件时需要传入
encoding
参数。如读取gbk
编码的文件:f = open('1.txt', 'r', ending = 'gbk')
。 - 如果文本文件中夹杂了一些非法编码的字符,在读取时就会遇到
UnicodeDecodeError
。但open()
函数还接受一个errors
参数表示遇到编码错误时的处理方式。最简单的方式是'ignore'
,即直接忽略:f = open('1.txt', 'r', encoding = 'gbk', errors = 'ignore')
。 - 当在
open
函数中传入标识符w
或wb
(二进制文件)即可往指定文件中写入。如果指定文件不存在则会新建一个指定名称的文件。如:
运行后f = open('1.txt', 'w') f.write('SoraNiimi\n') f.write('SoraNiimi') f.close()
1.txt
就变成了:SoraNiimi SoraNiimi
- 为什么
1.txt
中原有的内容消失了?因为以w
方式写入文件时,如果文件已经存在,则会在open
时首先清空文件(相当于删掉后重新写入一个)然后再写入。如果我们想要在文件尾部追加内容,可以以a
(append)模式写入。 - 各模式的定义和含义可以参考:https://docs.python.org/3/library/functions.html#open。
write
方法的返回值是写入的字符个数,虽然这东西好像没什么用。write
方法可以反复调用,但我们必须保证最后调用了f.close()
来关闭文件。因为操作系统是异步 IO 的——在我们写文件时,操作系统往往不会立刻把数据写入磁盘,而是会将数据缓存在内存中,空闲时再慢慢写入。只有调用了close()
方法,才能保证操作系统把所有数据全部写入了磁盘,否则有数据丢失的风险。因此我们常常使用with
语句来保险:with open('1.txt', 'w') as f: f.write('SoraNiimi\n') f.write('SoraNiimi')
StringIO 和 BytesIO
- 顾名思义,
StringIO
就是在内存中进行类似文件的读写。使用前需要from io import StringIO
。只需创建一个StringIO
对象即可像读写文件一样对它进行操作了。举个例子:
输出为:from io import StringIO f = StringIO('SoraNiimi') #初始化 StringIO 对象 print(f.read()) f.write('\nSukisuki') #在当前指针位置继续写入 f.seek(0) #指针归零 print(f.readlines())
SoraNiimi ['SoraNiimi\n', 'Sukisuki']
- 特别地,使用
getvalue
方法可以无视当前指针位置而直接获得对象str
的值,但是调用后指针将会指向str
的尾部。 - 如果要操作二进制数据,则需要使用 BytesIO。举个例子:
输出为:from io import BytesIO f = BytesIO() f.write('新海天'.encode('utf-8')) print(f.getvalue())
b'\xe6\x96\xb0\xe6\xb5\xb7\xe5\xa4\xa9'
。 - StringIO 和 BytesIO 是在内存中操作 str 和 bytes 的方法,使得操作这两种对象时可以使用和读写文件一致的接口。其作用是将文件缓存在内存中,便于之后一次性序列化到磁盘中。
- 上面这段是抄的,因为现在我还不知道怎么一次性序列化到磁盘中。留个坑先。
操作文件和目录
- 众所周知可以在 cmd 输入各种指令来操作文件和目录,但在 Python 中要怎么做呢?其实 cmd 中的指令只是简单地调用了操作系统提供的接口函数,而Python 内置的
os
模块也可以直接调用接口函数来实现文件和目录的操作。 - 首先需要
import os
。 os.environ
变量中储存了操作系统中定义的所有环境变量。- 操作文件和目录的函数一部分在
os
模块中,一部分在os.path
模块中。 - 使用
os.path.abspath()
函数可以查看指定目录的绝对路径对应的字符串。代码运行时,'.'
代表当前文件所在的目录:
输出为:import os path1 = os.path.abspath('.') #当前目录路径对应的字符串 path2 = os.path.abspath('C:/') #C 盘的路径……虽然这个例子看起来没什么意义 print(path1) print(path2)
C:\Users\Luckyblock233\Desktop\work_station C:\
- 如果要实现路径的拼接和拆分时,不应直接对字符串做操作,而应使用
os.path.join()
和os.path.split()
函数。因为不同系统的路径分隔符不同,Linux/Unix/Mac 使用/
作为路径分隔符,而 Windows 则会使用\
,直接操作字符串会使得代码不具有可移植性。两个函数使用方法举例如下:
输出结果为:import os path = os.path.abspath('.') tar = os.path.join(path, 'my_dir') #得到在 path 目录下创建新目录 my_dir 的路径 print(tar) tar = os.path.join(path, 'my_txt.txt') #得到在 path 目录下创建新文件 my_txt.txt 的路径 print(tar) division = os.path.split(tar) #将一个路径拆分为最后级别的目录或文件名,与该目录或文件所在的目录 print(division) division = os.path.splitext(tar) #在 os.path.split 的基础上,仅保留最后级别的文件扩展名,如果为目录则无扩展名 print(division)
有几点需要注意的地方:C:\Users\Luckyblock233\Desktop\work_station\my_dir C:\Users\Luckyblock233\Desktop\work_station\my_txt.txt ('C:\\Users\\Luckyblock233\\Desktop\\work_station', 'my_txt.txt') ('C:\\Users\\Luckyblock233\\Desktop\\work_station\\my_txt', '.txt')
- 注意
\\
是转义字符\
。上述division[0]
中的\\
和输出到终端里的tar
的\
在字符串层面是等价的。 - 这些合并、拆分路径的函数本质上只是操作字符串的函数,并不要求目录和文件要真实存在。
- 注意
- 学会了如何操作路径后,创建和删除目录可以这么调用:
当import os path = os.path.abspath('.') tar = os.path.join(path, 'my_dir') os.mkdir(tar) #创建新的空目录 tar os.rmdir(tar) #删除空目录 tar
mkdir
准备创建的目录已经存在,或是rmdir
准备删除的目录不存在,或是rmdir
准备删除的目录不为空时都会抛出错误。 - 然后是对文件的一些操作。举例如下:
如果遇到import os path = os.path.abspath('.') tar = os.path.join(path, '1.txt') #获得路径 with open(tar, 'w', encoding = 'utf-8') as f: #新建文件 f.write('SoraNiimi') os.rename('1.txt', 'sora.txt') #重命名 os.remove('sora.txt') #删除
PermissionError
可以参考:https://blog.csdn.net/weixin_44630029/article/details/118021429。
另外需要注意,使用rename
重命名文件时可以通过修改文件扩展名来改变文件类型,但重命名目录后的结果只能是目录。 - 有一点奇怪的是,
os
模块中并没有提供复制文件的函数,因为复制文件并非由操作系统提供的系统调用。理论上我们可以通过文件读写来自行实现复制文件的功能,但我们一般选择调用shutil
模块中的copyfile()
函数。shutil
是Python
中的高级文件操作模块,提供了移动、复制、压缩、解压等操作,恰好与os
模块的功能互补,一起使用使即可基本能完成所有文件的操作。
shutil
模块的内容详见下方常用内建模块中shutil
一节。 - 当我们理解各种路径操作函数本质上是字符串操作函数后,我们便可以利用 Python 的特性来实现一些非常厉害的功能:
在我的电脑上输出为:import os print(os.listdir('.')) #当前目录下的文件和一级子目录和 print([x for x in os.listdir('.') if os.path.isdir(x)]) #过滤出当前目录下所有子目录 print([x for x in os.listdir('.') if '.py' in x]) #过滤出当前目录下所有 .py 文件
['.vscode', 'ACM', 'py.md', 'sth', 'test.c', 'test.cpp', 'test.drawio', 'test.exe', 'test.py', 'tmp.md', '杂记.md'] ['.vscode', 'ACM', 'sth'] ['test.py']
- 最后,给出一个输出当前目录下所有子目录及各的目录文件的程序:
import os def List(path_, spaces): for i in range(0, spaces): print(' ', end = '') print(os.path.split(path_)[1]) if (os.path.isdir(path_)): for i in [x for x in os.listdir(path_)]: List(os.path.join(path_, i), spaces + 4) List(os.path.abspath('.'), 0) os.system('pause')
常用内建模块
- 学这部分的时候直接是大开眼界。
- 我的评价是太厉害啦,Python!
datetime
-
datetime
是 Python 处理日期和时间的标准库。 -
datetime
中常用的有三个类:datetime
用于表示具体的时间,timedelta
用于处理时间的变化,timezone
用于处理时区。为了方便我们一般这么导入模块,不然引用时还需要datetime.datetime.....
:from datetime import datetime, timedelta, timezone
-
获取指定日期和时间:我们可以调用
now
方法获得当前操作系统设定的时区的现在时间,也可以依次传入参数year
、month
、day
、hour
、minute
、second
、microsecond
来直接构造时间:from datetime import datetime now = datetime.now() #当前时间 print(type(now)) print(now) doomsday = datetime(2012, 12, 21, 15, 14, 35, 114514) #世界末日 print(doomsday)
在我运行上述程序的那一刻输出为:
<class 'datetime.datetime'> 2023-03-01 16:38:48.695389 2012-12-21 15:14:35.114514
获取当前时间时为什么要强调当前操作系统设定的时区?详见下文。
-
timestamp
在计算机内部,时间实际上是用一个浮点数表示的。我们把
1970-1-1- 00:00:00 UTC+00:00
的时刻称为epoch time,记为 0,当前时间就是相对于 epoch time 经过的秒数,称为timestamp
,1970 年以前的时间timestamp
为负数,即有:timestamp = 0 = 1970-1-1 00:00:00 UTC+0:00 = 1970-1-1 01:00:00 UTC+1:00 = ... = 1970-1-1 08:00:00 UTC+8:00 = ...
可见
timestamp
的值是绝对的,与时区并无关系。因此计算机内使用timestamp
表示时间——在已校准的情况下,全球各地的计算机在任意时刻的timestamp
完全相同。根据这再考虑上文
datetime.now()
方法获取当前时间的原理:将当前计算机内的timestamp
转化为了当前操作系统设定的时区下的时间。 -
datetime
转换为timestamp
把
datetime
转化为timestamp
仅需调用timestamp()
方法,结果是一个浮点数,整数部分代表秒数:from datetime import datetime doomsday = datetime(2012, 12, 21, 15, 14, 35, 114514) #世界末日 x = doomsday.timestamp() print(type(x), x)
输出为:
<class 'float'> 1356074075.114514
。 -
timestamp
转换为datetime
使用
datetime
类的fromtimestamp()
方法:from datetime import datetime doomsday = datetime(2012, 12, 21, 15, 14, 35, 114514) x = doomsday.timestamp() print(datetime.fromtimestamp(x))
输出为:
2012-12-21 15:14:35.114514
。注意到
timestamp
是没有时区的概念的浮点数,而datetime
是有时区的。上述转换默认是将timestamp
转换为本地时间(本地时间指当前操作系统设定的时区下的时间)。timestamp
也可以直接被转换到UTC
标准时区的时间:print(datetime.utcfromtimestamp(x))
输出为:
2012-12-21 07:14:35.114514
。 -
str
转换为datetime
通过
datetime
类的strptime()
方法实现,需要传入被转换的字符串和一个日期和时间的格式化字符串:from datetime import datetime now = datetime.strptime("2023-3-1 17:00:00", "%Y-%m-%d %H:%M:%S") print(now)
输出为:
2023-03-01 17:00:00
。格式化字符串的写法详见:Python 官方文档,注意上述例子中转换后的datetime
是没有时区信息的。如果要同时输入时区可能需要使用正则表达式进行匹配。如何处理时区详见下文。
-
datetime
转换为str
通过
strftime()
方法实现的,同样需要一个日期和时间的格式化字符串:from datetime import datetime now = datetime.now() print(now.strftime("The current time is %a, %b %d %H:%M:%S"))
在我运行上述程序的那一刻输出为:
The current time is Wed, Mar 01 17:05:05
。 -
使用
timedelta
实现datetime
的变化通过
timedelta
类我们可以描述datetime
的变化量,以此可以实现datetime
的变化。如:from datetime import datetime, timedelta now = datetime.now() print(now) print(now + timedelta(days = 1, hours = 1)) print(timedelta(days = 1) - timedelta (hours = 1)) doomsday = datetime(2012, 12, 21, 15, 14, 35, 114514) print(type(now - doomsday), now - doomsday)
在我运行上述程序的那一刻输出为:
2023-03-01 17:12:19.274752 2023-03-02 18:12:19.274752 23:00:00 <class 'datetime.timedelta'> 3722 days, 1:57:44.160238
注意两个
datetime
类的实例之间进行+
运算是无意义的。 -
使用
timezone
类处理时区datetime
类有一个名为tzinfo
的属性,该属性为timezone
类,描述了对应的datetime
类的实例的时区。我们可以通过传入一个
timedelta
类的参数自定义一个timezone
类的实例,参数描述了该时区与 UTC 标准时区的时间差:utc_0 = timezone.utc utc_8 = timezone(timedelta(hours = 8)) utc_minus_1 = timezone(timedelta(hours = -1)) utc_23_59_59 = timezone(timedelta(hours = 23, minutes = 59, seconds = 59)) print(utc_0, utc_8, utc_minus_1, utc_23_59_59)
输出为:
UTC UTC+08:00 UTC-01:00 UTC+23:59:59
。注意参数必须在(-timedelta(hours=24), timedelta(hours=24))
范围内。 -
如果向上文中一样,在使用
datetime
获取时间时不指明时区,获得的时间在数值上等于当前操作系统设定的时区下的时间,但是tzinfo
属性是不存在的。例:from datetime import datetime, timedelta, timezone now = datetime.now() doomsday = datetime(2012, 12, 21, 15, 14, 35, 114514) print(now.tzinfo) print(doomsday.tzinfo)
输出为:
None None
有了
timezone
类之后,我们就可以在使用datetime
获取时间时指定时区了,例:from datetime import datetime, timedelta, timezone utc_0 = timezone.utc utc_8 = timezone(timedelta(hours = 8)) now = datetime.now(tz = utc_8) print(now.tzinfo, now) now = datetime.now(tz = utc_0) print(now.tzinfo, now) doomsday = datetime(2012, 12, 21, 15, 14, 35, 114514, tzinfo = utc_8) print(doomsday.tzinfo, doomsday) doomsday = datetime(2012, 12, 21, 15, 14, 35, 114514, tzinfo = utc_0) print(doomsday.tzinfo, doomsday)
在我运行上述程序的那一刻输出为:
UTC+08:00 2023-03-01 22:29:51.705176+08:00 UTC 2023-03-01 14:29:51.705176+00:00 UTC+08:00 2012-12-21 15:14:35.114514+08:00 UTC 2012-12-21 15:14:35.114514+00:00
-
replace()
方法:可以任意改变一个
datetime
类的实例的属性,通过修改对应的timestamp
实现。如:from datetime import datetime, timedelta, timezone utc_0 = timezone.utc utc_8 = timezone(timedelta(hours = 8)) now = datetime.now(tz = utc_8) print(now, now.timestamp()) dt = now.replace(hour = 23) print(dt, dt.timestamp()) dt = now.replace(tzinfo = utc_0) print(dt, dt.timestamp())
在我运行上述程序的那一刻输出为:
2023-03-01 22:41:40.513929+08:00 1677681700.513929 2023-03-01 23:41:40.513929+08:00 1677685300.513929 2023-03-01 22:41:40.513929+00:00 1677710500.513929
-
时区转换
我们可以先获取带有
timezone
属性的当前的 UTC 时间,再使用astimezone
方法转换为任意时区的时间。注意astimezone
方法只能对带有timezone
属性的datetime
类的实例操作:from datetime import datetime, timedelta, timezone utc_0 = timezone.utc utc_8 = timezone(timedelta(hours = 8)) utc_9 = timezone(timedelta(hours = 9)) utc_dt = datetime.now(tz = timezone.utc) print(utc_dt) bj_dt = utc_dt.astimezone(utc_8) print(bj_dt) tokyo_dt = utc_dt.astimezone(utc_9) print(tokyo_dt) tokyo_dt = bj_dt.astimezone(utc_9) print(tokyo_dt)
在我运行上述程序的那一刻输出为:
2023-03-01 15:04:54.077513+00:00 2023-03-01 23:04:54.077513+08:00 2023-03-02 00:04:54.077513+09:00 2023-03-02 00:04:54.077513+09:00
collections
collections 是 Python 内建的一个集合模块,提供了许多有用的集合类。
-
namedtuple
用于创建一个自定义的 tuple 类,在 tuple 的基础上规定了元素的个数,并可以用自定义的索引引用 tuple 的元素。 使用
namedtuple
即可方便地定义一种数据类型,避免了繁琐的 class 的使用。例:from collections import namedtuple Lkp = namedtuple("Aliemo", ["name", "money"]) Kersen = Lkp("Kersen", "many") print(isinstance(Kersen, tuple)) print(Kersen) print(Kersen.name, Kersen.money) print(Kersen[0], Kersen[1])
输出为:
True Aliemo(name='Kersen', money='many') Kersen many Kersen many
-
deque
deque
是高效实现插入和删除操作的双向列表。list
的插入和删除都是线性的,直接用于实现 stack 和 queue 时间效率上非常不划算的,此时可以使用deque
来实现。直接上代码:from collections import deque q = deque([1, 2, 3]) q.append(4) #加入队尾 print(q) q.appendleft(0) #加入队首 print(q) q.pop() #删除队尾 print(q) q.popleft() #删除队首 print(q)
输出为:
deque([1, 2, 3, 4]) deque([0, 1, 2, 3, 4]) deque([0, 1, 2, 3]) deque([1, 2, 3])
-
defaultdict
传入一个函数和一个
dict
,在传入的dict
基础上创建一个带有默认值的dict
(也可以不传入dict
,若不传入则创建空dict
),使得引用不存在的key
时不抛出KeyError
而是返回设定的默认值:from collections import defaultdict d = {'a':1} new_d = defaultdict(lambda: "N/A", d) print(new_d['a'], new_d['b'])
输出为:
1 N/A
。除了具有返回默认值的功能之外,
defaultdict
与dict
的功能完全一致。 -
OrderedDict
:dict
内部的key
是无序的,对dict
做迭代时无法确定key
的顺序。而OrderedDict
内的key
会按照插入的顺序排列:from collections import OrderedDict d = OrderedDict({'a': 1, 'c': 2, 'b': 3}) print(d) d['e'] = 4 print(d)
输出为:
OrderedDict([('a', 1), ('c', 2), ('b', 3)]) OrderedDict([('a', 1), ('c', 2), ('b', 3), ('e', 4)])
然而在我测试时发现普通的
dict
的key
也是按照插入顺序进行排列的……没感觉出来OrderedDict
和普通的dict
有什么区别。留个坑先。 -
Counter
Counter
类是dict
的一个子类,用于方便地统计可迭代对象中元素的出现次数。以下以字符串为例:from collections import Counter c = Counter() c.update("I am nb Lkp") print(c['I'], c) c.update("IIIII") print(c['I'], c) for i in "IIIII": c[i] = c[i] + 1 print(c['I'], c)
输出为:
1 Counter({' ': 3, 'I': 1, 'a': 1, 'm': 1, 'n': 1, 'b': 1, 'L': 1, 'k': 1, 'p': 1}) 6 Counter({'I': 6, ' ': 3, 'a': 1, 'm': 1, 'n': 1, 'b': 1, 'L': 1, 'k': 1, 'p': 1}) 11 Counter({'I': 11, ' ': 3, 'a': 1, 'm': 1, 'n': 1, 'b': 1, 'L': 1, 'k': 1, 'p': 1})
-
ChainMap
缺少计算机知识,没看懂例子……留个坑先。
argparse
发现这篇是今年二月刚更新的,连评论都没有。
但是看不懂。留坑。
base64
-
当我们使用记事本直接打开 jpg、pdf 等文件时会出现一堆乱码。因为这些二进制文件经过编码后有许多无法显示和打印的字符。如果我们想要使用记事本实现这些文件的编辑,或是用可见文本的形式对这些文件进行传输和储存,就需要一种能够表示任意二进制数据的编码方法。
-
base64 是一种用 64 个字符来表示任意二进制数据的方法。其编码方式如下:
- 首先我们需要准备 64 个字符,在标准 base64 中它们分别为:
['A'~'Z', 'a'~'z', '0'~'9', '+', '/']
,按照顺序将它们标记为 0 ~ 63。 - 然后将二进制数据进行处理,将二进制数据每 3 个字节分为一组,这一组中一共有 \(3\times 8=24\) 个 bit。再将这 24 个 bit 划分为 4 块,这样每块正好有 6 个 bit,对应 0~63。
- 然后以每块中的数为索引,获得对应编号的字符,按块的顺序排列起来就是编码后的字符串。
- 如果二进制数据不是 3 的倍数,最后一组中少了 1 个或 2 个字节怎么办呢?base64 会用
\x00
字节在末尾补足,再在编码字符串的末尾加上对应数量的=
代表补了多少字节,解码的时候会自动去掉。
- 首先我们需要准备 64 个字符,在标准 base64 中它们分别为:
-
使用 Python 中的
base64
模块即可直接进行 base 64 的编码和解码。传入的参数和结果都是 bytes 类型:import base64 print(base64.b64encode(b"a")) print(base64.b64encode(b"aa")) print(base64.b64encode(b"aaa")) print("") print(base64.b64encode(b"aaaa")) print(base64.b64encode(b"aaaaa")) print(base64.b64encode(b"aaaaaa")) print("") print(base64.b64decode("YWFhYWFh"))
输出为:
b'YQ==' b'YWE=' b'YWFh' b'YWFhYQ==' b'YWFhYWE=' b'YWFhYWFh' b'aaaaaa'
-
通过上述输出可以发现,base64 编码将 3 个字节的二进制数据编码成了 4 个字节的文本数据,文本数据的长度在原来的基础上增加了 \(\frac{1}{3}\)。
-
然而标准的 base64 在某些情况下可能会出现问题。如标准的 base64 中可能出现字符
+
和/
,这样的字符串就不能直接在 URL 中作为参数。在各种奇奇怪怪的需求的驱使下,base64 产生了许多变体。如一种叫urlsafe
的 base64 编码将标准 base64 中的字符+
和/
替换成了-
和_
。例:print(base64.b64encode(b'i\xb7\x1d\xfb\xef\xff')) print(base64.urlsafe_b64encode(b'i\xb7\x1d\xfb\xef\xff'))
上述两个相同的二进制数据,在编码后的字符串分别为:
b'abcd++//' b'abcd--__'
-
由于
=
用在 URL、Cookie 里面会造成歧义,所以,很多 base64 编码后会把=
去掉。解码之前再加上=
使得 base64 编码字符串长度为 4 的倍数即可。
hashlib
-
我先放两个数字在这里:\(10^9+7\),\(1e9+9\)。
-
摘要算法又称哈希算法、散列算法,可以接受任意长度的数据,并将该数据转化成一个长度固定的字符串,称为摘要。
-
摘要具有高度的独特性,两组不同的数据计算出的摘要相同的概率极低,因此通过比较摘要是否相同,可以错误概率极低地判断两组大数据是否相同,或是检查某组大数据是否损坏或是被修改过。
-
hashlib
模块提供了常见的摘要算法,如 MD5,SHA1 等等。我们需要先创建一个所需摘要算法对应的实例,之后调用update()
方法传入一个bytes
类型的数据即可运行算法:import hashlib md5 = hashlib.md5() sha1 = hashlib.sha1() md5.update("Aliemo vw50 eat KFC".encode("utf-8")) sha1.update("Aliemo vw50 eat KFC".encode("utf-8")) print(md5.hexdigest()) print(sha1.hexdigest())
输出为:
a71c7867521fca6a582def2101ad5a73 326d6013868aae8d7aed62c98dbc81e8b97d8d69
当需要被转化的数据较长时,可以按照顺序将数据分块,依次对每块数据调用
update()
,最后计算的结果是一样的(OI 选手可以考虑字符串哈希):import hashlib md5 = hashlib.md5() md5.update("Aliemo vw".encode("utf-8")) md5.update("50 eat KFC".encode("utf-8")) print(md5.hexdigest())
输出为:
a71c7867521fca6a582def2101ad5a73
-
MD5 是最常见的摘要算法,速度很快,生成结果是固定的128 bit/16 字节,通常用一个 32 位的 16 进制字符串表示。另一种常用的摘要算法是 SHA1 ,其结果是 160 bit/20 字节,通常用一个 40 位的 16 进制字符串表示。显然,摘要越长,发生哈希碰撞(两组不同的数据计算出的摘要相同)的概率越低,就越安全。比 SHA1 更安全的算法是 SHA256 和 SHA512,不过越安全的算法不仅越慢,而且摘要长度更长。
-
摘要算法的应用:
- 数据库中不直接以明文保存用户口令,而是储存用户口令的摘要,用户登录时比较输入的明文口令的摘要与数据库中储存的摘要是否一致。
- 如果你和 Lb 一样网盘里有点小资源,这个东西你一定不会陌生:
学习了摘要算法之后,我们可以知道.md5
后缀的文件是储存的资源的 MD5 摘要值,计算出我们已下载的资源的摘要并与该文件进行比较,即可判断我们下载的资源在传输过程中是否损坏。
-
“加盐”
假设我们使用人尽皆知的 MD5 计算用户口令的摘要,此时某个黑客拿到了我们储存摘要口令的数据库,他想要从中解析出每个用户的明文密码。使用摘要值反推明文是很困难的,但黑客可以先计算出常用口令的摘要,并对比数据库中的摘要,就获得了使用常用口令的用户账号。为了避免这样的悲剧发生,我们一方面可以强制要求用户设置又臭又长的口令,另一方面我们可以从算法上动一些手脚。我们可以在用户输入的原始口令基础上加上一个复杂的字符串,再计算出新口令的 MD5 值再储存到数据库中。这一方法俗称“加盐”,例:
import hashlib def Calc_MD5(password): md5 = hashlib.md5() md5.update((password + 'the-Salt').encode("utf-8")) return md5 md5 = hashlib.md5() md5.update("Aliemo vw50 eat KFC".encode("utf-8")) print(md5.hexdigest()) saltymd5 = Calc_MD5("Aliemo vw50 eat KFC") print(saltymd5.hexdigest())
输出为:
a71c7867521fca6a582def2101ad5a73 4b2602c6ced38a7a6a07614983542eaa
“加盐”的法则可以非常灵活,比如将登录名作为
Salt
的一部分来计算 MD5,使得使用相同口令的不同用户对应的摘要值也不同。如果不知道我们“加盐”的具体法则,就算已知了数据库中的摘要值,黑客也将无能为力。 -
注意摘要算法不是加密算法,因为我们不能通过摘要值反推明文,只能用于防篡改。利用它的单向计算特性即可在不存储明文口令的情况下验证用户口令。
Hmac
- Keyed-Hashing for Message Authentication。
- 一种标准化的“加盐”算法,需要与某种哈希算法配合使用。
- 其过程详见:https://zhuanlan.zhihu.com/p/336054453。
- 使用
hmac
类的new
方法,依次传入密钥(即“加盐”的混入的数据)、要计算摘要的数据以及某种哈希算法,即可进行标准的 Hmac 算法求得哈希值:
输出为:import hmac message = b"Lkp vw50 eat KFC" key = b"the-Salt" h = hmac.new(key, message, digestmod="MD5") print(h.hexdigest())
d62b6fef9431351aef69fd465dfd9569
- Hmac 的一个典型应用是用在“挑战/响应”(Challenge/Response)身份认证中:
- 客户端向服务器发出一个验证请求。
- 服务器接到此请求后生成一个随机数并通过网络传输给客户端(此为挑战)。
- 客户端将收到的随机数提供给 ePass,由 ePass 使用该随机数与存储在 ePass 中的密钥进行 Hmac 运算并得到一个结果作为认证证据传给服务器(此为响应)。
- 与此同时,服务器也使用该随机数与存储在服务器数据库中的该客户密钥进行 Hmac 运算,如果服务器的运算结果与客户端传回的响应结果相同,则认为客户端是一个合法用户 。
- 来源于:https://blog.csdn.net/pz641/article/details/110876060。
itertools
-
一些用起来很方便的针对迭代器的方法。
-
count(firstval=0, step=1)
:接受两个参数firstval
和step
,返回一个无限的迭代器,其第一个元素为firstval
,之后的每个元素都比前一个元素大step
,两参数均可为实数。例:import itertools n = itertools.count(1) for i in n: print(i)
输出为:
1 2 3 4 ...
停不下来了!此时只能在终端中按
ctrl+c
来强制停止程序。 -
cycle()
:接受一个可迭代对象,返回一个无限迭代器,该迭代器会按顺序依次返回该对象中的元素,到达结尾后会从头再次重复上述过程。例:n = [x for x in range(1, 3)] n = itertools.cycle(n) for i in n: print(i)
输出为:
1 2 1 2 1 ...
-
repeat()
接受一个任意类型的参数,返回一个无限迭代器,该迭代器的每个元素都是传入的参数。传入第二个参数则可以限定重复的次数。n = [x for x in range(1, 3)] n = itertools.repeat(n, 3) for i in n: print(i)
输出为;
[1, 2] [1, 2] [1, 2]
-
takewhile()
,用于截取可迭代对象的一个前缀。接受一个筛选函数和一个可迭代对象,返回一个迭代器,储存了该对象的满足筛选条件的极长前缀。
注意返回的是传入对象的一个满足条件的极长前缀,而不同于使用filter
时会返回所有满足条件的元素。例:import itertools n = [x for x in range(1, 10)] print(list(itertools.takewhile(lambda x: x < 5, n))) print(list(filter(lambda x: x < 5, n))) print(list(itertools.takewhile(lambda x: x > 2, n))) print(list(filter(lambda x: x > 2, n)))
输出为:
[1, 2, 3, 4] [1, 2, 3, 4] [] [3, 4, 5, 6, 7, 8, 9]
-
一个综合的例子。利用 \(\arctan x\) 的傅里叶级数求 \(\pi\) 的近似值,具体地有:
\[\arctan x = \sum_{n=0}^{\infin}\dfrac{(-1)^n x^{2n+1}}{2n+1} (-1<x\le 1) \]取 \(x=1\) 即有:
\[\dfrac{\pi}{4} = 1-\frac{1}{3}+\frac{1}{5} - \dfrac{1}{7} + \dots \]import itertools n = itertools.count(1) n = itertools.takewhile(lambda x: x < 10000000, n) n = map(lambda x: 4 / ((-1) ** (x - 1) * (2 * x - 1)), n) print(sum(n))
输出为:
3.1415927535898014
此无穷级数公式由 (J.Grengory, 1671) 和 (Leibniz, 1673) 提出,开辟了以反正切函数计算 \(\pi\) 的新时代。然而收敛速度非常慢,上述代码中计算了 \(10^7\) 项才精确到小数点后 6 位。
说到这里不得不提超级神级大神拉马努金在 1910 年代凭“直觉”写出的超级牛逼公式了:
\[\dfrac{1}{\pi} = \dfrac{2\sqrt 2}{9801} \sum_{n=0}^{\infin} \dfrac{(4n)!}{(n!)^4}\dfrac{26390n+1103}{396^{4n}} \]取 1 项即可精确到 7 位有效数字,取两项则精确到 16 位有效数字。
我直接跪下来扑通扑通地磕两个头、、、
-
chain()
把一组可迭代对象串联起来形成一个更大的迭代器:import itertools n = itertools.chain("ABC", "DEF", "GHI") print(type(n)) print(list(n))
输出为:
<class 'itertools.chain'> ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I']
-
groupby()
:传入一个可迭代对象,把其中相邻的重复元素进行挑出来放在一起。默认“重复元素”指完全相同的元素。OI 选手可以类比离散化。例:import itertools n = itertools.groupby("AABBBCCCDDDAAA") for key, group in n: print(key, list(group))
输出为:
A ['A', 'A'] B ['B', 'B', 'B'] C ['C', 'C', 'C'] D ['D', 'D', 'D'] A ['A', 'A', 'A']
import itertools n = itertools.groupby(['c', 'c', 1, 2, 2, "AB", "AB", 2]) for key, group in n: print(key, list(group))
输出为:
c ['c', 'c'] 1 [1] 2 [2, 2] AB ['AB', 'AB'] 2 [2]
还可以传入一个函数,以迭代对象中的元素为参数,按照函数返回值判断是否完全相同,以此自定义“重复元素”的含义:
n = itertools.groupby("AabBBCcCddDAaa", lambda x: x.upper()) for key, group in n: print(key, list(group))
输出为:
A ['A', 'a'] B ['b', 'B', 'B'] C ['C', 'c', 'C'] D ['d', 'd', 'D'] A ['A', 'a', 'a']
contextlib
-
来填“单元测试”章节中的坑了。
-
在前文中“单元测试”和“文件读写”章节中,我们都使用了
with
关键字。前者中assertRaises
函数利用with
接受某个代码块抛出的错误,并判断其与期望类型是否相同;后者的open
函数利用with
关闭了文件输出防止了异步 IO 中的数据丢失。在“文件读写”中我们简单地解释了with
关键字的原理,下文中我们给出其原理的更加底层的解释以及其他的应用。 -
什么是 「context」?
- 中文翻译为“上下文”。
- 建议参考:https://www.zhihu.com/question/26387327。
- 个人理解:实现某个功能所需要的“环境”或是“跟随信息”,也就是用于确定该功能具体含义并使之运行所需的“上文”。
-
什么是 「上下文管理器协议」?
- 在一个类中,只要实现以下方法,就实现了上下文管理器协议:
__enter__
:在进入 with 语法块之前调用,返回值会赋值给 with 的 target。__exit__
:在退出 with 语法块时调用,一般用作异常处理。
-
with
关键字的使用形式:with context_expression [as target(s)]: with-body
其中
context_expression
是一个实现了「上下文管理器协议」的对象。运行至这一部分时,首先会调用context_expression
中的__enter__
方法,将__enter__
中的返回值赋值给as
后的target
,然后执行with
语句下的代码块with-body
,最后调用__exit__
方法。 如果代码块中出现错误也会调用__exit__
方法,并会将错误信息作为参数传入其中。 -
一个例子:
class Aliemo: def __init__(self, name): self.name = name def __enter__(self): print("Hajimaruyo!") return self def __exit__(self, exc_type, exc_value, exc_tb): print('exc_type: %s' % exc_type) print('exc_value: %s' % exc_value) print('exc_tb: %s' % exc_tb) print("owaru!") with Aliemo("lkp") as lkp: print("%s have %d yuan" % (lkp.name, 114514)) print("KimitoKanojonoKoi")
上述代码中没有抛出错误,输出为:
--Hajimaruyo! lkp have 114514 yuan exc_type: None exc_value: None exc_tb: None --owaru! KimitoKanojonoKoi
-
当我们把
with-body
修改成print("%s have %d yuan" % (lkp.name, 114514 / 0))
之后,输出变为:--Hajimaruyo! exc_type: <class 'ZeroDivisionError'> exc_value: division by zero exc_tb: <traceback object at 0x000001925AA173C0> --owaru! Traceback (most recent call last): File "c:\Users\Luckyblock233\Desktop\work_station\test.py", line 14, in <module> print("%s have %d yuan" % (lkp.name, 114514 / 0)) ZeroDivisionError: division by zero
__exit__
接受了with-body
抛出的错误,并输出了错误信息。这也是“单元测试”章节中
assertRaises
接受某个代码块抛出的错误,并判断其与期望类型是否相同的原理。 -
回到本章节的标题:
contextlib
是什么? -
@contextmanager
:虽然使用
with
语句替代try...finally...
降低了调用时的编写难度,但是编写__enter__
和__exit__
方法还是很麻烦。因此 Python 的标准库contextlib
提供了更简单的写法,上面的代码可以改为如下形式:from contextlib import contextmanager class Aliemo: def __init__(self, name): self.name = name @contextmanager def Create_Aliemo(name): print("--Hajimaruyo!") ret = Aliemo(name) yield ret print("--owaru!") with Create_Aliemo("lkp") as lkp: print("%s have %d yuan" % (lkp.name, 114514 / 1)) print("KimitoKanojonoKoi")
输出为:
--Hajimaruyo! lkp have 114514 yuan --owaru! KimitoKanojonoKoi
解释一下:装饰器
@contextmanager
接受一个generator
,generator
中yield
语句以及之前的部分相当于__enter__
方法,用yield
语句返回了赋值给with...as
后的target
的对象,yield
语句之后的部分相当于__exit__
方法。这样我们就可以把装饰后的generator
用于with
语句了。不过这样实现上下文管理器似乎无法实现上个例子中在
__exit__
进行错误处理的功能……留个坑先。 -
有时我们希望在某段代码执行前后自动执行特定代码,而使用
@contextmanager
可以很方便地实现这个功能。如:from contextlib import contextmanager @contextmanager def tag(name): print("<%s>" % name) yield print("</%s>" % name) with tag("h1"): print("I love Miyuki.")
输出为:
<h1> I love Miyuki. </h1>
太厉害啦!
-
有一点需要注意:如果被装饰的
generator
内发生了异常,则需要在该generator
中进行异常处理,否则将不会执行yield
之后的语句:from contextlib import contextmanager @contextmanager def test(): print("--Hajimaruyo!") try: yield "no save and no load" a = 1 / 0 finally: print("--owaru!") with test() as t: print(t)
输出为:
--Hajimaruyo! no save and no load --owaru! Traceback (most recent call last): File "c:\Users\Luckyblock233\Desktop\work_station\test.py", line 28, in <module> with test() as t: File "C:\Users\Luckyblock233\AppData\Local\Programs\Python\Python310\lib\contextlib.py", line 142, in __exit__ next(self.gen) File "c:\Users\Luckyblock233\Desktop\work_station\test.py", line 24, in test a = 1 / 0 ZeroDivisionError: division by zero
-
@closing
:作用于已经实现了
close
方法的实例上。使用with closing(var) as target
即可将var
赋值给targer
,并在执行完with
中的代码块后自动调用实例var
的close
方法。如:from contextlib import closing class test(): def __init__(self, name): self.name = name def close(self): print("--owaru!") with closing(test("rice")) as f: print("%s suki" % f.name)
输出为:
rice suki --owaru!
实际上,
closing
也是一个被@contextmanager
装饰的generator
,本质上它等价于:from contextlib import contextmanager @contextmanager def closing(target): try: yield target finally: target.close()
它的作用就是把任意对象变为上下文对象并支持
with
语句。
shutil
-
来填操作文件和目录章节的坑了。
-
高级的目录和文件操作库,主要针对文件和目录的复制、删除、移动、压缩和解压。
-
为了把小人立绘从马娘解包文件里提取出来才来学的,呃呃呃呃,哈哈!
-
此处仅简单记录常用函数的用法,详见官方参考文档。
-
copy(src, dst, *, follow_symlinks=True)
:对文件src
进行复制操作:- 当
dst
为一目录时,将文件src
复制到目录dst
下。 - 当
dst
为一文件名时,在对应目录下创建名为dst
的src
的副本,如果dst
已经存在则使用src
的内容覆写dst
。 - 后面俩参数不懂,略。
- 返回值为复制结果的路径。
- 使用例如下,在当前目录下创建文件
1.txt
和目录newfolder1
,并将1.txt
复制到目录newfolder
下两次:import os import shutil pos = os.path.abspath('.') tar = os.path.join(pos, '1.txt') with open(tar, 'w', encoding='utf-8') as f: f.write('Admire Vega aishiteru!') newfolder1 = os.path.join(pos, 'newfolder1') try: os.mkdir(newfolder1) except: pass print(shutil.copy(tar, newfolder1)) newfile = os.path.join(newfolder1, '2.txt') print(shutil.copy(tar, newfile))
- 注意
copy()
并不会保留如文件的创建和修改时间等元数据。如果要保留此类元数据请使用下述copy2()
函数。 - 理论上
copy()
也可以复制目录的,但是一直报错PermissionError
,不懂了,留个坑先。如果要复制目录请使用下面的copytree()
- 当
-
copy2(src, dst, *, follow_symlinks=True)
:功能基本同上,但是保留如文件的创建和修改时间等元数据。 -
copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2, ignore_dangling_symlinks=False, dirs_exist_ok=False)
:对目录src
递归地进行复制操作,创建目录dst
并将src
中的所有内容递归地复制到dst
中。- 参数
symlinks
和ignore_dangling_symlinks
不懂,略。 copy_function
指定进行目录中的文件复制时使用的方法。- 参数
ignore
用于指定复制的文件和目录。它是一个接受该子目录的内容和路径作为参数的可调用对象,将会返回一个列表,表示需要在该子目录的复制中忽略的文件或目录的特征。在每次递归访问到某个子目录时都会调用该可调用对象。该可调用对象一般通过shutil.ignore_patterns()
构造,也可以自定义并增加一些牛逼功能。如何使用详见官方文档。 - 参数
dirs_exist_ok
指定是否在dst
已存在时抛出错误,若设为True
则不会抛出错误,并且在复制时保留dst
中原有的文件,如果出现同名文件处理方法由copy_function
指定的方法进行处理。 - 返回值为复制结果的路径。
- 使用例如下,在当前目录下创建含有文件
1.txt
的目录newfolder1
,并将newfolder1
以名称newfolder2
复制到当前目录下;然后忽略1.txt
并将newfolder1
以名称newfolder3
复制到目录newfolder2
下。import os import shutil pos = os.path.abspath('.') tar = os.path.join(pos, '1.txt') with open(tar, 'w', encoding='utf-8') as f: f.write('Admire Vega aishiteru!') newfolder1 = os.path.join(pos, 'newfolder1') try: os.mkdir(newfolder1) except: pass shutil.copy(tar, newfolder1) newfolder2 = os.path.join(pos, 'newfolder2') print(shutil.copytree(newfolder1, newfolder2, dirs_exist_ok=True)) newfolder3 = os.path.join(newfolder2, 'newfolder3') print(shutil.copytree(newfolder1, newfolder3, ignore=shutil.ignore_patterns('1.txt'), dirs_exist_ok=True))
- 一个自定义
ignore
接收的对象的例子,使之在访问到每层子目录时输出当前目录的路径与目录下的文件内容。
输出为:import os import shutil def _logpath(path, names): print('Working in %s and there\'re %s' % (path, names)) return [] #nothing will be ignored, if sth. need to be ignored then modify it pos = os.path.abspath('.') tar = os.path.join(pos, '1.txt') with open(tar, 'w', encoding='utf-8') as f: f.write('Admire Vega aishiteru!') newfolder1 = os.path.join(pos, 'newfolder1') try: os.mkdir(newfolder1) os.mkdir(os.path.join(newfolder1, 'ayabe suki')) except: pass shutil.copy(tar, newfolder1) newfolder2 = os.path.join(pos, 'newfolder2') shutil.copytree(newfolder1, newfolder2, ignore= _logpath, dirs_exist_ok=True)
Working in C:\Users\Luckyblock233\Desktop\work_station\newfolder1 and there're ['1.txt', 'ayabe suki'] Working in C:\Users\Luckyblock233\Desktop\work_station\newfolder1\ayabe suki and there're []
- 参数
-
move(src, dst, copy_function=copy2)
:将文件或目录src
移动到目录dst
下。- 本质上还是一种
copy
,参数copy_function
指定了移动时的方法。 - 使用例如下:在当前目录下创建文件
1.txt
和目录newfolder1
,并将1.txt
移动到目录newfolder
下:import os import shutil pos = os.path.abspath('.') tar = os.path.join(pos, '1.txt') with open(tar, 'w', encoding='utf-8') as f: f.write('Admire Vega aishiteru!') newfolder1 = os.path.join(pos, 'newfolder1') try: os.mkdir(newfolder1) except: pass shutil.copy(tar, newfolder1) print(shutil.move(tar, newfolder1))
- 本质上还是一种
-
shutil.rmtree(path, ignore_errors=False, onerror=None, *, dir_fd=None)
:递归地删除某个目录path
。- 参数
ignore_errors
指定是否忽略删除失败。若设为True
则忽略并继续执行程序,若设为False
则通过调用参数oneror
指定的处理程序来处理,若没有指定oneror
则会抛出异常。 - 参数
oneror
是一个接收三个参数function, path, and excinfo
的可调用对象。function
表示抛出异常的函数,path
为传入function
的路径,excinfo
为sys.exc_info()
返回的异常信息。 - 如果要删除文件请使用
os.remove()
。 - 使用例:
import os import shutil pos = os.path.abspath('.') tar = os.path.join(pos, '1.txt') with open(tar, 'w', encoding='utf-8') as f: f.write('Admire Vega aishiteru!') newfolder1 = os.path.join(pos, 'newfolder1') try: os.mkdir(newfolder1) except: pass os.system('pause') os.remove(tar) shutil.rmtree(newfolder1)
- 感觉处理异常好恶心、、、
oneror
相关内容之后看心情补充。
- 参数
-
shutil.make_archive(base_name, format[, root_dir[, base_dir[, verbose[, dry_run[, owner[, group[, logger]]]]]]])
:压缩。 -
shutil.get_archive_formats()
:获得压缩格式。 -
shutil.get_archive_formats()
:解压。 -
暂时用不到,之后再补充,留个坑先。
网络编程
简单玩玩。
没有仔细学。
TCP 编程
一个服务端:
import socket
import threading
import time
def tcplink(sock, addr):
print("Accept new connect from %s:%s..." % addr)
sock.send(b"Welcome!")
while True:
data = sock.recv(1024)
time.sleep(1)
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)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('127.0.0.1', 9999))
s.listen(5)
print("Waiting...")
while True:
sock, addr = s.accept()
t = threading.Thread(target = tcplink, args = (sock, addr))
t.start()
一个客户端:
import socket
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'Aliemo', b'Ke2sen', b'yu__xuan']:
s.send(data)
t = s.recv(1024).decode("utf-8")
print(t)
s.send(b"exit")
s.close()
a = input()
写在最后
-
Python 自带的注释实在是太良心了,它真的在尝试教会我,它真的,我哭死。
-
曾想用 py 写点竞赛题练习一下 py,现在一想这完全是在侮辱 py。py 为了灵活的写法和大量集成的功能舍弃了运行时间,我却想用它写追求效率的算法,多么讽刺啊!
-
我终于理解了你!Python!(不是)
-
2022.11.13 完成 IO 编程-操作文件和目录一节。接下来就进入边学边做的实战了,这一篇杂记也就告一段落了。从 10.05 到现在断断续续地边摸边写了 96KB 的 md,令人感慨。
-
最后闲扯两句,推完天天天两周多了,后劲太大了,一种难以释怀的空虚感在心里挥之不去,放一首音乐在这里,留给之后的我破防:
-
2023.2.27 回顾杂记,调整排版,进行了众多细节的修改。充分认识到自己的水平离实际应用还有很多不足,仍需继续不断坚持长期持续频繁学习。顺带一提被上面的音乐又整破防了。
-
2023.6.30 结束期末考试,身心俱疲,学学 py 养生。顺带一提被上面的音乐又整破防了。