Daydreaming in Brookline, MA

Python Cookbookレシピリスト

1 はじめに

Python Cookbook 3rd edition by David Beazley & Brian K. Jonesを読んだのですが、読んだことはほとんど忘れてしまっていて、レシピ集として活用できるレベルになっていません。やはり、掲載されているコードサンプルは全て自分で入力して動きを確認していかないと駄目そうです。

そこで、復習の意味でサンプルコードの実行と、自分用にレシピリストを作ることにしました。少しずつアップデートするので、完成までに時間がかかりそうです。今のペースだと5ヶ月くらい。。。

2 レシピ一覧

2.1 Chapter 1: データ構造とアルゴリズム

1.1 シーケンスやiterableは = で複数変数にアンパックできる
1.2 * (star unpacking)を使って可変数変数をリストに受ける
  リストのレコードにタグを付けて異なる処理をするレシピ
1.3 限定数のヒストリーを持つのにcollections.dequeが使える deque(maxlen=3)
  ファイル内でアイテムを探す、yieldを使うレシピ
1.4 最大/最小からN個のアイテムを見つけるのにheapqが使える heapq.nlargest(3, somelist)
  大きなリストを扱うにはheapify()しておく
1.5 プライオリティキューを作るのにheapqが使える
1.6 複数値を持つdictionaryの作り方
  初期値の自動作成にdefaultdictが使えるが、やや不自然
1.7 OrderedDictの使い方。後でJSON化したい時に有用
1.8 zip()を使ってdictionaryを(value, key)のシーケンスに並び替えてソート等する
1.9 dictionaries間の共通等を操作するのに &, - 等のset操作を使う
1.10 シーケンスの順序を維持したままdedupeする
  重複をsetで管理し、重複していない場合に値をyieldするdedupe関数のレシピ
1.11 スライスに名前を付けてコードの可読性を上げる。ビルトインslice()関数の使い方
  IndexErrorが出ないように範囲にお編めるには sliceobj.indices(len(s)) を使う
1.12 Counterオブジェクトを使ってアイテムの登場回数を管理する
  most_common(n)メソッド。 +, - での複数Counter操作
1.13 ディクショナリのリストをソートするのに、ソートキーkey=itemgetter(キー)を使う
  lambda関数も使える。ソートだけでなくmin(), max()でも指定可能
1.14 比較をサポートしないオブジェクトをソートする。キーにattrgetter()を指定する
1.15 ディクショナリのリストをフィールド値でグループ分けしたい
  フィールドでソート(itemgetter())し、groupby()でグループ分けする
1.16 シーケンスをフィルタするにはlistcomp/genexpを使う
  filter()やcompress()の使い方。compressはbooleanのシーケンスを受ける
1.17 ディクショナリをフィルタしてサブセットのディクショナリを取り出すにはdictcompを使う
1.18 namedtupleの使い方。dict → namedtuple変換レシピ
  デフォルト値からなるnamedtupleを作り、_replace(**dic)でアップデートする
1.19 変換/フィルタしてからsum(),min(),max()等するのにgenexpを使う(値のみ返す)
  genexpの代わりに、min(), max()等にkey引数を渡すレシピ(全体を返す)
1.20 collections.ChainMapは複数のマッピングを仮想的に一つにまとめる
  ChainMapへのアップデートは、最初のマップにのみ反映される

2.1.1 defaultdictとsetdefault(1.6)

defaultdict は以下のように使う

from collections import defaultdict
d = defaultdict(list)  # list, set等を指定
d['a'].append(1)  # setの場合は.add(1)

defaultdictは、間違えてアクセス(値の取得でも)をした場合にも空きリスト/セットが設定されてしまう。

setdefault は通常のディクショナリに対して使える

d = {}  # 普通のディクショナリ
d.setdeault('a', []).append(1)

2.1.2 itemgetter()関数(1.13)

operator.itemgetter(item), itemgetter(*items) は引数に __getitem__() に渡せるインデックスを取り、callableを返す。 f = itemgetter(2) の後で f(r) をコールすると r[2] を返す。同様に、 g = itemgetter(2, 5, 3) の後で g(r) をコールすると (r[2], r[5], r[3]) のタプルを返す。

sort等の関数に key=itemgetter('some_key') のように指定する。レシピ1.14の attrgetter() も同様。

2.1.3 dictcompを使ってフィルタする(1.17)

以下のようなパターンで使う。

{ key:value for key,value in prices.items() if value > 200}
{ key:value for key,value in prices.items() if key in tech_names }

2.2 Chapter 2: 文字列とテキスト

2.1 混在するdelimitersに対処 re.split(r'[;,\s]\s*', line) のように使う
2.2 文字列の先頭または末尾を調べるのにstartswith(), endswith()を使う
  複数選択肢がある場合はタプルを渡す name.endswith(('.py', 'html'))
2.3 fnmatch(), fnmatchcase()の使い方 fnmatch('foo.txt', '*.txt')
  ワイルドカードを使いたいとき。正規表現未満。ファイル名以外でも
2.4 正規表現 re.compile(), match(), findall(), finditer()の使い方
2.5 str.replace('before', 'after') re.sub()/subn()使い方
  re.sub(r'(\d+)/(\d+)/(\d+)', r'\3-\1-\2', text) コールバック指定可
2.6 re.findall()等にflags=re.IGNORECASEを付ける
  置き換え単語のケースを維持する、closureのヘルプ関数を使ったレシピ
2.7 正規表現で最短マッチ re.compile(r'\"(.*?)\"') のように?を付ける
2.8 複数行にわたるマッチ re.compile(r'/\*((?:.¦\n)*?)\*/')
  re.DOTALLよりも自分でマッチパターンを書いた方が融通が利く
2.9 unicode textをnormalizeする unicodedata.normalize('NFC', text)
  combining文字を含むテキストをasciiに変換するレシピ。
2.10 uniocde文字を正規表現で扱う場合、case foldに注意
  本格的にやるなら3rd partyのregexライブラリ等を使う
2.11 strip(), lstrip(), rstrip() スペース以外の文字も指定可能 strip('-=')
  先頭、末尾以外もstripしたい場合 re.sub('\s+', ' ', text)
2.12 テキストをsanitize, clean upするのにtranslate()を使うレシピ
  NFDにnormalizeして b.encode('ascii', 'ignore').decode('ascii')
  速度ではstr.replace()メソッドが一番速い
2.13 ljust(), rjust(), center()を使ったallignment text.rjust(20, '=')
  format()も使える。ストリング以外でも format(x, '10.2f')
2.14 str.join()の使い方 + は文字列をコピーするので非効率(遅い)
  print文のセパレーター指定は使える print(a,b,c,sep=' ')
  多くの文字列小片をyieldするgenerator関数を使うレシピ
2.15 文字列に変数を入れ込む s.format_map(vars())
  dictを継承し、__missing__()を持つクラスを作って例外にならないように
  vars()の代わりにsys._getframe(1).f_localsを使うframe hackレシピ
2.16 1行の文字数を制限して表示するためにtextwrapモジュールを使う
  textwrap.fill(s, 40, initial_indent=' ')
2.17 HTML/XMLエンティティーの扱い html.escape(), html.unescape()
2.18 token化する (?P<grp_name>...) でグループ名を付け、scanner()を使う
2.19 (パーサーを書く。BNFとかEBNFとか)
2.20 byte stringsにテキスト操作が意外と使える。
  正規表現も使えるが、パターンをbyte string b'…'で指定する

2.2.1 noncaptureグループ(2.1)

>>> re.split(r'[;,\s]\s*', line)
['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo']
>>> re.split(r'(;|,|\s)\s*', line)  # captureグループ(結果のリストに出力される)
['asdf', ' ', 'fjdk', ';', 'afed', ',', 'fjek', ',', 'asdf', ',', 'foo']
>>> re.split(r'(?:;|,|\s)\s*', line)  # noncaptureグループ (?: ...)
['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo']

2.2.2 テキストファイル全体をstripする(2.11)

ファイル内の行を全てstripしたい場合、genexpで効率よく実現できる。

with open(filename) as f:
    lines = (line.strip() for line in f)  # iteratorを作るだけ
    for line in lines:
        ...

2.2.3 (2.14)

genexpを使ってリストを文字列に変換するレシピ。文字列化と連結を一括で実施する。

>>> data = ['ACME', 50, 91.1]
>>> ','.join(str(d) for d in data)
'ACME,50,91.1'

2.3 Chapter 3: 数、日にちと時間

3.1 数字を四捨五入するのにround()を使う。 round(-1.27, 1) >>> -1.3
  マイナス値も使える round(7734, -2) >>> 7700
3.2 浮動小数点の誤差が許せない場合は Decimal('4.2') を使う
  with localcontext() as ctx: のように精度等を制御できる
3.3 formatの使い方 format(x, '^10.1f'), format(x, '0.2e')
3.4 2進、8進、16進での表記 oct(x), format(x, 'o')
  10進に直すにはintを使う int('4d2', 16) 8進は0o755のように
3.5 バイト列から大きなintegerをパック、アンパックする
  int.from_bytes(data, 'little'), x.to_bytes(16, 'little')
  x.bit_length() でビット長取得
3.6 複素数を使う complex(2, 4), 3-5j, cmath.sqrt(-1)
3.7 無限とNaN(not a number) float('inf'), float('-inf'), float('nan')
3.8 分数 fractions.Fraction Fraction(5, 4)
3.9 numpyの基本的な使い方 カラム a[:,2], subregion a[1:3, 1:3] 選択
3.10 numpyでの線形代数計算(パス)
3.11 ランダム値を得る randint(0, 10)は0も10も含む。random()
3.12 時間の変換 timedelta(hours=4.5), datetime(2021, 1, 26)
  dateutil.relativedelta()
3.13 先週の火曜日は何日、のレシピ。 dateutil.relativedeltaで簡単に。
  relativedelta(weekday=FR), relativedelta(weekday=FR(-1) 先週
3.14 今月をリストするレシピ。ビルトインrange()の日にち版レシピ
  date.today().replace(day=1), calendar.monthrange(yr, mon)
3.15 文字列で表記された日にちをdatetimeに変換するレシピ
  datetime.strptime(text, '%Y-%m-%d') strftimeで逆変換
3.16 タイムゾーンの変換はpytzモジュールを使う
  ローカル時間を操作するレシピ。いったんUTCに変換する
   

2.3.1 ランダム値を得る(3.11)

>>> import random
>>> values = [1,2,3,4,5,6]
>>> random.choice(values)  # 一つ選ぶ
2
>>> random.sample(values, 3)  # 3つ選ぶ
[4, 3, 1]
>>> random.shuffle(values)  # ランダムに並び替える
>>> values
[5, 2, 3, 1, 4, 6]
>>> random.random()  # 0-1のfloat値を得る
0.14043684176026439
>>> random.seed()  # システム時間やos.random()ベースのシード
>>> random.seed(12345)

暗号に使う厳密なランダム値は ssl.RAND_byes() 等を使うこと。

2.3.2 タイムゾーンの扱い(3.16)

>>> from datetime import datetime
>>> import pytz
>>> d = datetime(2021, 1, 27, 21, 30,0)  # (1) datetimeで日時を表記
>>> print(d)
2021-01-27 21:30:00
>>> boston = timezone('America/New_York')
>>> bos_d = boston.localize(d)  # (2) 東海岸時間に指定
>>> print(bos_d)
2021-01-27 21:30:00-05:00
>>> utc_d = bos_d.astimezone(pytz.utc)  # (3) UTCに変換
>>> print(utc_d)
2021-01-28 02:30:00+00:00
>>> later_utc_d = utc_d + timedelta(minutes=30)  # (4) UTCで時間計算
>>> japan = timezone('Asia/Tokyo')
>>> print(later_utc_d.astimezone(japan))  # (5) JST時間で表示
2021-01-28 12:00:00+09:00

国のタイムゾーンを取得する方法。

>>> pytz.country_timezones['JP']
['Asia/Tokyo']
>>> pytz.country_timezones['US']
['America/New_York', 'America/Detroit', 'America/Kentucky/Louisville', 
'America/Kentucky/Monticello', 'America/Indiana/Indianapolis', 
'America/Indiana/Vincennes', 'America/Indiana/Winamac', 
'America/Indiana/Marengo', 'America/Indiana/Petersburg', 
'America/Indiana/Vevay', 'America/Chicago', 'America/Indiana/Tell_City', 
'America/Indiana/Knox', 'America/Menominee', 'America/North_Dakota/Center', 
'America/North_Dakota/New_Salem', 'America/North_Dakota/Beulah', 
'America/Denver', 'America/Boise', 'America/Phoenix', 
'America/Los_Angeles', 'America/Anchorage', 'America/Juneau', 
'America/Sitka', 'America/Metlakatla', 'America/Yakutat', 'America/Nome', 
'America/Adak', 'Pacific/Honolulu']

2.4 Chapter 4: アイテレーターとジェネレーター

4.1 for文を使わずにiterateする
4.2 カスタムコンテナオブジェクトにiteratorプロトコルを実装する
  __iter__()は__next__()を実装するiteratorオブジェクトを返す
  iter(s)はs.__iter__()と同じ
4.3 新たなiterationパターンはジェネレーター関数で実装する
4.4 オブジェクトに再帰呼び出しするジェネレーター関数を定義してiterateする
4.5 シーケンスを逆にiterateする。reverse()を使う。
  __reverse__()を実装する(効率よい)か、いったんリストにする
4.6 ジェネレーター関数にステートを持たせるには、クラスにすればよい
4.7 iteratorをスライス(eg, [1:5])するにはitertools.islice()を使う
4.8 iterableの最初の部分をスキップするにはitertools.dropwhile()を使う
4.9 itertools.permutations, combinations, combinations_with_replacement
4.10 シーケンスに行番号やインデックス番号を付けるにはenumerateを使う
4.11 複数のシーケンスを同時にたどるにはzip()を使う zip_longest()
4.12 複数のシーケンスを一つにつなげるのはchain()を使う
  シーケンスを+でつなげるのは、同じタイプでないといけない& inefficient
4.13 ジェネレーター関数をパイプラインでつなぐ
4.14 ネストされたシーケンスをflattenするのに、ジェネレーター関数を
  yield fromで再帰呼び出しするレシピ
4.15 ソートされた複数のリストをマージするのにheapq.mergeが使える
4.16 sentinelが出るまでストリームをiterateするiter(lambda: xx, sentinel)
  を使うレシピ

2.4.1 iterate可能なオブジェクト (4.4)

ルートノードから世代をiterateするオブジェクトの実装例。

class Node:
    def __init__(self, value)
        self._value = value
        self._children = []
<snip>
    def __iter__(self):
        return iter(self._children)
    def depth_first(self):  # ジェネレーター関数
        yield self
        for c in self:
            yield from c.depth_first()  # 再帰呼び出し

2.4.2 iter()の特別な用法(4.16)

iter()に引数を取らないcallableとsentinelを与えると、sentinelが出るまで指定callableを繰り返し呼ぶiteratorを返す。引数を取らないcallableを作るためにlambdaを使う。

def reader(s):
    for chunk in iter(lambda: s.recv(CHUNKSIZE), b''):
        process_data(chunk)

import sys
f = open('/etc/passwd')
for chunk in iter(lambda: f.read(10), ''):
    n = sys.stdout.write(chunk)

2.5 Chapter 5: ファイルとI/O

5.1 テキストファイルを読み書きするにはopenする。withを使うと便利。
  リターンコード、エンコードエラーの扱い
5.2 print('…', file=fd)を使ってファイルにリダイレクトする
5.3 print('…', sep=',', end='!!\n')
  str.join()と異なり、文字列以外にも使える
5.4 open('filename', 'rb') 'wb' でバイナリモードでオープン
  配列やC構造体はいったんバイナリに変換しなくてよい
  readinto()はメモリに直接読み可能
5.5 既存ファイルがあるとライトを失敗させるには'xb' 'xt'を指定する
  with open('file', 'xt') as f:
5.6 文字列やバイト列をファイルライクなオブジェクトとして扱うレシピ
  io.StringIO(), io.BytesIO()
5.7 gzip.open(), bz2.open()を使って圧縮ファイルを読み書きする
  正しいファイルモード(テキスト/バイナリ)を使う事が重要
5.8 固定サイズずつバイナリファイルをiterateするレシピ
  iter(partial(f.read, REC_SZ), b'')を使う b'': sentinel
5.9 mutableなバッファにバイナリデータを読む f.readinto(buf)を使う
  memoryviewを使っても。bytes(mv)で中身を見る
5.10 mmapを使うレシピ。コンテキストマネージャとしても使える
5.11 パス名の操作にはos.pathモジュールを使う
5.12 os.pathの関数 exists(), isfile(), islink(), realpath(), getsize(), etc.
5.13 ディレクトリのリスティング。os.listdir(), endswith(), glob, fnmatch等
  os.stat(file)でメタデータ一括取得
5.14 listdir(b'.'), open(b'..txt')でファイル名エンコーディングをバイパスする
5.15 デコードできないファイル名をどう扱うか
5.16 オープンしているファイルのエンコーディングを変えるレシピ
  f.detach()してTextIOWrapperレイヤーを別エンコードで付け直す
5.17 テキストファイルにバイトを書き込む。 f.buffer.write(b'…')
5.18  

2.5.1 バッファインタフェース(5.4)

配列(アレイ)等はバッファインタフェースを持ち、いったんバイトに変換せずにバイトとして読み書きできる。配列等にバイトを直接読み出すのにfd.readinto(array)が使える。

>>> import array
>>> nums = array.array('i', [1,2,3,4])
>>> with open('tt.bin', 'wb') as f:
...     f.write(nums)
>>> a = array.array('i',[0,0,0,0,0,0])
>>> with open('tt.bin', 'rb') as f:
...     f.readinto(a)
>>> a
array('i', [1, 2, 3, 4, 0, 0])

2.5.2 圧縮ファイルの読み書き(5.7)

import gzip
with gzip.open('file.gz', 'rt') as f:
    text = f.read()

import bz2
with bz2.open('file.bz2', 'wb') as f:
    f.write(bytes)

バイナリモードでオープンした既存ファイルに重ねることが可能。

f = open('file.gz', 'rb')
with gzip.open(f, 'rt') as g:
    text = g.read()

これは、ソケット、パイプ、インメモリファイル等のファイルライクなオブジェクトに対して使えるということ。

2.5.3 mmapを使う(5.10)

mmapを使うユーティリティ関数。

import os                                                                              
import mmap                                                                            
def memory_map(filename, access=mmap.ACCESS_WRITE):                                    
    size = os.path.getsize(filename)                                                   
    fd = os.open(filename, os.O_RDWR)                                                  
    return mmap.mmap(fd, size, access=access)

mmapするファイルを用意する。

size = 10000
with open('tt.bin', 'wb') as f:
    f.seek(size-1)
    f.write(b'\x00')

使い方。

>>> m = memory_map('tt.bin')
>>> len(m)
10000
>>> m[:10]
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
>>> m[0]
0
>>> m[:11] = b'Hello World'
>>> m
<mmap.mmap closed=False, access=ACCESS_WRITE, length=10000, pos=0, offset=0>
>>> m.close()  # 必須!
>>> m
<mmap.mmap closed=True>
>>> with open('tt.bin', 'rb') as f:
...     print(f.read(11))
... 
b'Hello World'

コンテキストマネージャとしても使える。close()不要。

with memory_map('tt.bin') as m:
    print(len(m))
    print(m[0:10])

2.5.4 テキスト操作の3つのレイヤー(5.16)

テキストファイルは3つのレイヤーでアクセスする。

>>> f = open('tt.txt', 'w')
>>> f  # テキストハンドリングレイヤー
<_io.TextIOWrapper name='tt.txt' mode='w' encoding='UTF-8'>
>>> f.buffer  # buffered I/Oレイヤー
<_io.BufferedWriter name='tt.txt'>
>>> f.buffer.raw  # OSの低レベルファイルデスクリプター
<_io.FileIO name='tt.txt' mode='wb' closefd=True>