迫于博士学院的学分要求,选了门网课,是我用了好多年的python。本想能水则水,但上课的过程中发现了自己一些知识上的漏洞,以流水账的形式记于本文。纯粹为私人复习,不建议阅读。
- 多条件排序。例如,按照名字、姓氏、第二姓氏将花名册排序,如果两个人名字相同,则比较姓氏,如果两人姓氏也相同,则检查其中是否有人有第二姓氏,有第二姓氏的排在后面,如果两人都有第二姓氏,则比较第二姓氏。
# 输入的是一个字典列表,每个元素代表一个人,其中的键包含了:'n', 'p', 'p2',
# n 表示名字,p 表示姓氏,p2 表示第二姓氏
# 例如:
# [{'n': 'Dupont', 'p': 'Laura', 'p2': 'Marie'},
# {'n': 'Martin', 'p': 'Jean'},
# {'n': 'Martin', 'p': 'Jeanneot'},
# {'n': 'Dupont', 'p': 'Alex'},
# {'n': 'Martin', 'p': 'Jean', 'p2': 'Pierre'},
# {'n': 'Martin', 'p': 'Jeanne'},
# {'n': 'Dupont', 'p': 'Alexandre'},
# {'n': 'Dupont', 'p': 'Alex', 'p2': 'Pierre'},
# {'n': 'Martin', 'p': 'Jeanne', 'p2': 'Marie'},
# {'n': 'Dupont', 'p': 'Alex', 'p2': 'Paul'},
# {'n': 'Martin', 'p': 'Jean', 'p2': 'Paul'},
# {'n': 'Dupont', 'p': 'Laura'}]
def tri_custom(liste):
key = lambda x: (x['p'],x['n'],x.get('p2','A')) # 注意这里get 的用法,如果没有键,返回其第二各参数,默认值 'A',因为我们希望把没有第二姓氏的人排在最前面
liste.sort(key=key)
return liste
inspect.getsource()
可以用来查看函数原代码,主要用于提交的内容不通过时,配合pdb.set_trace()
查看错误。合并 list of list 最简单的方法:
x = [[1,2,3], [4,5], ['a', 'c', True]]
sum(x, [])
- 生成器表达式,可以节约内存,在程序遍历时才会逐个元素计算:
square = (x**2 for x in range(1000000))
sym = [x for x in square if str(x)==str(x)[::-1]]
生成器函数,用 yield 取代 return,调用时返回的是一个迭代器,对迭代器施加 next 函数来逐个获取函数的返回值。
调用另一个生成器函数的生成器函数,需要用关键字
yield from
# 这是一个生成器函数,它返回一个数除了1和它本身之外所有的因子。
def divs(n, verbose=False):
for i in range(2, n):
if n%i ==0:
if verbose:
print(f'Trouve diviseur {i} de {n}')
yield i
# 我们想进一步返回一个数因子的因子
def divdivs(n):
for i in divs(n): # 遍历所有因子,没问题
# divs(i) # 不行,因为这样编译器不知道 divdivs 是生成器函数,会认为它返回值为 None
# yield divs(i) # 也不行,因为这样返回的是求解各因子的因子的生成器,而非它们的因子
yield from divs(i) # 可以,
# yield from 关键字表明该函数是生成器函数,
# from 表示从 divs(i) 中取出元素,取完后再进
# 入下一个循环,取出下一个因子的因子。
- Python 解释器只会加载一次外部模块,后续的 import 将不会重新加载。例如
import this
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
import this
# 第二次import 不会打印上述文字
如果在开发过程中,一定要重新加载模块便于调试,可以使用:
from importlib import reload
reload(this)
-
sys.modules
以字典形式存储了当前环境中加载的所有模块,python 正是通过这个变量,来获取模块加载状态的,如果加载的模块已经出现在字典中了,那么就不会重复加载。
import sys
import this
import numpy as np
sys.modules # 返回一个字典,其键包含 'this', 'numpy'(注意不是 'np') 等。
del sys.modules['this']
import this
# 将会重新打印那段 python 的歌谣
获取当前路径,除了用
os.getcwd()
还可以用pathlib.Path.cwd()
。加载模块时的搜索顺序:首先搜索 built-in 中存不存在这个模块;接下来在 sys.path 中搜索是否存在以该模块命名的 .py 文件(或者文件夹(搭配了
__init__.py
)。具体的搜索顺序是这样的:程序运行入口所在的路径,PYTHONPATH 的环境变量中存储的路径,编译 python 时定义的一些路径(?没搞懂这句话什么意思)。可以模块名字的字符串来加载模块,需要用到
importlib.import_module
。
from importlib import import_module
loaded = import_module('numpy') # 和下面的语句功能一样,但这里加载的模块名字就变成了 "loaded" 。
import numpy
numpy is loaded # True
import math
# 等价于
math = import_module('math')
# 用 import module 加载子模块比较繁琐,传入的字符串中不能加句点
from pathlib import Path
# 需要转换成:
tmp = import_module('pathlib')
Path = tmp.Path
del tmp
- 为类设置
property
。property()
可以对类的属性进行封装,并为用户提供设置属性的接口,如下示例为一个热力学温度的类,众所周知,热力学温度将绝对零度定义为0,所有的温度都是正的,因此,在设置时,需要考虑这一情况,把传入的负值归零。
class Temperature:
def __init__(self, kelvin):
self.kelvin = kelvin
# 注意,第一个 self.kelvin 是暴露给用户的属性,它由后文的
# kelvin = property(_get_kelvin, _set_kelvin) 定义
def _get_kelvin(self):
return self._kelvin
def _set_kelvin(self, kelvin):
self._kelvin = max(0, kelvin)
# 这行的含义是,kelvin 是该类的用户接口,为它赋值,
# 并不直接赋值给它本身,而是将该值传入 self._set_kelvin 中,
# 由 self._set_kelvin 为该类“真正”的属性 self._kelvin 赋值,获取 self.kelvin
# 获取的数据流,也由 self._get_kelvin() 的返回值提供。
kelvin = property(_get_kelvin, _set_kelvin)
def __repr__(self):
return f'{self._kelvin}K'
-
__hash__
和__eq__
是类的两个特殊方法,__hash__
可以自定义计算哈希值的方法,如果不实现它,则该类的哈希值是基于其内存 id 计算的。__eq__
提供了比较两个该类实例是否相等的接口,如果不实现,那么两个实例永远不相等(两个变量名表示的是同一个实例的情况除外)。
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __hash__(self):
return (11*self.x + self.y) // 16
def __repr__(self):
return f'Point[{self.x}, {self.y}]'
这两个特殊方法需要满足下面的条件:如果两个对象相等,那么这两个对象的 hash 值一定相等(反之不成立);如果相等性的比较是基于实例属性的,那么哈希值的计算至少也要基于属性。通过实现 __eq__
,就可以直接用 ==
比较两个对象了。通过实现 __hash__
就可以将对象存入 set()
或者作为 dict
的键来使用了(要想让 set()
发挥剔除重复元素的作用,还需要配合 __eq__
)。需要注意的是:如果不实现 __hash__
,采用系统默认的 __hash__
也可以让对象存入集合中,但是如果只实现了 __eq__
没有实现 __hash__
则不能让对象存入集合,会报错说该类型是 unhashable 的。
Python 文档指出:
如果一个类定义了可变对象,并实现了
__eq__()
方法,那么它不应该实现__hash__()
方法,因为可哈希的集合需要保证键的哈希值是不可变的。
如果对可变对象实现了__hash__()
方法,会引发潜在bug,例如根据上面定义的Point
类:
t1, t2 = Point(10, 10), Point(10,10)
s = {t1, t2} # s 中只有一个元素 Point[10,10]
t1 in s, t2 in s
# (True, True)
# 现在改变 t1 的属性:
t1.x = 100
# 此时输出 s 发现其元素也变成了 Point[100,10]
# 再测试归属性:
t1 in s # False
t2 in s # False
出现上述问题的原因是:s 由 t1 和 t2 初始化,由于 t1 先存入,因此计算 t1 的 hash value,并求出其在 hashtable 中的下表,接着存入 t2 时,发现 t2 的 hash value 和 t1 相等,同时根据 __eq__()
,t2 等于 t1,因此 s 便不再存入 t2 了。接下来,改变 t1 的 attribute 后,在 s 中搜索现在的 t1,由于 t1 的attribute 被改变,其对应的 hash value 和 由它计算出的 hash index 都不再是原来的那个了,当然系统会认为 t1 不在 s 中,同时,t2 计算的 hash index 还是原来的那个,但是据此取出该位置存储的值,发现和 t2 并不相等,因此也会认为 t2 不在 s 中。
-
__getattr__()
和getattr()
。__getattr__()
是一个特殊方法。当获取实例的某个方法、属性失败时,该方法被调用。getattr()
是内置函数,它可以设置三个参数:实例名、属性名(字符串形式)、默认值(可缺省),用于获取实例的某个属性或方法,当获取某个属性/方法失败时,会返回默认值。
首先看getattr()
的示例:
class A:
def __init__(self, a):
self.a = a
def print(self):
print('In class A')
a = A('abc')
getattr(a, 'a') # 'abc'
getattr(a, 'b') # AttributeError, 'A' object has no attribute b
getattr(a, 'b', 10) # 10
getattr(a, 'print')() # 也可以获取方法,并调用,打印:In class A
接下来是 __getattr__()
:
class A:
def __init__(self, a):
self.a = a
def print(self):
print('In class A')
def __getattr__(self, attribute):
return (f"You asked for {attribute}, but I'm giving you default")
a = A('abc')
a.b # "You asked for b, but I'm giving you default"
getattr(a, 'b') # "You asked for b, but I'm giving you default"
# 注意下面的调用,设置了 __getattr__() 之后,
# getattr() 的 default 参数将不再起作用
getattr(a, 'b', 100) # "You asked for b, but I'm giving you default"
-
namedtuple
是一个函数,返回一个类,类名就是它的第一个参数,类的属性由第二个参数决定,第二个参数是一个可迭代对象(除了 list tuple set 之类,甚至可以是 iterator),对象中的元素为字符串类型,对应了需要创建的属性。
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
p = Point(1,2) # Point(x=1, y=2)
isinstance(p, Point) # True
isinstance(p, tuple) # True
# 注意,isinstance(p, Point) 这里的 Point 是第一个 Point,而不是 namedtuple 中的 "Point"
abc = namedtuple('Point', ['x', 'y'])
p = abc(1,2) # Point(x=1, y=2)
isinstance(p, tuple) # True
isinstance(p, Point) # False
isinstance(p, abc) # True
namedtuple
创建的实例本质是tuple类型,故是不可变数据类型。可以通过创建 namedtuple 返回值的子类,创建 attribute 不可变的类 ,这样就可以同时定义 __eq__()
和 __hash__()
方法了。
class Point2(namedtuple('Point2', ['x', 'y']))
def __eq__(self, other):
return self.x = other.x and self.y == other.y
def __hash__(self):
return (11* self.x+self.y)//16
q = Point2(10, 100)
q.x = 100 # AttributeError: can't set attribute
-
dataclasses
是namedtuple
之外又一种定义数据存储类型的途径。它的目标仅仅是为了存储数据。
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
email: str=""
p = Person(name='peter', age=12)
print(p)# Person(name='peter', age=12)
p.name = 'jean'
print(p) # Person(name='jean', age=12)
# 通过设置 dataclass 装饰器的参数,可以让其装饰的对象变成不可变类
@dataclass(frozen=True)
class Point:
x: float
y: float
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __hash__(self):
return (11*self.x + self.y) // 16
-
Enum
的作用是集中为某些常量赋予一些有意义的名称,它的作用是作为一些自定义类的父类。
from enum import Enum
class Flavour(Enum):
CHOCOLAT = 1
VANILLA = 2
PEAR = 3
# 这里 vanilla 相当于 Flavour 的一个实例
isinstance(vanilla, Flavour) # True
vanilla = Flavour.VANILLA
print(vanulla) # Flavour.VANILLA
vanilla.name # 'VANILLA'
vanilla.value # 2
-
IntEnum
会把所有的值转为 int 类型。
from enum import IntEnum
class HttpError(IntEnum):
OK = 200.9
REDIRECT = 301
REDIRECT_TMP = 302
NOT_FOUND = 404
INTERNAL_ERROR = 500
# avec un IntEnum on peut faire des comparaisons
def is_redirect(self):
return 300 <= self.value <= 399
code = HttpError.OK
code.value # 200
Enum
和 IntEnum
的一个特性是,类型本身(而不是实例)是可迭代的。其中每个元素,都是该类型的一个实例,因此都绑定了该类型中定义的方法:
class Couleur(IntEnum):
TREFLE = 0
CARREAU = 1
COEUR = 2
PIQUE = 3
def glyph(self):
glyphs = {
Couleur.TREFLE: '\u2663',
Couleur.CARREAU: '\x1b[31;1m\u2666\x1b[39;0m',
Couleur.COEUR: '\x1b[31;1m\u2665\x1b[39;0m',
Couleur.PIQUE: '\u2660',
}
return glyphs[self]
for couleur in Couleur: # 遍历时,couleur 已经成为实例了
print(f"Couleur {couleur} -> {couleur.glyph()}")
-
generator, iterator 和 iterable 的区别。发现一张神图:
iterator 实现了 iter 和 next,iteratrable 只实现了 iter,iter 返回的是该 iterable 的 iterator。
from collections.abc import Iterator, Iterable
class Foo:
def __iter__(self):
return self
foo = Foo()
isinstance(foo, Iterator) # False
isinstance(foo, Iterable) # True
class Foo2:
def __iter__(self):
return self
def __next__(self):
return
foo2 = Foo2()
isinstance(foo2, Iterator) # True
isinstance(foo2, Iterable) # True
-
mro
方法,mro 为 "method resolution order" 即方法解析顺序。它可以展示类的继承关系,用类名调用,而非类的对象调用:
class A:
pass
a = A()
a.mro()# AttributeError
A.mro() # [__main__.A, object] 表示先解析 A 类的方法,再解析其父类,object 类的方法。
python 多重继承解析顺序可以这样分析:1. 画好继承关系图,从上往下为父类到子类,2. 子类沿图回溯,将遇到的父类逐个添加到列表中,如果遇到分叉,按照先左后右的顺序,3. 列表中观察是否有重复的类,如果遇到,则删掉排在前面的那个。例如:
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
object
|
A
/ \
B C
\ /
D
回溯:[D, B, A, object, C, A, object]
删掉重复:[D, B, C, A, object]
D.mro() # [D, B, C, A, object]
- 所有 python 类都隐式继承了
object
类,因此没有必要把它写出来,下面的写法是过时的:
class A(object): pass
- 用 iterator+递归 实现序列全排列。使用 iterator 的一个好处,是不必一次性算出所有全排列的可能,防止占用过大内存,而是每计算一种就输出一次。思路是:
Permutation(n)
可以看成Permutation(n-1)
加上最后一个元素,插入Permutation(n-1)
返回的序列的 n 个缝隙中(包括开头和末尾)。
import itertools
class Permutation:
def __init__(self, n):
self.n = n
if n>=2:
self.sub_iterator = Permutation(n-1)
# 设置序列迭代器的终止flag
self.done = False
# 设置元素下标
self.cycle = itertools.cycle(list(range(n))[::-1])
def __iter__(self):
return self
def __next__(self):
if self.n ==1:
if not self.done:
self.done = True
return [0]
else:
raise StopIteration
cutter = next(self.cycle)
if cutter == (self.n-1):
self.subsequence = next(self.sub_iterator)
return self.subsequence[0:cutter] + [self.n-1] \
+ self.subsequence[cutter:self.n-1]
- 自定义 Exception。方法是继承
Exception
类。
class Phrase:
def __init__(self, phrase):
self.phrase = phrase
if not phrase:
raise PhraseVideError() # 如果不传参数可以省略括号
class PhraseVideError(Exception):
pass
p = Phrase('')
PhraseVideError Traceback (most recent call last)
in
----> 1 p = Phrase('')
in __init__(self, phrase)
3 self.phrase = phrase
4 if not phrase:
----> 5 raise PhraseVideError()
6
7 class PhraseVideError(Exception):
PhraseVideError:
# 也可以在 raise 时传入参数:
class Phrase:
def __init__(self, phrase):
self.phrase = phrase
if not phrase:
raise PhraseVideError('phrase vide', 18)
try:
p = Phrase('')
except PhraseVideError as e:
print(e.args)
# ('phrase vide', 18)
-
try
,except
,finally
,else
。之前只对 try... except 熟悉,后两者的作用如下:
def foo(x):
try:
x+=1
return x
except TypeError:
print('Input must be number')
return 'TypeError'
finally:
print('This is a demo of try...except...finally')
b = foo('1')
# Input must be number
# This is a demo of try...except...finally
# 即,不论前面是否有return,都会执行 finally 语句块的内容。
def foo(x):
try:
x+=1
except TypeError:
print('Input must be number')
else:
return x**2
b = foo(3)
print(b) # 16
b = foo('3')
print(b)
# Input must be number
# None
# 如果没有出现 exception,try 执行完后会执行 else(前提是 try 没有 return
# else 中的语句如果出现 exception,仍然会导致终止执行
# 有效防止了 try 的作用太大,掩盖了没有事先料到的 exception。
- 上下文管理器,context manager。它包含两个特殊方法
__enter__
和__exit__
。前者返回的是上下文管理器本身,即self
,后者返回布尔值,决定了是否需要将 exception 隐藏。
class Timer:
def __enter__(self):
self.start = time.time()
return self
def __exit__(self, *args):
duree = time.time()-self.start
print(f'{duree}s')
return False # True 将隐藏exception,这里不返回任何值,None 也会被认为是 False
def __str__(self):
duree = time.time()-self.start
return f'intermediaire: {duree}s'
with Timer() as t:
sum(x for x in range(10_000_000))
print(t)
# 1/0 # 解除注释会制造一个 exception,
# 如果 __exit__ 返回的是 True,则该 exception 不会终止程序,
# 反之,如果返回 False,该 exception 会导致程序终止,因此通常返回 False。
sum(x**2 for x in range(10_000_000))
-
__exit__
的参数:exc_type, exc_value, traceback
,三者收集了在 with 语句块内出现的 exception,用户可以根据上下文管理器的使用场景,定义哪些 exception 是可以被忽略的,哪些是需要返回的。
class Timer2:
def __enter__(self):
print("Entering Timer1")
self.start = time.time()
# rappel : le retour de __enter__ est ce qui est passé
# à la clause `as` du `with`
return self
def __exit__(self, exc_type, exc_value, traceback):
if exc_type is None:
# pas d'exception levée dans le corps du 'with'
print(f"Total duration {time.time()-self.start:2f}")
# dans ce cas la valeur de retour n'est pas utilisée
else:
# il y a eu une exception de type 'exc_type'
if exc_type in (ZeroDivisionError,) :
print("on étouffe")
# on peut l'étouffer en retournant True
return True
else:
print(f"OOPS : on propage l'exception "
f"{exc_type} - {exc_value}")
# et pour ça il suffit... de ne rien faire du tout
# ce qui renverra None
-
contextlib.contextmanager
可以将生成器函数转化为上下文管理器。
from contextlib import contextmanager
@contextmanager
def compact_timer(message):
start = time.time()
yield # yield 之前相当于 __enter__,yield 之后相当于 __exit__
print(f'{message}: duration={time.time()-start}')
with compact_timer('squares sum'):
print(sum(x**2 for x in range(10**5)))