効果的なPython その4

python

 6章 最終章の読解です。なかなか読むのに時間がかかった本書ですが、やっと最後までたどり着きました。

第6章 組み込みモジュール

 便利だし、自分で書かずに標準ライブラリを使おうぜ。って章です。こんな機能あるけど、気を付けて!こんな機能とセットで使おうね。的な感じです。

functools.wrapsを使って関数デコレータを定義する

 デコレータを正しく使いましょう。とい章ですね。関数の前後で追加コードをはさんだりできるのでデバッグとかに使いやすくなります。でも気を付けてね。functoolsを使おうね。

 単純にデコレータを使う場合には、

def trace(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print("in deco ", result)
        return result
    return wrapper

@trace
def test(a):
    print("in test ", a)
    return a+1

test(3)
print(test.__name__)
in test  3
in deco  4
wrapper

 こんな感じですが、1点おやおや?って点が。test関数の名前を聞いているのに、その奥のwrapperの名前が返ってきます。名前だけならまだしもDocstringも中のそれが返ってきてしまうのでいまいちです。

 そんな時はfunctools.wrapsを使いましょう。

from functools import wraps

def trace(func):
    @wraps(func) # ←ココ追記
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print("in deco ", result)
        return result
    return wrapper

@trace
def test(a):
    print("in test ", a)
    return a+1

test(3)
print(test.__name__)
in test  3
in deco  4
test

 こうなります。必要なプロパティはすべてコピーされるようなので、名前に限らずDocstringもコピーされるようです。知らないと書けないなぁ。こんなの。@wraps(func)を付けましょう。

 正直名前だけなら

def trace(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print("in deco ", result)
        wrapper.__name__ = func.__name__
        return result
    return wrapper

 これでも十分なのですけどね。ちょっとダサい。

 というわけで、問題を起こさないようにデコレータを自分で定義するときには、組み込みモジュールのfunctoolsのデコレータwrapsを使いましょう。という教えでした。

contextlibとwith文をtry/finallyの代わりに考える

 with文ってスマートですよね。クールですよね。でも気を付けて!これを自分で書く時にはcontextmanagerデコレータを使いましょう。

 自分で書く時も__enter__や__exit__を持つクラスを作れば、with文で使えるクラス作ることは可能なのですが、それよりもずっと簡単です。

from contextlib import contextmanager

a = "without"

@contextmanager
def test():
    global a
    a = "with"
    try:
        yield
    finally:
        a = "without"

with test():
    print(a)
    print(a)

print(a)
with
with
without

 withに入っている間だけ、文字列aの値が変わって、抜けたら元に戻っています。イメージ通りの動きです。

 yield式のところで、withブロックの内容が実行されます。withブロックで起こるすべての例外はyield式で再度起こされてtest関数の中で捕らえられます。

 withと対となるasに関しても、contextmanagerで簡単に作れます。asターゲットとしてyieldしてやればよいようです。

a = "without"

@contextmanager
def test():
    global a
    a = "with"
    b = []
    try:
        yield b
    finally:
        a = "without"
        print(b)

with test() as c:
    c.append("1")
    c.append("2")
    c.append("3")
    print(a)

print(a)
with
['1', '2', '3']
without

 ファイルハンドルとか、そんなのをyieldしてやればよさそうです。

 というわけで、覚えておくことは、with文はfry/finallyより見た目がすっきり。その時はcontextlibのcontextmanagerを使おう。yieldでasの値が返ってくるよ。ってところですかね。

copyregでpickleを信頼できるようにする

 pythonのオブジェクトをバイトストリームにしたり、逆にそのバイトストリームからオブジェクトに戻したり。そんな時に便利なのはpickleだけど、気を付けて!copyregと一緒に使おうね。

import pickle

class c:
    def __init__(self, a = 0, b = 1):
        self.a = a
        self.b = b

state = c()
state.a = 1
state.b = 2

with open("dat.bin", "wb") as f:
    pickle.dump(state, f)

with open("dat.bin", "rb") as f:
    after = pickle.load(f)

print(after.__dict__)
{'a': 1, 'b': 2}

 これはこれで正しく展開できます。でも将来class cを拡張したくなるかもしれません。なのでpickleするときに将来見越してある細工をします。これがcopyregってやつです。

import pickle, copyreg

class c:
    def __init__(self, a = 0, b = 1):
        self.a = a
        self.b = b

state = c()
state.a = 1
state.b = 2

def unpickle_c(kwargs):
    return c(**kwargs)
    
def pickle_c(a):
    kwargs = a.__dict__
    return unpickle_c, (kwargs, )

copyreg.pickle(c, pickle_c)

with open("dat.bin", "wb") as f:
    pickle.dump(state, f)

with open("dat.bin", "rb") as f:
    after = pickle.load(f)

print(after.__dict__)
{'a': 1, 'b': 2}

 ここでは同じ結果が出ます。が、pickleする関数をregしてあるので、保存されるdat.binのサイズが変わってます。(旧49 byte 新58 byte)

 ここでclass cを拡張します。前のクラスと互換性はないにもかかわらず、前のクラスで保存したファイルから、この拡張したcクラスとして展開できています。

import pickle, copyreg

class c:
    def __init__(self, a = 0, b = 1, e = 2):
        self.a = a
        self.b = b
        self.e = e # これを追加

def unpickle_c(kwargs):
    return c(**kwargs)
    
def pickle_c(a):
    kwargs = a.__dict__
    return unpickle_c, (kwargs, )

copyreg.pickle(c, pickle_c)

with open("dat.bin", "rb") as f:
    after = pickle.load(f)

print(after.__dict__)
{'a': 1, 'b': 2, 'e': 2}

 というわけで、旧フォーマットのファイルを新クラスでデフォルト引数を使って展開できました。もちろんこのcopyregしないで保存したファイルは同じように展開できません。エラーにもならないから困ったものです。

 書籍ではversion情報を入れとくといいよ。って書いてあります。なるほど。そのバージョン情報使ってif文使ってきりかえればよさそうです。

 またこのクラスcの名前を変えたくなるかもしれません。こうなるともうあきらめろと言いたくなるところですが、それもcopyregを使うと何とかなります。これはunpickleするパス(関数名みたいな理解)がこのバイナリデータに含まれているため、クラス名ではなく、unpickleする関数を通してデータが戻ってくるから。だそうです。

import pickle, copyreg

class c2: # クラス名変更
    def __init__(self, a = 0, b = 1, e = 2):
        self.a = a
        self.b = b
        self.e = e

def unpickle_c(kwargs): # ここさえ変えなければOK
    return c2(**kwargs)
    
def pickle_c2(a):
    kwargs = a.__dict__
    return unpickle_c, (kwargs, )

copyreg.pickle(c2, pickle_c2)

with open("dat.bin", "rb") as f:
    after = pickle.load(f)

print(after.__dict__)

 としても同じ結果が返ってきます。unpickle_cの関数名とIFさえ変えなければOKです。

 というわけで覚えておくことは、pickleは便利だよ。だけど将来考えてちゃんとcopyregと一緒に使おうね。って感じ。

ローカルクロックにはtimeではなくdatetimeを使う

 だそうです。なぜdatetimeが最良で、timeを避けるべきかを理解するには、両方を完全に知っておくことが必要。らしいですが、だるいのでメモだけ残しておきます。

 timeモジュールがプラットフォーム依存で、元になっているC関数がOSでどう動いているか次第になってしまい、いけてない。

 その点datetimeはいい感じ。だけどpytzってモジュールと一緒に使おうね。って感じ

組み込みアルゴリズムとデータ構造を使う

 メジャーなアルゴリズムないしデータ構造はすでに用意されているから自分で作んなよ。ってことだと思います。

 だいたい失敗して遅いコードになったり、保守が面倒だったり、バグったりするよ。って。いくつか代表的なものが紹介されています。

両端キュー(Double-ended Queue)

 前にも後ろにも出し入れできるqueueで、collectionsモジュールのdequeクラスがそれになります。普通のlistでもいいのですが、後ろならいいのですが、頭に要素を足したり削除したりするのに大きな時間がかかってしまいます。

順序つき辞書(Orderd Dictionary)

 普通の辞書だとキーと値の順序が保証されないのですが、このcollectionsモジュールのOrderdDictクラスは、キーの挿入順を記録している特別な型の辞書だそうで。便利に使いましょう。

デフォルト辞書(Default Dictionary)

 普通の辞書だとキーが無いことを想定しないといけませんが、この辞書はキーが無ければデフォルトの値を返してくれるそうです。これもコードがすっきりするので便利に使いましょう。

ヒープキュー(Heap Queue)

 優先度をつけてpopができる便利なQueueだそうです。listをsortするより早いようです。

二分法(Bisection)

 リストを二分探索してくれるようで。

イテレータツール

 組み込みモジュールitertoolsを便利に使いましょう。ということみたい。なんだかよくわからないけど。

精度が特に重要な場合はdecimalを使う

 浮動小数点演算で計算すると、割り切れる値な癖に、小数点以下がいっぱい出ちゃう。まぁたまにハマるこれですが、decimal使って回避しましょう。

a = 321.9 / 60
print(a)

5.364999999999999

 ですが、電卓たたくと

5.365

 です。

a = 3219 / 600
print(a)

 これなら平気です。困ったものです。特にお金の計算などシビアな計算の時にはdecimalを使いましょう。

from decimal import Decimal

a = Decimal("321.9") / Decimal("60")
print(a)

 これで電卓の結果と一致します。人の頭は10進なのです。

コミュニティ作成モジュールをどこで見つけられるかを知っておく

 pypiからpipでとってこようね。って。

終わり

 やっと終わりました。1章~6章までコードを書きながら真面目に読んでみました。ほぼ個人メモですが、何か困ったときにあぁあんなこと書いてあったなくらいには思い出せそうな気がします。

python
スポンサーリンク
キャンプ工学

コメント

タイトルとURLをコピーしました