この記事でやってること
FizzBuzzを関数、ジェネレータでそれぞれ実装し、メモリ消費量と処理順序を比較する(ことでジェネレータの特長を理解する)。
環境
$ python3 --version
Python 3.8.5
uname -o
GNU/Linux
$ uname -p
x86_64
基本のソースコード
まずは通常の関数とジェネレータでそれぞれ FizzBuzz を実装。
関数
def fizzbuzz(nums):
result = []
for num in nums:
if num % 15 == 0:
result.append('Fizz Buzz')
elif num % 5 == 0:
result.append('Buzz')
elif num % 3 == 0:
result.append('Fizz')
else:
result.append(str(num))
return result
def main():
numbers = [i+1 for i in range(15)] # [1, 2, 3, ..., 15]
for i in fizzbuzz(numbers):
print(i)
if __name__ == '__main__':
main()
ジェネレータ
def fizzbuzz(nums):
for num in nums:
if num % 15 == 0:
yield 'Fizz Buzz'
elif num % 5 == 0:
yield 'Buzz'
elif num % 3 == 0:
yield 'Fizz'
else:
yield str(num)
def main():
numbers = [i+1 for i in range(15)] # [1, 2, 3, ..., 15]
for i in fizzbuzz(numbers):
print(i)
if __name__ == '__main__':
main()
メモリの使用状況を見てみる
ジェネレータの特長としてメモリ消費を抑えられるというのをよく目にするので実際に確認してみる。
なおメモリ使用量の計測には memory_profiler を使用。
$ pip3 --version
pip 20.0.2 from /usr/lib/python3/dist-packages/pip (python 3.8)
$ pip3 show memory_profiler
Name: memory-profiler
Version: 0.58.0
Summary: A module for monitoring memory usage of a python program
Home-page: https://github.com/pythonprofilers/memory_profiler
Author: Fabian Pedregosa
Author-email: f@bianp.net
License: BSD
Location: /home/h-okabe/.local/lib/python3.8/site-packages
Requires: psutil
Required-by:
関数
基本のソースコードに下記の変更を実施。
-
fizzbuzz()
に食わすデータ数を10^7個に設定(マシンスペックに合わせて要調整、桁一つ間違えるとメモリが枯渇する) -
main()
のループ内処理でprint
とかすると邪魔なのでpass
だけ実行しておく -
main()
を呼び出した際のメモリ使用量を0.1秒間隔で計測し、計測回数と最大値を出力
from memory_profiler import memory_usage
def fizzbuzz(nums):
result = []
for num in nums:
if num % 15 == 0:
result.append('Fizz Buzz')
elif num % 5 == 0:
result.append('Buzz')
elif num % 3 == 0:
result.append('Fizz')
else:
result.append(str(num))
return result
def main():
numbers = [i+1 for i in range(10**7)] # [1, 2, 3, ..., 10000000]
for i in fizzbuzz(numbers):
pass
if __name__ == '__main__':
mem_usage = memory_usage((main), interval=.1, timeout=None)
print('monitoring count: {}'.format(len(mem_usage)))
print('max usage: {}'.format(max(mem_usage)))
このプログラムをコマンドラインから実行する際にtime
で呼び出して実行時間も計測する。
5回実行した結果、メモリの使用量はいずれも 800MB ちょっとで収束。
$ time python3 fizzbuzz_func.py
monitoring count: 30
max usage: 812.96484375
real 0m2.806s
user 0m2.469s
sys 0m0.345s
$ time python3 fizzbuzz_func.py
monitoring count: 30
max usage: 811.05859375
real 0m2.880s
user 0m2.607s
sys 0m0.281s
$ time python3 fizzbuzz_func.py
monitoring count: 31
max usage: 812.88671875
real 0m2.953s
user 0m2.672s
sys 0m0.290s
$ time python3 fizzbuzz_func.py
monitoring count: 31
max usage: 812.31640625
real 0m2.941s
user 0m2.540s
sys 0m0.410s
$ time python3 fizzbuzz_func.py
monitoring count: 31
max usage: 811.23828125
real 0m2.975s
user 0m2.642s
sys 0m0.341s
ジェネレータ
こちらも上記の関数と同様に下記の変更を実施。
-
fizzbuzz()
に食わすデータ数を10^7個に設定(マシンスペックに合わせて要調整、桁一つ間違えるとメモリが枯渇する) -
main()
のループ内処理でprint
とかすると邪魔なのでpass
だけ実行しておく -
main()
を呼び出した際のメモリ使用量を0.1秒間隔で計測し、計測回数と最大値を出力
from memory_profiler import memory_usage
def fizzbuzz(nums):
for num in nums:
if num % 15 == 0:
yield 'Fizz Buzz'
elif num % 5 == 0:
yield 'Buzz'
elif num % 3 == 0:
yield 'Fizz'
else:
yield str(num)
def main():
numbers = [i+1 for i in range(10**7)] # [1, 2, 3, ..., 10000000]
for i in fizzbuzz(numbers):
pass
if __name__ == '__main__':
mem_usage = memory_usage((main), interval=.1, timeout=None)
print('monitoring count: {}'.format(len(mem_usage)))
print('max usage: {}'.format(max(mem_usage)))
関数の計測時と同様にtime
で呼び出して5回計測した結果、メモリの使用量はいずれも 400MB ちょっとで収束。
今回のように要素数が多いケースではリスト一つの差が顕著にあらわれた。
$ time python3 fizzbuzz_gen.py
monitoring count: 26
max usage: 404.37109375
real 0m2.416s
user 0m2.286s
sys 0m0.138s
$ time python3 fizzbuzz_gen.py
monitoring count: 26
max usage: 404.8828125
real 0m2.412s
user 0m2.299s
sys 0m0.120s
$ time python3 fizzbuzz_gen.py
monitoring count: 26
max usage: 405.10546875
real 0m2.440s
user 0m2.247s
sys 0m0.200s
$ time python3 fizzbuzz_gen.py
monitoring count: 26
max usage: 405.44140625
real 0m2.418s
user 0m2.215s
sys 0m0.210s
$ time python3 fizzbuzz_gen.py
monitoring count: 26
max usage: 403.8828125
real 0m2.454s
user 0m2.261s
sys 0m0.200s
処理の流れを確認
関数とジェネレータそれぞれの実装において処理が行き来するタイミングを把握する。
関数
-
fizzbuzz()
に食わすデータ数を15個に設定 -
main()
のループ内処理のタイミングとfizzbuzz()
のreturn
のタイミングをprint
で出力
def fizzbuzz(nums):
result = []
for num in nums:
if num % 15 == 0:
result.append('Fizz Buzz')
elif num % 5 == 0:
result.append('Buzz')
elif num % 3 == 0:
result.append('Fizz')
else:
result.append(str(num))
print('return from fizzbuzz')
return result
def main():
numbers = [i+1 for i in range(15)] # [1, 2, 3, ..., 15]
for i in fizzbuzz(numbers):
print(i)
if __name__ == '__main__':
main()
これを実行すると fizzbuzz()
の実行が終わりリストが返ってきた後に main()
のループ処理が行われていることが読み取れる。
$ python3 fizzbuzz_func.py
return from fizzbuzz
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
Fizz Buzz
ジェネレータ
-
fizzbuzz()
に食わすデータ数を 15個に設定 -
main()
のループ内処理のタイミングとfizzbuzz()
のyield
のタイミングをprint
で出力
def fizzbuzz(nums):
for num in nums:
if num % 15 == 0:
print('yield from fizzbuzz')
yield 'Fizz Buzz'
elif num % 5 == 0:
print('yield from fizzbuzz')
yield 'Buzz'
elif num % 3 == 0:
print('yield from fizzbuzz')
yield 'Fizz'
else:
print('yield from fizzbuzz')
yield str(num)
def main():
numbers = [i+1 for i in range(15)] # [1, 2, 3, ..., 15]
for i in fizzbuzz(numbers):
print(i)
if __name__ == '__main__':
main()
ジェネレータの場合には fizzbuzz()
と main()
のループ処理を都度行き来していることが読み取れる。
※ fizzbuzz()
から main()
に戻る契機は yield
だが、再度 fizzbuzz()
に戻る契機となる処理は謎。
(ループ内の処理が全て終わり次のループが開始するタイミングであるとは想像できるがソースなし)
$ python3 fizzbuzz_gen.py
yield from fizzbuzz
1
yield from fizzbuzz
2
yield from fizzbuzz
Fizz
yield from fizzbuzz
4
yield from fizzbuzz
Buzz
yield from fizzbuzz
Fizz
yield from fizzbuzz
7
yield from fizzbuzz
8
yield from fizzbuzz
Fizz
yield from fizzbuzz
Buzz
yield from fizzbuzz
11
yield from fizzbuzz
Fizz
yield from fizzbuzz
13
yield from fizzbuzz
14
yield from fizzbuzz
Fizz Buzz
おまけ
上記のジェネレータを使用したプログラムにおいては、以下のようにリストを使わない形するとメモリ消費を更に抑えられる。
from memory_profiler import memory_usage
def fizzbuzz(nums):
for num in nums:
if num % 15 == 0:
yield 'Fizz Buzz'
elif num % 5 == 0:
yield 'Buzz'
elif num % 3 == 0:
yield 'Fizz'
else:
yield str(num)
def main():
for i in fizzbuzz(range(10**7)): # [1, 2, 3, ..., 10000000]
pass
if __name__ == '__main__':
mem_usage = memory_usage((main), interval=.1, timeout=None)
print('monitoring count: {}'.format(len(mem_usage)))
print('max usage: {}'.format(max(mem_usage)))
実行してみると使用メモリはおよそ 17MB、書き方ひとつの工夫でこうまで結果が違うのはなんともおもしろい。
$ time python3 fizzbuzz_gen.py
monitoring count: 21
max usage: 17.5
real 0m1.886s
user 0m1.875s
sys 0m0.017s
$ time python3 fizzbuzz_gen.py
monitoring count: 21
max usage: 17.46875
real 0m1.925s
user 0m1.913s
sys 0m0.018s
$ time python3 fizzbuzz_gen.py
monitoring count: 21
max usage: 17.6484375
real 0m1.898s
user 0m1.885s
sys 0m0.020s
$ time python3 fizzbuzz_gen.py
monitoring count: 21
max usage: 17.6015625
real 0m1.960s
user 0m1.966s
sys 0m0.000s
$ time python3 fizzbuzz_gen.py
monitoring count: 21
max usage: 17.4296875
real 0m1.909s
user 0m1.916s
sys 0m0.000s