Table of Contents
- 1. Chapter 3: 関数
- 1.1. Item 19: 関数が複数の値を返すとき、4つ以上の変数にunpackするな
- 1.2. Item 20: Noneを返すくらいなら例外を上げよ
- 1.3. Item 21: Closuresが変数のスコープにどう影響するかを知っておけ
- 1.4. Item 22: 可変数のpositional argumentsを使って見やすくしろ
- 1.5. Item 23: オプションとなる挙動はキーワード引数で与えよ
- 1.6. Item 24: 動的なデフォルト引数を指定するときはNoneとDocstringsを使え
- 1.7. Item 25: キーワードオンリー引数、位置オンリー引数を使って、明確さを強制せよ
- 1.8. Item 26: 関数のデコレーターをfunctools.wrapsを使って定義せよ
- 2. Chapter 4: Comprehensionsとジェネレーター
- 2.1. Item 27: mapとfilterの代わりにcomprehensionsを使え
- 2.2. Item 28: 3つ以上のコントロールsubexpressionsをcomprehensionsで使うな
- 2.3. Item 29: Assignment表現を使って、comprehensions内での繰り返しを避けよ
- 2.4. Item 30: リストを返すくらいならジェネレーターを考慮せよ
- 2.5. Item 31: 引数をたどるときには保守的になれ
- 2.6. Item 32: 大きなリストcomprehensionsの代わりにジェネレーターexpressionsを考えよ
- 2.7. Item 33: yield fromを使って複数のジェネレーターを組み合わせよ
- 2.8. Item 34: ジェネレーターにsendを使ってデータを送ってはいけない
- 2.9. Item 35: throwで例外を投げてジェネレータの状態遷移を起こすな
- 2.10. Item 36: Iteratorやジェネレーターを使うときにはitertoolsの使用を検討せよ
1 Chapter 3: 関数
1.1 Item 19: 関数が複数の値を返すとき、4つ以上の変数にunpackするな
うっかり順番を間違えたりするので。catch-allの星付きexpression(eg, *others)を使うか、namedtupleを使う。
namedtupleについてはここの説明がわかりやすかったです。(逆にIntroducing Pythonの説明はさっぱり。。。) 以下のように使います。
from collections import namedtuple Car = namedtuple('Car', 'color milage') または Car = namedtuple('Car', ['color', 'milage'])
前者の文字列部分('color milage'
)は内部で split()
されてリストになるため、後者と等価だそうです。
メソッドの無いimmutableなクラスのように使えます。
my_car = Car('red', 3812.4)
namedtupleは内部ではクラスとして表現されていますが、メモリ効率は良いそうです。
複数の値を返す関数は、それらをタプルにして返すことは知りませんでした。関数の呼び出し元が複数の変数にunpackする時にタプルから取り出します。
もう一つ。日本語ではあまり馴染みがありませんが、英語ではよく使うmaiden(中央値)。要素が偶数の場合にどうするのか知りませんでした。真ん中の二つの平均を取るのですか。
if count % 2 == 0: lower = sorted_numbers[middle - 1] upper = sorted_numbers[middle] median = (lower + upper) / 2
1.2 Item 20: Noneを返すくらいなら例外を上げよ
None
や0, 空の文字列, etc.は全て False
と解釈されるので、if文などでの判定でうっかり間違いやすい。
適切な例外(そのままでなく)を上げて、それをドキュメントに書く。更にtype annotationsする。
def careful_divide(a: float, b: float) -> float: """Divides a by b. Raises: ValueError: When the inputs cannot be divided. """" try: return a / b except ZeroDivisionError as e: raise ValueError('Invalid inputs')
1.3 Item 21: Closuresが変数のスコープにどう影響するかを知っておけ
えーっと、closuresって何でしたっけ? Introducing Pythonを読んだときに消化不良のまま終わったような気が。。
30分ほど調べてみました。ここの定義が一番しっくり来ました。
- 関数と、そのインナー関数がある
- インナー関数は、外側の関数で定義された変数を参照する
- 外側の関数は、インナー関数を戻り値として返す
def print_msg(msg): def printer(): # inner function print(msg) # msg was defined in the enclosing function return printer # return the inner function
これを使うには、
another = print_msg("Hello") another() >>> Hello
メリットの説明はここがわかりやすかったです。
- コールバックとして定義されるため、ある意味データ隠蔽(hiding)に使える
- ハードコードされた定数や文字列の代わりに使える
- コードに関数が1, 2個しかないときに有効
この節では、やたらと小難しい具体例が続きますが、言っていることは単純です: インナー関数でアサインしている変数のスコープは、インナー関数で閉じる(ie, 外側の関数に届かない)。そして、もし外側の関数にスコープを広げたい場合は nonlocal
を付けます。
この nonlocal
の使い方にも注意が必要で、長い関数だと nonlocal
であることがわかりづらくなります。単純な関数以外ではヘルパークラスにするのがよい、とのこと。
class SomeClass: def __call__(self, x, y): <snip> sc = SomeClass() sc(5, 7) # __call__が使われる
__call__()
スペシャル関数は、クラスを関数であるかのように振る舞わせる関数。なお、この大事なところでKindle版は最後の return (1, x)
のインデントがずれています。しかも、Click here to view code imageがここには何故か無い。どうしろと。。。
1.4 Item 22: 可変数のpositional argumentsを使って見やすくしろ
- 可変長の引数(0個含む)は関数の定義def文において
*args
のようにアスタリスク付きで書くことで実現できる - 関数の呼び出し側で、
*vars
としてアスタリスクを付けて引数を指定することで、リスト等のシーケンスの中身を(展開して)渡せる
二つ目において、呼び出し側がgeneratorにアスタリスクを付けると、呼び出しのたびにgeneratorを全て展開してしまうため、メモリ圧迫&クラッシュに注意、とのことです。
1.5 Item 23: オプションとなる挙動はキーワード引数で与えよ
ここは特にコメントなしです。
1.6 Item 24: 動的なデフォルト引数を指定するときはNoneとDocstringsを使え
例:
def log(message, when=None): “““Log a message with a timestamp. Args: message: Message to print. when: datetime of when the message occurred. Defaults to the present time. ””” if when is None: when = datetime.now() print(f'{when}: {message}')
上の関数において、whenのデフォルトはNoneとしておき、関数の中で現在時刻を設定しています。def文で when=datetime.now()
を書いても機能しないのは、 datetime.now()
が評価されるのが関数のロード時のみだからです。同様に、 {}
, []
のような動的な値を使うときにも None
をデフォルト値として置きます。
例えば、以下のケースでは {}
の同一オブジェクトが各呼び出し間で共有されてしまいます。
def decode(data, dafault={}) try: return json.loads(data) except ValueError: return default
1.7 Item 25: キーワードオンリー引数、位置オンリー引数を使って、明確さを強制せよ
float('inf')
は無限大を意味するそうです。
引数リストにあるアスタリスク *
は、位置引数の終わりと、キーワードオンリー引数の始まりを強制します。また、Python 3.8より、引数リストにある /
は、これよりも前の引数は位置オンリー引数であることを強制します。例えば、
def func(num, div, /, *, ov_flag=False, zd_flag=True):
とあったときに、
func(10, 2, True, False)
や
func(num=10, div=2)
などと呼び出すとTypeErrorとなります。
/
と *
に挟まれた引数は、位置引数としてもキーワード引数としてもどちらでもよいそうです。これがPythonのデフォルトです。
1.8 Item 26: 関数のデコレーターをfunctools.wrapsを使って定義せよ
コードスニップを見ていると、見慣れない表現が。。。
{args!r}
この !r
は、 __repr__()
で解釈せよ、という意味だそうです。 f'This string {name!r} is a good one' のように使います。関係ありませんが、 !r
はgoogle等で検索しづらいですね。。
デコレーターは難しいですね。 ここ の説明がわかりやすかったです。なるほど、 __name__
や __doc__
を表示したり help()
を使うと、インナー関数でなくwrapper関数自体の情報が表示されてしまうようです。
その解決策がfunctools.wrapsを使うことで、
from functools import wraps def mydeco(func): @wraps(func) def wrapper(*args, *kwargs): return f'{func(args, **kwargs)}!!!' return wrapper
wraps
デコレーターはインナー関数のdoc stringやその他の情報を外側のwrapper関数にコピーしてくれます。
2 Chapter 4: Comprehensionsとジェネレーター
ここを参考にして、ジェネレーターについて整理します。(このリンクはかなり詳細に説明しています)
- ジェネレーター関数は、
return
の代わりにyield
を持つ関数で、ジェネレーター(=ジェネレーターiterator)を作る。 - ジェネレーターは特定のタイプのiterator。iteratorとして機能するために、ジェネレーターは
__next__()
メソッドを持つ。 - ジェネレーターから次の値を得るには、iterators:
next()
を使う。(next()
は__next__()
メソッドを呼ぶ) - ジェネレーターは
next()
の呼び主に対して、yield
文で値を返す。
ジェネレーターの使い方
- 一通りたどるとジェネレーターは
StopIteration
例外を上げる。これは普通のiteratorと同じ挙動で、for文などでは問題視せずにサイレントに抜ける。 - その後、もう一度ジェネレーターを呼ぶと
StopIteration
例外を上げる。 - ジェネレーター関数をもう一度呼んで、新たなジェネレーターを作り直すことができる。
2.1 Item 27: mapとfilterの代わりにcomprehensionsを使え
map()
ビルトイン関数は知りませんでした。ここによると、第二引数のiterable(例: リスト)の各アイテムに、第一引数の関数を適用して、iterableなmapオブジェクトを返す、とのこと。
mapobj = map(lambda x: x ** 2, somelist)
filter
ビルトイン関数も知りませんでしたが、名前から明らかです。
filter(lambda x: x % 2 == 0, somelist)
filter
もフィルターされたiteratorを返します。
両方とも返すのはiteratorですが、リストcomprehensionsと似てますね。
map
と filter
を組み合わせると読みづらいので、スッキリとかけるcomprehensionsを使え、ということでした。 somelist
の中から2で割りきれる要素のみ2乗したいとき、
map(lambda x: x**2, filter(lambda x: x % 2 == 0, somelist))
これが、
[x**2 for x in somelist if x % 2 == 0]
。。。確かに。
2.2 Item 28: 3つ以上のコントロールsubexpressionsをcomprehensionsで使うな
初級者的にキツい表現が出ました。やりたいことは、二次元にネストされたリストであるmatrixの中身をflattenする(一次元にする)、です。えーと、ネストされた for
は左から解釈するらしいので、、
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] flat = [x for row in matrix for x in row]
最初の for
で matrix
の各アイテム= row
を取り出して、次の for
で row
の中の x
を取り出し、これを使ってリストを作る、ということでした。
print(flat) >>> [1, 2, 3, 4, 5, 6, 7, 8, 9]
これは便利そうです。
次は、先ほど matrix
の要素を二乗する(flattenしない)場合。
squared = [[x ** 2 for x in row] for row in matrix] print(squared) >>> [[1, 4, 9], [16, 25, 36], [49, 64, 81]]
このリストcomprehensionのリストが二重になっているので、flattenしないのですね。
お題のコントロールsubexpressionsは、 for
によるループだけでなく、 for
に付けられる if
も指していました。下の例は for
が二つと if
があるのでアウト。
[x for wor in matrix for x in row if x % 2 == 0]
2.3 Item 29: Assignment表現を使って、comprehensions内での繰り返しを避けよ
Assignment expressions - Walrusのやつです(:=
)。これは便利なので、是非とも使っていきたいと思います。
次のようなストックがあって、
stock = { 'nails': 125, 'screws': 35, 'wingnuts': 8, 'washers': 24, }
次のようなオーダー(8個単位とする)が来た場合について考えます。
order = ['screws', 'wingnuts', 'clips']
ディクショナリcomprehensionを使って、次のように書くと良いです。
def get_batches(count, size): return count // size found = {name: batches for name in order if (batches := get_batches(stock.get(name, 0), 8))}
get_batches
を使って、8個のセットが何セットあるかを求めています。
ポイントWalrusを使って get_batches
の実行をif節の中で1回だけ使っているところ。
print(found) >>> {'screws': 4, 'wingnuts': 1}
知りませんでした、generator expressions。ここの説明がわかりやすかったです。まずは似たようなリストcomprehension:
listcomp = ['Hello' for i in range(3)]
これがジェネレーターexpression:
genexpr = ('Hello' for i in range(3))
後者が返すのはジェネレーターです。
>>> next(genexpr) 'Hello' >>> next(genexpr) 'Hello' >>> next(genexpr) 'Hello' >>> next(genexpr) StopIteration
先ほどの例はジェネレーターexpressionを使っても書けます。
found = ((name, batches) for name in order if (batches := get_batches(stock.get(name, 0), 8)))
もちろん、返るのはジェネレーター。
print(next(found)) print(next(found)) >>> ('screws', 4) ('wingnuts', 1)
2.4 Item 30: リストを返すくらいならジェネレーターを考慮せよ
お、なんだか今更ジェネレーターの説明がありました。どうしてジェネレーターexpressionsを説明なしで使った後にこれが来るかな。。。
この本は、どこから読んでも良いと謳っていますが、逆に、最初から読もうが途中から読もうが読みづらさは変わらない、という残念なことになっていると思います。
- リストにまとめて返すより、generatorsを使ったほうがよりクリア
- generatorsはメモリ使用量が少ない
2.5 Item 31: 引数をたどるときには保守的になれ
ジェネレーター関数の read_visits()
は、1行に一つの数字(入場者数)が書いてあるファイルを読んで yield します。
def read_visits(data_path): with open(data_path) as f: for line in f: yield int(line)
normalize_func()
は入場者数をyieldするiterator(正確にはiteratorを返すジェネレーター関数)を引数に取り、毎日の入場者数をパーセントに変換したリストを返します。
def normalize_func(get_iter): total = sum(get_iter()) # 最初にiteratorを使いきる result = [] for value in get_iter(): # 更にiteratorをループで回す percent = 100 * value / total result.append(percent) return result
この時、以下がどうなるかぱっと見、わかりずらいです。特にlambdaが何をしているのか。
percentages = normalize_func(lambda: read_visits(path))
もしlambdaが無ければ、normalize_funcの引数read_visits(path)はiteratorをそのまま返します。normalize_funcに渡したiteratorは最初にsumを取るところでexhaustされ、次のfor文では既にexhaustしたままのため、このfor文のブロックは一度も実行されません。
これに lambda
が付くと、normalize_func()に渡されるlambdaの無名関数は、iteratorそのものでなく、iteratorを(呼び出しの度に)返す関数です。
sum関数でiteratorをいったんexhaustさせますが、次のfor文ではまた新たな(ie, exhaustしていない)iteratorが返されるので、for文ブロックが期待通り実行されます。
ただ、お勧めのやり方はこれではなく、自分で __iter__()
メソッドを書いたクラスを用意することでした。 __iter__()
メソッドはジェネレーター関数でなくてはならず、iteratorを返します。
class ReadVisits: def __init__(self, data_path): self.data_path = data_path def __iter__(self): with open(self.data_path) as f: for line in f: yeild int(line)
iteratorを返す __iter__()
メソッドを持つクラスを用意することで、このクラスのオブジェクトをfor文などでiterateすることが可能です。
また、normalize_funcにおいて、引数がiteratorそのものならばexceptionを上げるようにチェックを追加できます。
def normalize_defensive(numbers): if iter(numbers) is numbers: # An iterator -- bad!
または、
from collections.abc import Iterator def normalize_defensive(numbers): if ininstance(numbers, Iterator):
2.6 Item 32: 大きなリストcomprehensionsの代わりにジェネレーターexpressionsを考えよ
(今更ですが)ジェネレーターexpressionsの説明でした。その戻り値であるiteratorを入力とした、ジェネレーターexpressionsのチェイン実行も可能で効率的。
it = (len(x) for x in open('my_file.txt')) roots = ((x, x**0.5) for x in it)
listcompsは大量のインプットがある時にメモリを使いすぎてしまいますが、genexpsはメモリ効率がとても良いです。
2.7 Item 33: yield fromを使って複数のジェネレーターを組み合わせよ
yield from
です。for文で yield
を回すところを yeild from
とシンプルに書け、リーダビリティーを向上させるだけでなく、ループで yield
を回すよりも高速です。 yield
を組み合わせるケースでは、可能な限り yeild from
を使うべきです。
def slow(): for i in child(): yield i
は、以下のようによりシンプルに書けます。
def fast(): yield from child()
2.8 Item 34: ジェネレーターにsendを使ってデータを送ってはいけない
えー、前の項目で何とか理解したばかりだというのに、 send()
を使うなと言っています。初心者にわかりづらいのと、 yield from
を使って初回に None
が返るケースでびっくりすることがあるため。この節はかなり難しいです。
def my_generator(): received = yield 1 it = iter(my_genarator()) output = it.send(None)
このコードにおいて、最後の行でitに対して(初めて)sendが呼ばれる時、 yield 1
部分のみが実行され、 received =
部分はまだ実行されません。つまり、sendからは何も受け取りません。receivedにsendから値が渡されるのはit.sendが再度呼ばれてジェネレーターが再開するタイミングです。確かに、慣れていないとわかりずらいです。
def wave_modulating(steps): step_size = 2 * math.pi / steps amplitude = yield # Receive initial amplitude for step in range(steps): radians = step * step_size fraction = math.sin(radians) output = amplitude * fraction amplitude = yield output # Receive next amplitude
上記で、最初にamplitudeの初期値を受け取るところが、yield fromするとNoneが返る箇所です。
def complex_wave_modulating(): yield from wave_modulating(3) yield from wave_modulating(4) yield from wave_modulating(5)
def run_modulating(it): amplitudes = [ None, 7, 7, 7, 2, 2, 2, 2, 10, 10, 10, 10, 10] for amplitude in amplitudes: output = it.send(amplitude) transmit(output) # outputをprintする
complex_wave_modulatingの yield from
と、run_modulatingの send
を以下のように組み合わせます。複雑でだいぶ頭が混乱しますが、、、
run_modulating(complex_wave_omdulating()) >>> Output is None Output: 0.0 Output: 6.1 Output: -6.1 Output is None Output: 0.0 Output: 2.0 Output: 0.0 Output: -10.0 Output is None Output: 0.0 Output: 9.5 Output: 5.9
。。。うまくありません。yield fromで引数を渡すところがNoneを返すため、出力にNoneが入り込んでしまいます。yield fromとsendの組み合わせはいまいちです。
これを、 send()
を使わずに、iteratorを引数で渡しておくように修正します。
def wave_cascading(amplitude_it, steps): step_size = 2 * math.pi / steps for step in range(steps): radians = step * step_size fraction = math.sin(radians) amplitude = next(amplitude_it) # Get next input output = amplitude * fraction yield output
最初の呼び出しでNoneをyieldすることが無くなりました。
そして、次のcomplex_cascadingから、このwave_cascadingをyield fromで組み合わせてジェネレーター関数のカスケードをします。
def complex_cascading(amplitude_it): yield from wave_cascading(amplitude_it, 3) yield from wave_cascading(amplitude_it, 4) yield from wave_cascading(amplitude_it, 5)
そして更に、
def run_cascading(): amplitudes = [7, 7, 7, 2, 2, 2, 2, 10, 10, 10, 10, 10] it = complex_cascading(iter(amplitudes)) for amplitude in amplitudes: output = next(it) transmit(output)
amplitudesから抽出したiteratorをcomplex_cascadingに渡してiterator it
を作ります。そして、amplitudesをループで回し、 it
からyieldした値をtransmitします。
なんだかやたらと複雑ですが、transmitでNoneが返らずに正しく動くようになりました。
まとめると、カスケードさせたジェネレーターとsendを組み合わせるのは筋が悪く、iteratorを引数として渡した方がよい、ということでした。
2.9 Item 35: throwで例外を投げてジェネレータの状態遷移を起こすな
throw
を使う駄目な例:
def run(): it = timer(4) while True: try: if check_for_reset(): current = it.throw(Reset()) # 自分で例外を投げている else: current = next(it) # ここで例外が発生する可能性あり except StopIteration: break else: # 例外が発生しなかったら announce(current)
何だか、何がしたいのかよくわかりません。。。。ので調べます。ここによると、 it.throw()
はit内部で例外を発生させるようです。 it
の Reset()
例外ハンドラーでタイマーをリセットしているのかな。確かに、少し読みづらいですかね。
ジェネレーターの __iter__()
を持つクラスを用意して run()
を書き換えます。
class Timer: def __init__(self, period): self.current = period self.period = period def reset(self): self.current = self.period def __iter__(self): while self.current: self.current -= 1 yield self.current
run()
関数はずっとシンプルになります。
def run(): timer = Timer(4) for current in timer: if check_for_reset(): timer.reset() announce(current)
2.10 Item 36: Iteratorやジェネレーターを使うときにはitertoolsの使用を検討せよ
itertoolsには便利な関数がたくさんあるので使うとよい、という節でした。
2.10.1 Iteratorsをつなぐ
- chain
it = itertools.chain([1, 2, 3], [4, 5, 6]) print(list(it)) >>> [1, 2, 3, 4, 5, 6]
- repeat
it = itertools.repeat('hello', 3) print(list(it)) >>> ['hello', 'hello', 'hello']
もっと簡単な方法はないのでしょうか。実験してみます。
>>> 'hello' * 3 'hellohellohello' >>> ['hello' * 3] ['hellohellohello'] >>> list('hello' * 3) ['h', 'e', 'l', 'l', 'o', 'h', 'e', 'l', 'l', 'o', 'h', 'e', 'l', 'l', 'o']
- cycle
it = itertools.cycle([1, 2]) result = [next(it) for _ in range(10)] print(result) >>> [1, 2, 1, 2, 1, 2, 1, 2, 1, 2]
- tee - iteratorを指定個数に分割して並列実行
it1, it2, it3 = itertools.tee(['first', 'second'], 3) print(list(it1)) print(list(it2)) print(list(it3)) >>> ['first', 'second'] ['first', 'second'] ['first', 'second']
- zip_longest - zipと似ているが、一番長いiteratorに合わせる。
2.10.2 Iteratorからアイテムをフィルタする
- islice - スライスする。ステップも指定可
first_five = itertools.islice(values, 5) # 最初の5個 mkddle_odds = itertools.islice(values, 2, 8, 2) # 3番目からステップ2で7まで
- takewhile - 指定関数がFalseを返すまでtakeする
values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] less_than_seven = lambda x: x < 7 it = itertools.takewhile(less_than_seven, values) print(list(it)) >>> [1, 2, 3, 4, 5, 6]
- dropwhile - takewhileの逆。指定関数がTrueを返すまでドロップする
- filterfalse - filterの逆
2.10.3 複数iteratorsのアイテムを結びつける
- accumulate - 積算する
二つ目の引数に関数を指定して、加工が可能。
values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] sum_reduce = itertools.accumulate(values) print('Sum: ', list(sum_reduce)) >>> Sum: [1, 3, 6, 10, 15, 21, 28, 36, 45, 55] def sum_modulo_20(first, second): return (first + second) % 20 modulo_reduce = itertools.accumulate(values, sum_modulo_20) print('Modulo: ', list(modulo_reduce)) >>> Modulo: [1, 3, 6, 10, 15, 1, 8, 16, 5, 15]
後者のユースケースは、行列計算かな。
- product - 直積(cartesian product)
直積、デカルト積、cartesian積と呼ばれます。AとBの要素からそれぞれ一つずつ取ってきた作ったペアの集合です。学校で習った記憶があまりありません。。。
single = itertools.product([1, 2], repeat=2) print('Single: ', list(single) >>> Single: [(1, 2), (1, 2), (2, 1), (2, 2)] multiple = itertools.product([1, 2], ['a', 'b']) print('Multi: ', list(multiple)) >>> Multi: [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')
上の例でのrepeatは、自身で積を計算するときの個数を指定します。product(A, repeat=4)はproduct(A, A, A, A)と等価。
- permutations - 順列
it = itertools.permutations([1, 2, 3, 4], 2) print(list(it)) >>> [(1, 2), (1, 3), (1, 4), (2, 1), (2, 3), (2, 4), (3, 1), (3, 2), (3, 4), (4, 1), (4, 2), (4, 3)]
- combinations - 組み合わせ
it = itertools.combinations([1, 2, 3, 4], 2) print(list(it)) >>> [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
- combinations_with_replacement
it = itertools.combinations_with_replacement([1, 2, 3, 4], 2) print(list(it)) >>> [(1, 1), (1, 2), (1, 3), (1, 4), (2, 2), (2, 3), (2, 4), (3, 3), (3, 4), (4, 4)]