概要

前回のつづきね。6章のノート。

 

楽しめたところノート

自作デコレータの気になるバグ潰し! functools.wraps

デコレータを自作するときは functools.wraps を使おう。フツーにデコレータを定義すると、デコレートされた関数には help() とかが使えなくなってしまうのだ。

import functools

def midori_decorator(func):
    """自作デコレータ!"""

    # デコレータ定義するときはこれをつけること。
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print(f'実行した関数:{func.__name__} 引数:{args} キーワード引数:{kwargs} 結果:{result}')
        return result
    return wrapper

@midori_decorator
def fibonacci(n):
    """フィボナッチ!!!"""
    if n in (0, 1):
        return n
    return (fibonacci(n - 2) + fibonacci(n - 1))

help(fibonacci)
print(fibonacci)
# help(fibonacci) でコレが出ます。 
Help on function fibonacci in module __main__:

fibonacci(n)
    フィボナッチ!!!

# print(fibonacci) でコレが出ます。
<function fibonacci at 0x000002387F061D90>

もし functools.wraps を使わなかったら次のようになってしまう。

# help(fibonacci) で wrapper 関数の help が出ちゃってる!
Help on function wrapper in module __main__:

wrapper(*args, **kwargs)

# print(fibonacci) で wrapper 関数が出ちゃってる! 思い出して、アナタは fibonacci なのよ!
<function midori_decorator.<locals>.wrapper at 0x000001FCD9BB1D90>

 

え?! 自作関数を with で使えるって?! contextlib.contextmanager

with open(...) as fwith closing(sqlite3.connect(':memory:')) as con でおなじみの with 文。まさかこの中に自分の関数を使えるときが来ようとは。 Exception を自分で作れることを知ったとき(Python raiseノート)と同じような感動があるぜ。

import contextlib
import logging

# ロギングの基本設定。(これがないと機能しなかった。)
logging.basicConfig()


# contextmanager でデコレートすると with で使えるようになります。
@contextlib.contextmanager
def log_level(level, name):
    """with の中だけログレベルを変更できる logger です!"""

    logger = logging.getLogger(name)
    old_level = logger.getEffectiveLevel()
    logger.setLevel(level)
    try:
        # contextmanager で yield した値は、 with 文の as に引き渡されます。
        yield logger
    finally:
        # with が終わったところでこの部分が実行されます。
        logger.setLevel(old_level)


# フツーのときは DEBUG レベルは print されない! レベルが WARNING だから。
logger = logging.getLogger('my-logger-out-of-with')
logger.debug('でばーぐ')

# しかし自作の with 用関数を使うと……
with log_level(logging.DEBUG, 'my-logger-in-with') as logger:
    logger.debug('でばーぐ in with sentence')

 

pickle は実は危険? アナタの pickle は大丈夫?

pickle は Python のオブジェクトを外部ファイルに保存できるパネェライブラリだ。便利だね。みろりHPを作るときにも使ったよ。ところが、その保存ファイルには、もとのオブジェクトをどのように再構築すればよいかを記述したプログラムを含んでいるらしくて、悪者のプログラムが侵入する余地があるんで危険らしい。その点 JSON はとっても安全なんだとか。

というわけで pickle は信頼されたプログラム間のみで使うべきである。

 

pickle の至らない点をカヴァー! copyreg

pickle には↑の信頼性の他に以下のような問題がある。

  • pickle で保存したオブジェクトの元となるクラスのほうにアトリビュートが足されたり引かれたりしたとき、 pickle データのほうは至らないデータとなってしまう。「そんな変更があったなんて知らないっすよ!」
  • 元となるクラスが名前変更されたら、 pickle データは困ってしまう。「俺の親どこ行った?!」

どちらも copyreg モジュールで解決する。まあこれは上の wraps や contextmanager ほどには感動しなかったから詳細ははぶこう。ようするに pickle 使うときは元のクラスとの互換性にご注意ってことだ。そのために copyreg を利用して「欠けている属性値の追加」、「クラスのバージョン管理」、「安定なインポートパスの提供」などを行うこと。

 

2020年の Pythonista にタイムゾーンは欠かせない datetime, pytz

じつはみろりHPに含まれる日付データはぜんぶ UTC で扱っており、画面に表示するときには日本時間に変換するということを行っている。なのでタイムゾーンは馴染みのある単元だ。そして datetime モジュールもガンガン使っているから大丈夫だ。ぼくは from datetime import datetime せずに import datetime して datetime.datetime.now() するほどの datetime フリーク だから問題ない。

  • time モジュールはダメ。本質的にプラットフォーム依存のモジュールだから。
  • 時刻はつねに UTC で管理し、必要なときにローカル時間に変換すること。
  • datetime モジュールと pytz モジュールを使ってそれを実現せよ。
import datetime
import pytz

TZ_JAPAN = pytz.timezone('Asia/Tokyo')
TZ_NEPAL = pytz.timezone('Asia/Katmandu')
# TZ_UTC は pytz.utc で OK。

# いまの UTC 時間。
current_utc = datetime.datetime.now(tz=pytz.utc)

# いまの日本時間。
current_japan = datetime.datetime.now(tz=TZ_JAPAN)

# naive datetime(タイムゾーンを持たないもののこと)
naive = datetime.datetime.now()
# を、日本時間にする。(localize:タイムゾーン付与)
current_japan = TZ_JAPAN.localize(naive)

# 日本時間を UTC にする。(normalize:タイムゾーン変換)
# pytz.utc がふたつもあって冗長に見えるけどこれはしかたないそうだ。
current_utc = pytz.utc.normalize(current_japan.astimezone(pytz.utc))

# UTC をネパール時間にする。ここで今の日本時間がネパール時間で何時なのかわかるね!
current_nepal = TZ_NEPAL.normalize(current_utc.astimezone(TZ_NEPAL))

# ↑のように一度 UTC にするのもいいけど、日本->ネパールに一発で変換するには?
current_nepal = TZ_NEPAL.normalize(current_japan.astimezone(TZ_NEPAL))

全部みろりHPでやっていることだ。

 

車輪の再発明ダメゼッタイ collections

ぼくはたまにプログラミング問題で遊ぶんだが……

などなど。

そういうとき、知らずのうちに、ちょー有名でビルトインモジュールに入っているようなアルゴリズムを手ずから車輪の再発明してしまうんだよな。そういうことがあんまりないように、どんな組み込みアルゴリズムがあるのかざっと眺めておこう。

  • 先入れ先出しキューを作りたいときに collections.deque()
  • 順序付き辞書がほしいときに collections.OrderedDict()
    • ただ Python3.7 からはフツーの辞書がすでに順序保証されているみたいだよ。どっちを使ったほうが効率いいのかはしらん。
  • キーが存在しないときのデフォルト値を返してほしいときに collections.defaultdict([ここに int とか str とか])
  • リストの要素に優先度をつけたいときに heappush(), heappop()
    • これ、パッと思いつかないんだけど、これまで何度も各要素に順位とかを設定するプログラムを、いちいちリストとディクショナリを併用して書いていた気がするので、ちゃんと覚えておこう。
  • めちゃめちゃでかいリストの要素を検索したいときは bisect.bisect_left() などを使うこと
  • itertools には目を通そう
    • chain 複数イテレータをくっつける(ええやん……)
    • cycle 永遠にイテレータを繰り返す(できたんか……)
    • tee イテレータを分割して並列イテレータにする(何をいってるのかわからない)
    • zip_longest 異なる長さのイテレータについて動作する zip 関数(知ってた)
    • islice イテレータ要素をスライス(これを知らなかったせいでいくつのイテレータをリスト化してきたのだろう)
    • takewhile 述語関数が True な限りイテレータからの要素を返す(おもしろい。 while を書く手間が省けるかもしれない)
    • dropwhile その逆。
    • filterfalse 述語関数が False であるイテレータ要素を返す。 filter 関数の逆。
    • product 直積(重複順列と同じだ。)
    • permutations 順列。
    • combination 組み合わせ。

 

精度をもとめるキミに decimal

decimal.Decimal() とはだいぶ前に出会って、ネタにしたな(Python Decimalでひとネタ)。でも丸めについての関数があることは知らんかった。

import decimal

# ちーさい数を 0.01 の単位に切り上げてみる。
d = decimal.Decimal('0.004166666666666666')
print( d.quantize(decimal.Decimal('0.01'), rounding=decimal.ROUND_UP) )
# 0.01 になる。

 

小休憩

組み込みモジュールの章では、単純に道具箱が充実してく感じがして楽しいね。イテレータについてはプログラミング問題をやれば使い所が見つかる。ロギングやタイムゾーンはみろりHPをメンテしていれば使える。デコレータとか with 文がむつかしいな。頭のかたすみに置いておいて、使い所がくるのを待ってみよう。

 

Effective Python 感想文一覧