LoginSignup
4
3

Effective Python 第2版 を自分なりにまとめてみる part4

Last updated at Posted at 2022-06-20

はじめに

こちらの書籍のまとめになります

Effective Python 第2版 ――Pythonプログラムを改良する90項目 (Brett Slatkin 著、黒川 利明 訳、石本 敦夫 技術監修)

  • 全てのパートをまとめているわけではありません

  • 個人的に難しくて理解できていなかったり腑に落ちていない箇所は省いています

  • もしくは新たな気づきは特にないなと感じたところも省略しています

  • コードに関しては書籍のものを丸々掲載するでなく、改変しています(その過程も個人的に有意義な時間でした)

  • そのような理由からこのブログでは多くの部分を削ってしまっています。オリジナルの書籍はかなり勉強になるなと思いました。興味ある人は是非読んでください。

  • このページでは本書の7章中盤〜8章序盤をまとめています。他の章はこちらを参照ください

  • そもそもmultiprocessing、threading、asyncioってなんぞや?って場合、下記の記事が非常に分かりやすく参考になると思います

コルーチンで高度な並行I/Oを

  • JavaScriptでよく出てくるPromiseasync/awai、だいぶ苦戦した経験がありますが、Pythonでもコルーチンで非同期処理がかけます
  • マルチスレッドでは、処理の裏で別の処理が同時に走るため、値の読み/書きでは必ずロックやスレッドセーフを考慮しなければない
  • しかしasync/awaitの場合、全ての処理は直列に走るので、余計な心配をする必要がない
  • I/Oでの待ち時間の場合に他の処理にCPUを譲るだけ
### まずは通常の同期処理、合計8秒かかる

import time


def normal_sleep(t, log_output):
    print(f'start {log_output}')
    time.sleep(t)
    print(f'finish {log_output}')


def main():
    normal_sleep(1, 'First')
    normal_sleep(4, 'Second')
    normal_sleep(3, 'Third')


start_time = time.time()
main()
end_time = time.time()
print(f'{end_time - start_time:.3f} sec.')
[out]
start First
finish First
start Second
finish Second
start Third
finish Third
8.015 sec.
### シンプルな記述で非同期処理を実行できる

import asyncio


async def async_sleep(t, log_output):
    print(f'start {log_output}')
    await asyncio.sleep(t)
    print(f'finish {log_output}')


async def main():
    task1 = asyncio.create_task(async_sleep(1, 'First'))
    task2 = asyncio.create_task(async_sleep(4, 'Second'))
    task3 = asyncio.create_task(async_sleep(3, 'Third'))
    await task1
    await task2
    await task3


start_time = time.time()

# jupyter上で実行した場合
await main()

# Pythonスクリプトでの実行は
# asyncio.run(main())

end_time = time.time()
print(f'{end_time - start_time:.3f} sec.')
  • なお、先に始まったはずのSecondよりもThirdが先に終わっている
    • 自身が待ちアイドル状態になった時に順番を譲るが、他処理が続いている間に割り込むことはしない
[out]
start First
start Second
start Third
finish First
finish Third
finish Second
4.003 sec.
  • 以下はHTTPヘッダーを取得している待ち時間の間にcpu_workをしている例
import asyncio
import urllib.parse
import sys


async def print_http_headers(url):
    url = urllib.parse.urlsplit(url)
    if url.scheme == 'https':
        reader, writer = await asyncio.open_connection(
            url.hostname, 443, ssl=True)
    else:
        reader, writer = await asyncio.open_connection(
            url.hostname, 80)

    query = (
        f"HEAD {url.path or '/'} HTTP/1.0\r\n"
        f"Host: {url.hostname}\r\n"
        f"\r\n"
    )

    writer.write(query.encode('latin-1'))
    while True:
        line = await reader.readline()
        if not line:
            break

        line = line.decode('latin1').rstrip()
        if line:
            print(f'HTTP header> {line}')

    # Ignore the body, close the socket
    writer.close()


async def cpu_work():
    for i in range(10**8):
        if i % 10**7 == 0:
            print(i)
            await asyncio.sleep(0.1)


async def main():
    print('https request start')
    task1 = asyncio.create_task(print_http_headers('https://example.com/path/page.html'))
    print('cpu word start')
    task2 = asyncio.create_task(cpu_work())
    await task1
    await task2


await main()
[out]
https request start
cpu word start
0
10000000
20000000
30000000
40000000
HTTP header> HTTP/1.0 404 Not Found
HTTP header> Accept-Ranges: bytes
HTTP header> Age: 147082
HTTP header> Cache-Control: max-age=604800
HTTP header> Content-Type: text/html; charset=UTF-8
HTTP header> Date: Fri, 17 Jun 2022 11:13:46 GMT
HTTP header> Expires: Fri, 24 Jun 2022 11:13:46 GMT
HTTP header> Last-Modified: Wed, 15 Jun 2022 18:22:24 GMT
HTTP header> Server: ECS (oxr/8322)
HTTP header> X-Cache: 404-HIT
HTTP header> Content-Length: 445
HTTP header> Connection: close
50000000
60000000
70000000
80000000
90000000

頑健性と性能

try/except/else/finallyのおさらい

  • よく使われるfinallyの用法として挙げられるのがfileのクローズ
  • ただこれはwith文を使うことで容易に実装できるけど、、
filename_good = 'good.txt'
with open(filename_good, 'wb') as f:
    f.write(b'Good')

filename_bad = 'bad.txt'
with open(filename_bad, 'wb') as f:
    f.write(b'\xf1\xf2')   # 不当なutf-8


def read_binary_txt(filename):
    print('* Open')
    handle = open(filename, encoding='utf-8')

    try:
        print('* Read')
        return handle.read()   # ここでUnicodeDecodeError
    finally:
        print('* Close')   # エラーがあってもなくてもfileをクローズ
        handle.close()

text = read_binary_txt(filename_good)
print(text)
print()
text = read_binary_txt(filename_bad)
[out]
* Open
* Read
* Close
Good

* Open
* Read
* Close

Pythonのcontextlibでwithに渡せる処理を定義する

  • opne関数はデフォルトで__enter____exit__メソッドを持っているのでwithを使うことで自動的にファイルがクローズされる
  • 下記のイメージ
class MyOpen:
    def __init__(self, path):
        self.path = path

    def __enter__(self):
        self.f = open(self.path)
        return self.f

    def __exit__(self, exception_type, exception_value, traceback):
        self.f.close()


with MyOpen('good.txt') as f:
    text = f.read()

print(text)
print(f'Is closed? -> {f.closed}')
[out]
Good
Is closed? -> True
  • 同じ処理がcontextlibtry/finallyを使うとこう書ける
from contextlib import contextmanager


@contextmanager
def my_open(path):
    try:
        f = open(path)
        yield f   # as で渡されるオブジェクトをここで定義している
    finally:
        f.close()


with my_open('good.txt') as f:
    text = f.read()

print(text)
print(f'Is closed? -> {f.closed}')
[out]
Good
Is closed? -> True
  • contextlibのさらなる詳細については以下のブログに分かりやすくまとめられていました

ローカルクロックには time よりも datetime & pytz

  • UNIX時間 ⇆ ホストコンピュータのローカル時間にtimeを使うのはいいものの、各地域のローカル時間を跨ぐときに使うのは避けるべき
  • 思わぬエラーの元
from datetime import datetime
import time
import pytz


now = datetime(2022, 1, 1, 0, 0, 0)
print(now)   # 2022-01-01 00:00:00

# 表記の変更
now_str = datetime.strftime(now, '%Y年%m月%d日 %H時%M分%S秒')
print(now_str)   # 2022年01月01日 00時00分00秒

# UNIX time
now_utc = now.timetuple()
now_utc = time.mktime(now_utc)
print(now_utc)   # 1640962800.0

# ローカル時間へ変換
tokyo = pytz.timezone('Asia/Tokyo')
eastern = pytz.timezone('US/Eastern')

tokyo_dt = tokyo.localize(now)
print(tokyo_dt)   # 2022-01-01 00:00:00+09:00
tokyo_dt = pytz.utc.normalize(tokyo_dt.astimezone(pytz.utc))
print(tokyo_dt)   # 2021-12-31 15:00:00+00:00

eastern_dt = eastern.localize(now)
print(eastern_dt)   # 2022-01-01 00:00:00-05:00
eastern_dt = pytz.utc.normalize(eastern_dt.astimezone(pytz.utc))
print(eastern_dt)   # 2022-01-01 05:00:00+00:00

copyregでpickleの信頼性を高める

  • 途中でHumanクラスの仕様を変更したが最後、ややこしいことになってしまっている
import pickle


class Human:
    def __init__(self, name, age):
        self.name = name
        self.age = age


takayoshi = Human('Takayoshi', age=0)
with open('tmp.pkl', 'wb') as f:
    pickle.dump(takayoshi, f)


# Classの設計をいじる
class Human:
    def __init__(self, name, age, height):
        self.name = name
        self.age = age
        self.height = height

with open('tmp.pkl', 'rb') as f:
    takayoshi = pickle.load(f)

print(isinstance(takayoshi, Human))   # -> これがTrueになってしまう
  • copyregを使う
  • Pythonオブジェクトをシリアライズ/デシリアライズする際に対象となる関数を登録することでpickleの振る舞いを制御できる
  • 下記例は更新後のクラスにデフォルト属性を追加することで新たな情報を追加した例
import copyreg


class Human:
    def __init__(self, name, age):
        self.name = name
        self.age = age


def pickle_human_state(human):
    kwargs = human.__dict__
    return unpickle_human_state, (kwargs,)


def unpickle_human_state(kwargs):
    return Human(**kwargs)


# 上記の情報を登録
copyreg.pickle(Human, pickle_human_state)

# 初期クラスから生成されたインスタンスを保存
takayoshi = Human('Takayoshi', 0)
takayoshi.age += 10
serialized = pickle.dumps(takayoshi)
takayoshi = pickle.loads(serialized)
print(takayoshi.__dict__)   # -> {'name': 'Takayoshi', 'age': 10}


# クラスの仕様を変更
class Human:
    def __init__(self, name, age, height=140):
        self.name = name
        self.age = age
        self.height = height

# 先ほどセーブしたインスタンスに新しいプロパティがheightが追加されている
takayoshi = pickle.loads(serialized)
print(takayoshi.__dict__)   # -> {'name': 'Takayoshi', 'age': 10, 'height': 140}
  • 尚、デフォルト属性でない場合、以下のようにTypeErrorを起こす
class Human:
    def __init__(self, name, age, height):
        self.name = name
        self.age = age
        self.height = height

takayoshi = pickle.loads(serialized)
print(takayoshi.__dict__)
[out]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [7], in <cell line: 7>()
      4         self.age = age
      5         self.height = height
----> 7 takayoshi = pickle.loads(serialized)
      8 print(takayoshi.__dict__)

Input In [6], in unpickle_human_state(kwargs)
     15 def unpickle_human_state(kwargs):
---> 16     return Human(**kwargs)

TypeError: Human.__init__() missing 1 required positional argument: 'height'
  • copyregを利用することでクラスのバージョン管理を行うこともできる
  • 元々のクラスから属性値を削除することもできる
print(takayoshi.__dict__)

# 再度クラスを更新
class Human:
    def __init__(self, name):
        self.name = name


def pickle_human_state(human):
    kwargs = human.__dict__
    kwargs['version'] = 2
    return unpickle_human_state, (kwargs,)


def unpickle_human_state(kwargs):
    version = kwargs.pop('version', 1)   # key:versionがないときは1を返す
    if version == 1:
        kwargs.pop('age')   # serializedされた元々のclassにはnameとageの2つがあったので除外
    return Human(**kwargs)

takayoshi = pickle.loads(serialized)
print(takayoshi.__dict__)
[out]
{'name': 'Takayoshi', 'age': 10, 'height': 140}
{'name': 'Takayoshi'}
4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3