はじめに
こちらの書籍のまとめになります
Effective Python 第2版 ――Pythonプログラムを改良する90項目 (Brett Slatkin 著、黒川 利明 訳、石本 敦夫 技術監修)
-
全てのパートをまとめているわけではありません
-
個人的に難しくて理解できていなかったり腑に落ちていない箇所は省いています
-
もしくは新たな気づきは特にないなと感じたところも省略しています
-
コードに関しては書籍のものを丸々掲載するでなく、改変しています(その過程も個人的に有意義な時間でした)
-
そのような理由からこのブログでは多くの部分を削ってしまっています。オリジナルの書籍はかなり勉強になるなと思いました。興味ある人は是非読んでください。
-
このページでは本書の7章中盤〜8章序盤をまとめています。他の章はこちらを参照ください
-
そもそもmultiprocessing、threading、asyncioってなんぞや?って場合、下記の記事が非常に分かりやすく参考になると思います
- Python multiprocessing vs threading vs asyncio(JX通信社テックブログ)
コルーチンで高度な並行I/Oを
- JavaScriptでよく出てくる
Promise
、async/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
- 同じ処理が
contextlib
とtry/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'}