迫于博士學(xué)院的學(xué)分要求,選了門網(wǎng)課,是我用了好多年的python。本想能水則水,但上課的過(guò)程中發(fā)現(xiàn)了自己一些知識(shí)上的漏洞,以流水賬的形式記于本文。純粹為私人復(fù)習(xí),不建議閱讀。
- 多條件排序。例如,按照名字、姓氏、第二姓氏將花名冊(cè)排序,如果兩個(gè)人名字相同,則比較姓氏,如果兩人姓氏也相同,則檢查其中是否有人有第二姓氏,有第二姓氏的排在后面,如果兩人都有第二姓氏,則比較第二姓氏。
# 輸入的是一個(gè)字典列表,每個(gè)元素代表一個(gè)人,其中的鍵包含了:'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 的用法,如果沒(méi)有鍵,返回其第二各參數(shù),默認(rèn)值 'A',因?yàn)槲覀兿M褯](méi)有第二姓氏的人排在最前面
liste.sort(key=key)
return liste
inspect.getsource()可以用來(lái)查看函數(shù)原代碼,主要用于提交的內(nèi)容不通過(guò)時(shí),配合pdb.set_trace()查看錯(cuò)誤。合并 list of list 最簡(jiǎn)單的方法:
x = [[1,2,3], [4,5], ['a', 'c', True]]
sum(x, [])
- 生成器表達(dá)式,可以節(jié)約內(nèi)存,在程序遍歷時(shí)才會(huì)逐個(gè)元素計(jì)算:
square = (x**2 for x in range(1000000))
sym = [x for x in square if str(x)==str(x)[::-1]]
生成器函數(shù),用 yield 取代 return,調(diào)用時(shí)返回的是一個(gè)迭代器,對(duì)迭代器施加 next 函數(shù)來(lái)逐個(gè)獲取函數(shù)的返回值。
調(diào)用另一個(gè)生成器函數(shù)的生成器函數(shù),需要用關(guān)鍵字
yield from
# 這是一個(gè)生成器函數(shù),它返回一個(gè)數(shù)除了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
# 我們想進(jìn)一步返回一個(gè)數(shù)因子的因子
def divdivs(n):
for i in divs(n): # 遍歷所有因子,沒(méi)問(wèn)題
# divs(i) # 不行,因?yàn)檫@樣編譯器不知道 divdivs 是生成器函數(shù),會(huì)認(rèn)為它返回值為 None
# yield divs(i) # 也不行,因?yàn)檫@樣返回的是求解各因子的因子的生成器,而非它們的因子
yield from divs(i) # 可以,
# yield from 關(guān)鍵字表明該函數(shù)是生成器函數(shù),
# from 表示從 divs(i) 中取出元素,取完后再進(jìn)
# 入下一個(gè)循環(huán),取出下一個(gè)因子的因子。
- Python 解釋器只會(huì)加載一次外部模塊,后續(xù)的 import 將不會(huì)重新加載。例如
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 不會(huì)打印上述文字
如果在開(kāi)發(fā)過(guò)程中,一定要重新加載模塊便于調(diào)試,可以使用:
from importlib import reload
reload(this)
-
sys.modules以字典形式存儲(chǔ)了當(dāng)前環(huán)境中加載的所有模塊,python 正是通過(guò)這個(gè)變量,來(lái)獲取模塊加載狀態(tài)的,如果加載的模塊已經(jīng)出現(xiàn)在字典中了,那么就不會(huì)重復(fù)加載。
import sys
import this
import numpy as np
sys.modules # 返回一個(gè)字典,其鍵包含 'this', 'numpy'(注意不是 'np') 等。
del sys.modules['this']
import this
# 將會(huì)重新打印那段 python 的歌謠
獲取當(dāng)前路徑,除了用
os.getcwd()還可以用pathlib.Path.cwd()。加載模塊時(shí)的搜索順序:首先搜索 built-in 中存不存在這個(gè)模塊;接下來(lái)在 sys.path 中搜索是否存在以該模塊命名的 .py 文件(或者文件夾(搭配了
__init__.py)。具體的搜索順序是這樣的:程序運(yùn)行入口所在的路徑,PYTHONPATH 的環(huán)境變量中存儲(chǔ)的路徑,編譯 python 時(shí)定義的一些路徑(?沒(méi)搞懂這句話什么意思)。可以模塊名字的字符串來(lái)加載模塊,需要用到
importlib.import_module。
from importlib import import_module
loaded = import_module('numpy') # 和下面的語(yǔ)句功能一樣,但這里加載的模塊名字就變成了 "loaded" 。
import numpy
numpy is loaded # True
import math
# 等價(jià)于
math = import_module('math')
# 用 import module 加載子模塊比較繁瑣,傳入的字符串中不能加句點(diǎn)
from pathlib import Path
# 需要轉(zhuǎn)換成:
tmp = import_module('pathlib')
Path = tmp.Path
del tmp
- 為類設(shè)置
property。property()可以對(duì)類的屬性進(jìn)行封裝,并為用戶提供設(shè)置屬性的接口,如下示例為一個(gè)熱力學(xué)溫度的類,眾所周知,熱力學(xué)溫度將絕對(duì)零度定義為0,所有的溫度都是正的,因此,在設(shè)置時(shí),需要考慮這一情況,把傳入的負(fù)值歸零。
class Temperature:
def __init__(self, kelvin):
self.kelvin = kelvin
# 注意,第一個(gè) 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
# 獲取的數(shù)據(jù)流,也由 self._get_kelvin() 的返回值提供。
kelvin = property(_get_kelvin, _set_kelvin)
def __repr__(self):
return f'{self._kelvin}K'
-
__hash__和__eq__是類的兩個(gè)特殊方法,__hash__可以自定義計(jì)算哈希值的方法,如果不實(shí)現(xiàn)它,則該類的哈希值是基于其內(nèi)存 id 計(jì)算的。__eq__提供了比較兩個(gè)該類實(shí)例是否相等的接口,如果不實(shí)現(xiàn),那么兩個(gè)實(shí)例永遠(yuǎn)不相等(兩個(gè)變量名表示的是同一個(gè)實(shí)例的情況除外)。
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}]'
這兩個(gè)特殊方法需要滿足下面的條件:如果兩個(gè)對(duì)象相等,那么這兩個(gè)對(duì)象的 hash 值一定相等(反之不成立);如果相等性的比較是基于實(shí)例屬性的,那么哈希值的計(jì)算至少也要基于屬性。通過(guò)實(shí)現(xiàn) __eq__,就可以直接用 == 比較兩個(gè)對(duì)象了。通過(guò)實(shí)現(xiàn) __hash__ 就可以將對(duì)象存入 set() 或者作為 dict 的鍵來(lái)使用了(要想讓 set() 發(fā)揮剔除重復(fù)元素的作用,還需要配合 __eq__)。需要注意的是:如果不實(shí)現(xiàn) __hash__ ,采用系統(tǒng)默認(rèn)的 __hash__ 也可以讓對(duì)象存入集合中,但是如果只實(shí)現(xiàn)了 __eq__ 沒(méi)有實(shí)現(xiàn) __hash__ 則不能讓對(duì)象存入集合,會(huì)報(bào)錯(cuò)說(shuō)該類型是 unhashable 的。
Python 文檔指出:
如果一個(gè)類定義了可變對(duì)象,并實(shí)現(xiàn)了
__eq__()方法,那么它不應(yīng)該實(shí)現(xiàn)__hash__()方法,因?yàn)榭晒5募闲枰WC鍵的哈希值是不可變的。
如果對(duì)可變對(duì)象實(shí)現(xiàn)了__hash__()方法,會(huì)引發(fā)潛在bug,例如根據(jù)上面定義的Point類:
t1, t2 = Point(10, 10), Point(10,10)
s = {t1, t2} # s 中只有一個(gè)元素 Point[10,10]
t1 in s, t2 in s
# (True, True)
# 現(xiàn)在改變 t1 的屬性:
t1.x = 100
# 此時(shí)輸出 s 發(fā)現(xiàn)其元素也變成了 Point[100,10]
# 再測(cè)試歸屬性:
t1 in s # False
t2 in s # False
出現(xiàn)上述問(wèn)題的原因是:s 由 t1 和 t2 初始化,由于 t1 先存入,因此計(jì)算 t1 的 hash value,并求出其在 hashtable 中的下表,接著存入 t2 時(shí),發(fā)現(xiàn) t2 的 hash value 和 t1 相等,同時(shí)根據(jù) __eq__(),t2 等于 t1,因此 s 便不再存入 t2 了。接下來(lái),改變 t1 的 attribute 后,在 s 中搜索現(xiàn)在的 t1,由于 t1 的attribute 被改變,其對(duì)應(yīng)的 hash value 和 由它計(jì)算出的 hash index 都不再是原來(lái)的那個(gè)了,當(dāng)然系統(tǒng)會(huì)認(rèn)為 t1 不在 s 中,同時(shí),t2 計(jì)算的 hash index 還是原來(lái)的那個(gè),但是據(jù)此取出該位置存儲(chǔ)的值,發(fā)現(xiàn)和 t2 并不相等,因此也會(huì)認(rèn)為 t2 不在 s 中。
-
__getattr__()和getattr()。__getattr__()是一個(gè)特殊方法。當(dāng)獲取實(shí)例的某個(gè)方法、屬性失敗時(shí),該方法被調(diào)用。getattr()是內(nèi)置函數(shù),它可以設(shè)置三個(gè)參數(shù):實(shí)例名、屬性名(字符串形式)、默認(rèn)值(可缺省),用于獲取實(shí)例的某個(gè)屬性或方法,當(dāng)獲取某個(gè)屬性/方法失敗時(shí),會(huì)返回默認(rèn)值。
首先看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')() # 也可以獲取方法,并調(diào)用,打?。篒n class A
接下來(lái)是 __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"
# 注意下面的調(diào)用,設(shè)置了 __getattr__() 之后,
# getattr() 的 default 參數(shù)將不再起作用
getattr(a, 'b', 100) # "You asked for b, but I'm giving you default"
-
namedtuple是一個(gè)函數(shù),返回一個(gè)類,類名就是它的第一個(gè)參數(shù),類的屬性由第二個(gè)參數(shù)決定,第二個(gè)參數(shù)是一個(gè)可迭代對(duì)象(除了 list tuple set 之類,甚至可以是 iterator),對(duì)象中的元素為字符串類型,對(duì)應(yīng)了需要?jiǎng)?chuàng)建的屬性。
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 是第一個(gè) 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 創(chuàng)建的實(shí)例本質(zhì)是tuple類型,故是不可變數(shù)據(jù)類型??梢酝ㄟ^(guò)創(chuàng)建 namedtuple 返回值的子類,創(chuàng)建 attribute 不可變的類 ,這樣就可以同時(shí)定義 __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之外又一種定義數(shù)據(jù)存儲(chǔ)類型的途徑。它的目標(biāo)僅僅是為了存儲(chǔ)數(shù)據(jù)。
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)
# 通過(guò)設(shè)置 dataclass 裝飾器的參數(shù),可以讓其裝飾的對(duì)象變成不可變類
@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 相當(dāng)于 Flavour 的一個(gè)實(shí)例
isinstance(vanilla, Flavour) # True
vanilla = Flavour.VANILLA
print(vanulla) # Flavour.VANILLA
vanilla.name # 'VANILLA'
vanilla.value # 2
-
IntEnum會(huì)把所有的值轉(zhuǎn)為 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 的一個(gè)特性是,類型本身(而不是實(shí)例)是可迭代的。其中每個(gè)元素,都是該類型的一個(gè)實(shí)例,因此都綁定了該類型中定義的方法:
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: # 遍歷時(shí),couleur 已經(jīng)成為實(shí)例了
print(f"Couleur {couleur} -> {couleur.glyph()}")
-
generator, iterator 和 iterable 的區(qū)別。發(fā)現(xiàn)一張神圖:
image.png
iterator 實(shí)現(xiàn)了 iter 和 next,iteratrable 只實(shí)現(xiàn)了 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" 即方法解析順序。它可以展示類的繼承關(guān)系,用類名調(diào)用,而非類的對(duì)象調(diào)用:
class A:
pass
a = A()
a.mro()# AttributeError
A.mro() # [__main__.A, object] 表示先解析 A 類的方法,再解析其父類,object 類的方法。
python 多重繼承解析順序可以這樣分析:1. 畫好繼承關(guān)系圖,從上往下為父類到子類,2. 子類沿圖回溯,將遇到的父類逐個(gè)添加到列表中,如果遇到分叉,按照先左后右的順序,3. 列表中觀察是否有重復(fù)的類,如果遇到,則刪掉排在前面的那個(gè)。例如:
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]
刪掉重復(fù):[D, B, C, A, object]
D.mro() # [D, B, C, A, object]
- 所有 python 類都隱式繼承了
object類,因此沒(méi)有必要把它寫出來(lái),下面的寫法是過(guò)時(shí)的:
class A(object): pass
- 用 iterator+遞歸 實(shí)現(xiàn)序列全排列。使用 iterator 的一個(gè)好處,是不必一次性算出所有全排列的可能,防止占用過(guò)大內(nèi)存,而是每計(jì)算一種就輸出一次。思路是:
Permutation(n)可以看成Permutation(n-1)加上最后一個(gè)元素,插入Permutation(n-1)返回的序列的 n 個(gè)縫隙中(包括開(kāi)頭和末尾)。
import itertools
class Permutation:
def __init__(self, n):
self.n = n
if n>=2:
self.sub_iterator = Permutation(n-1)
# 設(shè)置序列迭代器的終止flag
self.done = False
# 設(shè)置元素下標(biāo)
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() # 如果不傳參數(shù)可以省略括號(hào)
class PhraseVideError(Exception):
pass
p = Phrase('')
PhraseVideError Traceback (most recent call last)
<ipython-input-21-d50a533b0c1f> in <module>
----> 1 p = Phrase('')
<ipython-input-20-eb9093890fb9> in __init__(self, phrase)
3 self.phrase = phrase
4 if not phrase:
----> 5 raise PhraseVideError()
6
7 class PhraseVideError(Exception):
PhraseVideError:
# 也可以在 raise 時(shí)傳入?yún)?shù):
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。之前只對(duì) 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,都會(huì)執(zhí)行 finally 語(yǔ)句塊的內(nèi)容。
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
# 如果沒(méi)有出現(xiàn) exception,try 執(zhí)行完后會(huì)執(zhí)行 else(前提是 try 沒(méi)有 return
# else 中的語(yǔ)句如果出現(xiàn) exception,仍然會(huì)導(dǎo)致終止執(zhí)行
# 有效防止了 try 的作用太大,掩蓋了沒(méi)有事先料到的 exception。
- 上下文管理器,context manager。它包含兩個(gè)特殊方法
__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 也會(huì)被認(rèn)為是 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 # 解除注釋會(huì)制造一個(gè) exception,
# 如果 __exit__ 返回的是 True,則該 exception 不會(huì)終止程序,
# 反之,如果返回 False,該 exception 會(huì)導(dǎo)致程序終止,因此通常返回 False。
sum(x**2 for x in range(10_000_000))
-
__exit__的參數(shù):exc_type, exc_value, traceback,三者收集了在 with 語(yǔ)句塊內(nèi)出現(xiàn)的 exception,用戶可以根據(jù)上下文管理器的使用場(chǎng)景,定義哪些 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可以將生成器函數(shù)轉(zhuǎn)化為上下文管理器。
from contextlib import contextmanager
@contextmanager
def compact_timer(message):
start = time.time()
yield # yield 之前相當(dāng)于 __enter__,yield 之后相當(dāng)于 __exit__
print(f'{message}: duration={time.time()-start}')
with compact_timer('squares sum'):
print(sum(x**2 for x in range(10**5)))
