Python 進階(一些進階技巧)

我的筆記,基本都摘抄自 Python3 官方文檔html

一. 上下文管理

1. 傳統的類方式

Java 使用 try 來自動管理資源,只要實現了 AutoCloseable 接口,就能夠部分擺脫手動 colse 的地獄了。
而 Python,則是定義了兩個 Protocol:__enter____exit__. 下面是一個 open 的模擬實現:python

class OpenContext(object):

    def __init__(self, filename, mode):  # 調用 open(filename, mode) 返回一個實例
        self.fp = open(filename, mode)

    def __enter__(self):  # 用 with 管理 __init__ 返回的實例時,with 會自動調用這個方法
        return self.fp

    # 退出 with 代碼塊時,會自動調用這個方法。
    def __exit__(self, exc_type, exc_value, traceback):
        self.fp.close()

# 這裏先構造了 OpenContext 實例,而後用 with 管理該實例
with OpenContext('/tmp/a', 'a') as f:
    f.write('hello world')

這裏惟一有點複雜的,就是 __exit__ 方法。和 Java 同樣,__exit__ 至關於 try - catch - finallyfinally 代碼塊,在發生異常時,它也會被調用。正則表達式

當沒有異常發生時,__exit__ 的三個參數 exc_type, exc_value, traceback 都爲 None,而當發生異常時,它們就對應異常的詳細信息。
發生異常時,** __exit__ 的返回值將被用於決定是否向外層拋出該異常**,返回 True 則拋出,返回 False 則抑制(swallow it)。redis

Note 1:Python 3.6 提供了 async with 異步上下文管理器,它的 Protocol 和同步的 with 徹底相似,是 __aenter____aexit__ 兩個方法。
Note 2:與 Java 相同,with 支持同時管理多個資源,所以能夠直接寫 with open(x) as a, open(y) as b: 這樣的形式。shell

2. contextlib

2.1 @contextlib.contextmanager

對於簡單的 with 資源管理,編寫一個類可能會顯得比較繁瑣,爲此 contextlib 提供了一個方便的裝飾器 @contextlib.contextmanager 用來簡化代碼。編程

使用它,上面的 OpenContext 能夠改寫成這樣:緩存

from contextlib import contextmanager
@contextmanager
def make_open_context(filename, mode):
    fp = open(filename, mode)
    try:
        yield fp  # 沒錯,這是一個生成器函數
    finally:
        fp.close()


with make_open_context('/tmp/a', 'a') as f:
    f.write('hello world')

使用 contextmanager 裝飾一個生成器函數,yield 以前的代碼對應 __enter__,finally 代碼塊就對應 __exit__.安全

Note:一樣,也有異步版本的裝飾器 @contextlib.asynccontextmanager數據結構

2.2 contextlib.closing(thing)

用於將本來不支持 with 管理的資源,包裝成一個 Context 對象。app

from contextlib import closing
from urllib.request import urlopen

with closing(urlopen('http://www.python.org')) as page:
    for line in page:
        print(line)

# closing 等同於
from contextlib import contextmanager

@contextmanager
def closing(thing):
    try:
        yield thing
    finally:
        thing.close()  # 就是添加了一個自動 close 的功能

2.3 contextlib.suppress(*exceptions)

使 with 管理器抑制代碼塊內任何被指定的異常:

from contextlib import suppress

with suppress(FileNotFoundError):
    os.remove('somefile.tmp')

# 等同於
try:
    os.remove('somefile.tmp')
except FileNotFoundError:
    pass

2.4 contextlib.redirect_stdout(new_target)

將 with 代碼塊內的 stdout 重定向到指定的 target(可用於收集 stdout 的輸出)

f = io.StringIO()
with redirect_stdout(f):  # 將輸出直接寫入到 StringIO
    help(pow)
s = f.getvalue()

# 或者直接寫入到文件
with open('help.txt', 'w') as f:
    with redirect_stdout(f):
        help(pow)

redirect_stdout 函數返回的 Context 是可重入的( reentrant),能夠重複使用。

2、pathlib

提供了 OS 無關的文件路徑抽象,能夠徹底替代 os.pathglob.

基本上,pathlib.Path 就是你須要瞭解的全部內容。

1. 路徑解析與拼接

from pathlib import Path

data_folder = Path("./source_data/text_files/")
data_file = data_folder / "raw_data.txt"  # Path 重載了 / 操做符,路徑拼接超級方便

# 路徑的解析
data_file.parent  # 獲取父路徑,這裏的結果就是 data_folder
data_foler.parent # 會返回 Path("source_data")
data_file.parents[1] # 即獲取到 data_file 的上上層目錄,結果和上面同樣是 Path("source_data")
data_file.parents[2] # 上上上層目錄,Path(".")

dara_file.name # 文件名 "raw_data.txt"
dara_file.suffix  # 文件的後綴(最末尾的)".txt",還可用 suffixes 獲取全部後綴

data_file.stem  # 去除掉最末尾的後綴後(只去除一個),剩下的文件名:raw_data

# 替換文件名或者文件後綴
data_file.with_name("test.txt")  # 變成 .../test.txt
data_file.with_suffix(".pdf")  # 變成 .../raw_data.pdf

# 當前路徑與另外一路徑 的相對路徑
data_file.relative_to(data_folder)  # PosixPath('raw_data.txt')

2. 經常使用的路徑操做函數

if not data_folder.exist():
    data_folder.mkdir(parents=True)  # 直接建立文件夾,若是父文件夾不存在,也自動建立

if not filename.exists():  # 文件是否存在
    filename.touch()  # 直接建立空文件,或者用 filename.open() 直接獲取文件句柄

# 路徑類型判斷
if data_file.is_file():  # 是文件
    print(data_file, "is a file")
elif data_file.is_dir():  # 是文件夾
    for child in p.iterdir():  # 經過 Path.iterdir() 迭代文件夾中的內容
        print(child)

# 路徑解析
filename.resolve()  # 獲取文件的絕對路徑(符號連接也會被解析到真正的文件)

# 能夠直接獲取 Home 路徑或者當前路徑
Path.home() / "file.txt" # 有時須要以 home 爲 base path 來構建文件路徑
Path.cwd()  / "file.txt" # 或者基於當前路徑構建

還有不少其它的實用函數,可在使用中慢慢探索。

3. glob

pathlib 也提供了 glob 支持,也就是普遍用在路徑匹配上的一種簡化正則表達式。

data_file.match(glob_pattern)  # 返回 True 或 False,表示文件路徑與給出的 glob pattern 是否匹配

for py_file in data_folder.glob("*/*.py"):  # 匹配當前路徑下的子文件夾中的 py 文件,會返回一個可迭代對象
    print(py_file)

# 反向匹配,至關於 glob 模式開頭添加 "**/"
for py_file in data_folder.glob("*/*.py"):  # 匹配當前路徑下的全部 py 文件(全部子文件夾也會被搜索),返回一個可迭代對象
    print(py_file)

glob 中的 * 表示任意字符,而 ** 則表示任意層目錄。(在大型文件樹上使用 ** 速度會很慢!)

3、functools

functools 提供了幾個有時頗有用的函數和裝飾器

1. @functools.wraps

這個裝飾器用於使裝飾器 copy 被裝飾的對象的 __module__, __name__, __qualname__, __annotations__ and __doc__ 屬性,這樣裝飾器就顯得更加透明。

from functools import wraps
def my_decorator(f):
     @wraps(f)
     def wrapper(*args, **kwds):
         print('Calling decorated function')
         return f(*args, **kwds)
     return wrapper  # 用了 wraps,wrapper 會複製 f 的各類文檔屬性

@my_decorator
def func(xx):
    """ this is func's docstring"""
    print("this is func~")

若是不用 wraps 的話,由於實際上返回的是 wrapper,被裝飾對象的這些文檔屬性都會丟失。(好比 docstring)
所以在使用 wrapper 裝飾器時,添加 @wraps() 裝飾器是個好習慣。

2. functools.partial

這個感受和高等數學的偏函數很像:好比函數 z = f(x, y) 有 x 和 y 兩個變量,如今把 x 看做常數,就能夠對 y 進行求導運算。
而 python 的 partial 也差很少,不過它不是把 x 看做常數,而是先給定 x 的值。用法以下:

from functools import partial
basetwo = partial(int, base=2)  # 先給定 int 函數的 base 參數爲 2
basetwo.__doc__ = 'Convert base 2 string to an int.'  # 若是須要文檔,能夠添加 __doc__ 屬性
basetwo('10010')  # return 18

此外,還有個 partialmethod 函數,待了解

3. @functools.lru_cache(maxsize=128, typed=False)

若是某方法可能被頻繁調用(使用相同的參數),並且它的結果在必定時間內不會改變。能夠用 lru_cache 裝飾它,減小運算量或 IO 操做。

from functools import lru_cache

# 緩存最近的(least recently used,lru) 64 次參數不一樣的調用結果。
@lru_cache(maxsize=64)
def my_sum(x):  # 後續的調用中,若是參數能匹配到緩存,就直接返回緩存結果
    return sum(x)

好比用遞歸計算斐波那契數列,數值較低的參數會被頻繁使用,因而能夠用 lru_cache 來緩存它們。
或者爬取網頁,可能會須要頻繁爬取一個變化不快的網頁,這時徹底能夠用 cache 緩存。

可是它不能控制緩存失效時間,所以不能用於 Web 系統的緩存。仍是得本身寫個簡單的裝飾器,把緩存存到 redis 裏並設置 expires。或者直接用 Flask 或 Django 的 caching 插件。

4. @functools.singledispatch

單重派發,即根據函數的第一個參數的類型,來決定調用哪個同名函數。

@singledispatch
def parse(arg):  # 首先定義一個默認函數
    print('沒有合適的類型被調用')  # 若是參數類型沒有匹配上,就調用這個默認函數

@parse.register(type(None))  # 第一個參數爲 None
def _(arg):
    print('出現 None 了')

@parse.register(int)  # 第一個參數爲整數
def _(arg):
    print('此次輸入的是整數')

@parse.register
def _(arg: list):  # python3.7 開始,能夠直接用類型註解來標註第一個參數的類型
    print('此次輸入的是列表')

畫外:有單重派發,天然就有多重派發,Julia 語言就支持多重派發,即根據函數全部參數的類型,來決定調用哪個同名函數。
Julia 語言根本沒有類這個定義,類型的全部方法都是經過多重派發來定義的。

其餘

  1. @functools.total_ordering:用於自動生成比較函數。
  2. functools.cmp_to_key(func):用於將老式的比較函數,轉換成新式的 key 函數。

4、operator

operator 模塊包含四種類型的方法:

1. operator.itemgetter

常常被用於 sorted/max/mix/itertools.groupby 等

使用方法:

# itemgetter
f = itemgetter(2)
f(r)  # return r[2]

# 還能一次獲取多個值,像 numpy 那樣索引
f2 = itemgetter(2,4,5)
f2(r)  # return (r[2], r[4], r[5])

# 或者使用 slice 切片
s = itemgetter(slice(2, None))
s[r]  # return r[2:]

# dict 索引也能用
d = itemgetter('rank', 'name')
d[r]  # return d['rank'], d['name']

用途:

# 用於指定用於比較大小的屬性
key = itemgetter(1)
sorted(iterable, key=key)  # 使用 iterable[1] 對 iterable 進行排序
max(iterable, key=key)  # 找出最大的元素,使用 iterable[1] 作比較

# 用於高級切片(好比像 numpy 那樣的,指定只獲取某幾列)
s = itemgetter(1,3,4)
matrix = [[0,1,2,3,4], [1,2,3,4,5]]
map(s, matrix)  # list 後獲得 [(1, 3, 4), (2,4,5)]

2. operator.attrgetter

可用於動態獲取對象的屬性,與直接用 getattr() 不一樣的是,它能夠嵌套訪問屬性。

# 嵌套訪問屬性
att = attrgetter("a.b.c")
att(obj)  # return obj.a.b.c

# 和 itemgetter 同樣,也能夠一次獲取多個屬性
att = attrgetter("a.b.c", "x.y")
att(obj)  # return (obj.a.b.c, obj.x.y)

# 不嵌套的話,用 getattr 就行
getattr(obj, "a")  # return obj.a

這裏能夠回顧一下類的兩個魔法函數:

  1. __getattr__: 當被訪問的屬性不存在時,這個方法會被調用,它的返回值會成爲對象的該屬性。
    • 用於動態生成實例的屬性/函數
  2. __getattribute__: 與 __getattr__ 惟一的差異在於,訪問對象的任何屬性,都會直接調用這個方法,無論屬性存不存在

3. operator.methodcaller

可用於調用函數,它和 attrgetter 很像,差異在於 attrgetter 只是返回指定的屬性,而 methodcaller 會直接把指定的屬性當成函數調用,而後返回結果。

舉例

f = methodcaller('name', 'foo', bar=1)
f(b)  # returns b.name('foo', bar=1)

4. 各類操做符對應的函數

operator.add、operator.sub、operator.mul、operator.div 等等,函數式編程有時須要用到。

5、itertools

itertools 提供了許多針對可迭代對象的實用函數

方法不少,基本不可能一次全記住。仍是要用到時多查吧。大體記住有提供哪些功能,須要用到時能想起能夠查這個模塊就行。

1. 無限迭代器

  1. count(start=0, step=1): 從 start 開始,每次迭代時,返回值都加一個 step
    • 默認返回序列爲 0 1 2 3...
  2. cycle(iterable): 不斷循環迭代 iterable
  3. repeat(element, times=None): 默認永遠返回 element。(若是 times 不爲 None,就迭代 times 後結束)

2. 排列組合迭代器

  1. product(p1, p2, ..., repeat=1):p1, p2... 的元素的笛卡爾積,至關於多層 for 循環
    • repeat 指參數重複次數,好比
    >>> from itertools import product
    >>> r = product([1, 2], [3, 4], [5, 6])  # 重複一次,也就是 (p1, p2, p3) 的笛卡爾積
    >>> pprint(list(r))       
    [(1, 3, 5),
     (1, 3, 6),
     (1, 4, 5),
     (1, 4, 6),
     (2, 3, 5),
     (2, 3, 6),
     (2, 4, 5),
     (2, 4, 6)]
    >>> r2 = product([1, 2], [3, 4], [5, 6], repeat=2)  # 重複兩次,即 (p1, p2, p3, p1, p2, p3) 的笛卡爾積
    >>> pprint(list(r2))
    [(1, 3, 5, 1, 3, 5),
     (1, 3, 5, 1, 3, 6),
     (1, 3, 5, 1, 4, 5),
     (1, 3, 5, 1, 4, 6),
     (1, 3, 5, 2, 3, 5),
    ...
  2. permutations(p[, r]):p 中元素,長度爲 r 的全部可能的排列。至關於 product 去重後的結果。
  3. combinations(p, r):既然有排列,固然就有組合了。

3. 其餘

  1. zip_longest(*iterables, fillvalue=None):和 zip 的差異在於,缺失的元素它會用 fillvalue 補全,而不是直接結束。
  2. takewhile()
  3. dropwhile()
  4. groupby()

等等等,用獲得的時候再查了。。。

6、collections

提供了一些實用的高級數據結構(容器)

  1. defaultdict:這個感受是最經常使用的,能夠給定 key 的默認值
  2. Counter:方便、快速的計數器。經常使用於分類統計
  3. deque:一個線程安全的雙端隊列
  4. OrderedDict:有時候會須要有序字典
  5. namedtuple:命名元組,有時用於參數傳遞。與 tuple 的差異是它提供了關鍵字參數和經過名字訪問屬性的功能
  6. ChainMap:將多個 map 鏈接(chain)在一塊兒,提供一個統一的視圖。由於是視圖,因此原來的 map 不會被影響。
相關文章
相關標籤/搜索