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

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

[Effective Python 第2版 ――Pythonプログラムを改良する90項目](https://www.oreilly.co.jp/books/9784873119175/) (Brett Slatkin　著、黒川 利明　訳、石本 敦夫　技術監修)

- 全てのパートをまとめているわけではありません
- 個人的に難しくて理解できていなかったり腑に落ちていない箇所は省いています
- もしくは新たな気づきは特にないなと感じたところも省略しています
- コードに関しては書籍のものを丸々掲載するでなく、改変しています（その過程も個人的に有意義な時間でした）
- そのような理由からこのブログでは多くの部分を削ってしまっています。オリジナルの書籍はかなり勉強になるなと思いました。興味ある人は是非読んでください。
- このページでは本書の７章中盤〜８章序盤をまとめています。他の章はこちらを参照ください

    - [１〜３章](2022-06-07_Python_pythonic_d49713937b1b482458b1.md)
    - [４〜５章](2022-06-14_Python_e6d33a61568df437925f.md)
    - [６〜７章序盤](2022-06-17_Python_01d151ff92b98e83231d.md)
    - [７章中盤〜８章序盤](2022-06-20_Python_asyncio_d18df3835b07dd4d2c4a.md)
        - 7章は全体的に難易度が高くほとんどの部分を削っています
    - [８章中盤〜１０章](2022-07-13_EffectivePython_Python_c8638b85efc17bcff049.md)

- そもそもmultiprocessing、threading、asyncioってなんぞや？って場合、下記の記事が非常に分かりやすく参考になると思います
    - [Python multiprocessing vs threading vs asyncio](https://tech.jxpress.net/entry/2021/07/29/101150)（JX通信社テックブログ）

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

```python
### まずは通常の同期処理、合計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.
```

```python
### シンプルな記述で非同期処理を実行できる

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をしている例

```python
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`文を使うことで容易に実装できるけど、、

```python
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`を使うことで自動的にファイルがクローズされる
- 下記のイメージ

```python
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
```

- 同じ処理が`contextlib`と`try/finally`を使うとこう書ける

```python
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`のさらなる詳細については以下のブログに分かりやすくまとめられていました
    - [iMind Developers Blog](https://blog.imind.jp/entry/2019/07/06/144729)

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

```python
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クラスの仕様を変更したが最後、ややこしいことになってしまっている

```python
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の振る舞いを制御できる
- 下記例は更新後のクラスにデフォルト属性を追加することで新たな情報を追加した例

```python
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を起こす

```python
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を利用することでクラスのバージョン管理を行うこともできる
- 元々のクラスから属性値を削除することもできる

```python
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'}
```