Python は使いやすいプログラミング言語ですが、実行速度はお世辞にも速いとは言えません。時間がかかる処理をC言語で記述してライブラリを作成し、それをインポートして呼び出す方法もありますが、Python のプログラムを変更なして高速化できるならば、その方が簡単で便利です。今回は Python のプログラムを高速化する簡単な方法を 2 つ紹介します。
Python は Guido van Rossum 氏がC言語で作成した処理系 (CPython) が標準ですが、これ以外にもいくつか処理系があります。その中で JIT コンパイル機能を持つ PyPy は CPython の数倍速いといわれています。
PyPy は上記公式サイトからダウンロードすることができます。また、少し古いバージョンになりますが、Ubuntu 20.04 LTS の場合、次のコマンドでインストールすることができます。
$ sudo apt install pypy3 $ pypy3 --version Python 3.6.9 (7.3.1+dfsg-4, Apr 22 2020, 05:15:29) [PyPy 7.3.1 with GCC 9.3.0]
PyPy はどのくらい速いのか、たらいまわし関数を使って調べてみました。プログラムは次のようになります。
リスト : たらいまわし関数 (tarai.py)
import time
def tarai(x, y, z):
if x <= y: return y
return tarai(tarai(x - 1, y, z), tarai(y - 1, z, x), tarai(z - 1, x, y))
def tak(x, y, z):
if x <= y: return z
return tak(tak(x - 1, y, z), tak(y - 1, z, x), tak(z - 1, x, y))
def test(func, x, y, z):
s = time.time()
print(func(x, y, z))
e = time.time()
print(e - s)
それでは実行結果を示します。実行環境は Ubuntu 20.04 LTS (WSL1), Intel Core i5-6200U 2.30GHz です。
$ python3 Python 3.8.10 (default, Jun 22 2022, 20:18:18) [GCC 9.4.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> from tarai import * >>> test(tarai, 14, 7, 0) 14 56.31682109832764 >>> test(tak, 22, 11, 0) 11 61.39163279533386
$ pypy3 Python 3.6.9 (7.3.1+dfsg-4, Apr 22 2020, 05:15:29) [PyPy 7.3.1 with GCC 9.3.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>>> from tarai import * >>>> test(tarai, 14, 7, 0) 14 17.607698678970337 >>>> test(tak, 22, 11, 0) 11 21.849809169769287
PyPy の実行速度は CPython の約 3 倍になりました。プログラムによってはもっと速くなる場合もあるでしょうし、逆に遅くなる場合もあるかもしれません。たとえば、フィボナッチ関数の実行速度は次のようになりました。
リスト : フィボナッチ関数 (tarai.py に追加)
def fibo(n):
if n < 2:
return n
else:
return fibo(n - 1) + fibo(n - 2)
def testfibo(n):
s = time.time()
print(fibo(n))
e = time.time()
print(e - s)
$ python3 Python 3.8.10 (default, Jun 22 2022, 20:18:18) [GCC 9.4.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> from tarai import * >>> testfibo(38) 39088169 11.766613960266113
$ pypy3 Python 3.6.9 (7.3.1+dfsg-4, Apr 22 2020, 05:15:29) [PyPy 7.3.1 with GCC 9.3.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>>> from tarai import * >>>> testfibo(38) 39088169 1.1087980270385742
最新の PyPy を使うともう少し速くなるかもしれません。興味のある方はいろいろ試してみてください。
PyPy のほかにも、Python のプログラムを JIT コンパイルする方法があります。ライブラリ Numba をインポートすると Python に JIT コンパイラを導入することができます。
Numba のインストールは pip を使うと簡単です。pip は Python で書かれたパッケージ管理ツールです。次のコマンドで pip がインストールされているか確認することができます。
$ python3 -m pip -V /usr/bin/python3: No module named pip
pip が入っていない場合は、次のコマンドでインストールしてください。
sudo apt install python3-pip
あとは pip で Numba をインストールするだけです。
$ pip3 install numba
Numba の基本的な使い方は簡単で、高速化したい関数の前に @jit を付けるだけです。これでその関数が JIT コンパイルされます。それでは、たらいまわし関数で実行時間を計測してみましょう。
リスト : たらいまわし関数
from numba import jit
import time
@jit
def tarai(x, y, z):
if x <= y: return y
return tarai(tarai(x - 1, y, z), tarai(y - 1, z, x), tarai(z - 1, x, y))
@jit
def tak(x, y, z):
if x <= y: return z
return tak(tak(x - 1, y, z), tak(y - 1, z, x), tak(z - 1, x, y))
@jit
def fibo(n):
if n < 2:
return n
else:
return fibo(n - 1) + fibo(n - 2)
def test(func, x, y, z):
s = time.time()
print(func(x, y, z))
e = time.time()
print(e - s)
def testfibo(n):
s = time.time()
print(fibo(n))
e = time.time()
print(e - s)
$ python3 Python 3.8.10 (default, Jun 22 2022, 20:18:18) [GCC 9.4.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> from tarai import * >>> test(tarai, 14, 7, 0) 14 3.2922072410583496 >>> test(tarai, 14, 7, 0) 14 2.9705183506011963 >>> test(tarai, 14, 7, 0) 14 2.9666898250579834 >>> test(tak, 22, 11, 0) 11 3.4130196571350098 >>> test(tak, 22, 11, 0) 11 3.277550220489502 >>> test(tak, 22, 11, 0) 11 3.268494129180908 >>> testfibo(38) 39088169 0.545701265335083 >>> testfibo(38) 39088169 0.4606921672821045 >>> testfibo(38) 39088169 0.46005892753601074 実行環境 : Ubuntu 20.04 LTS (WSL1), Intel Core i5-6200U 2.30GHz
素の Python3 では tak(22, 11, 0) に 61.4 秒、PyPy3 では 21.8 秒かかりますが、Numba を使用することで 3.3 秒に短縮することができました。初回の実行にはちょっと時間がかかりますが、2 回目以降はどの関数も高速に実行することができました。
JIT コンパイラを使ったスクリプト言語では Julia が速いのですが、たらいまわし関数を実行すると約 2 秒ほどかかります。Julia に迫る速度を叩き出すのですから、Numba の JIT コンパイラは優秀ですね。Python のプログラムを高速化するときには試してみたいツールだと思ました。
>>> def foo():
... a = yield "foo"
... print("foo: ", a)
... b = yield "bar"
... print("foo: ", b)
... c = yield "baz"
... print("foo: ", c)
...
>>> g = foo()
>>> print(g.send(None))
foo
>>> print(g.send("foo1"))
foo: foo1
bar
>>> print(g.send("foo2"))
foo: foo2
baz
>>> print(g.send("foo3"))
foo: foo3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> for x in foo(): print(x)
...
foo
foo: None
bar
foo: None
baz
foo: None
リスト : 複数のコルーチンを呼び出す
def make_coroutine(code):
while True:
print(code, end="")
yield None
def test1(n):
a = make_coroutine("h")
b = make_coroutine("e")
c = make_coroutine("y")
d = make_coroutine("!")
e = make_coroutine(" ")
for _ in range(n):
for g in [a, b, c, d, e]: next(g)
for g in [a, b, c, d, e]: g.close()
>>> test1(5) hey! hey! hey! hey! hey! >>> >>> test1(10) hey! hey! hey! hey! hey! hey! hey! hey! hey! hey! >>>
リスト : 子コルーチンから子コルーチンを呼び出す
def make_coroutine2(code, g):
while True:
print(code, end="")
if g: next(g)
yield None
def test2(n):
e = make_coroutine2(" ", None)
d = make_coroutine2("!", e)
c = make_coroutine2("y", d)
b = make_coroutine2("e", c)
a = make_coroutine2("h", b)
for _ in range(n): next(a)
for g in [a, b, c, d, e]: g.close()
>>> test2(5) hey! hey! hey! hey! hey! >>> >>> test2(15) hey! hey! hey! hey! hey! hey! hey! hey! hey! hey! hey! hey! hey! hey! hey!
リスト : エラトステネスの篩
# n から始まる整数列
def integers(n):
while True:
yield n
n += 1
# フィルター
def stream_filter(pred, s):
while True:
x = next(s)
if pred(x): yield x
# n 個の素数を求める
def sieve(n):
# 変数 x の値をクロージャ内に保持する
def make_pred(x):
return lambda y: y % x != 0
#
nums = integers(2)
ps = []
for _ in range(n):
x = next(nums)
ps.append(x)
nums = stream_filter(make_pred(x), nums)
return ps
# 再帰にすると lambda だけでも動作する
def sieve1(n, nums, ps):
if n == 0:
return ps
else:
x = next(nums)
ps.append(x)
return sieve1(n - 1, stream_filter(lambda y: y % x != 0, nums), ps)
>>> sieve(25) [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97] >>> sieve(100) [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541] >>> sieve1(25, integers(2), []) [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97] >>> sieve1(100, integers(2), []) [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541]
リスト : 簡単なマルチプロセス
# 中断中のプロセスを格納するキュー
proc_queue = []
# キューに追加
def enqueue(x):
proc_queue.append(x)
# キューからデータを取り出す
def dequeue():
x = proc_queue[0]
del proc_queue[0]
return x
# キューは空か?
def isempty():
return len(proc_queue) == 0
# プロセスの生成
def fork(fn):
g = fn()
g.send(None)
proc_queue.append(g)
# メインプロセス
def main_process(*args):
# プロセスの登録
for fn in args: fork(fn)
# 実行
while not isempty():
p = dequeue()
if p.send(False):
enqueue(p)
else:
p.close()
# 子プロセス
def mtest0(name, n):
while n > 0:
print(name, n)
yield True
n -= 1
yield False
>>> main_process(lambda : mtest0("foo", 5), lambda : mtest0("bar", 7))
foo 5
bar 7
foo 4
bar 6
foo 3
bar 5
foo 2
bar 4
foo 1
bar 3
bar 2
bar 1
>>> main_process(lambda : mtest0("foo", 5), lambda : mtest0("bar", 7),
lambda : mtest0("oops", 6))
foo 5
bar 7
oops 6
foo 4
bar 6
oops 5
foo 3
bar 5
oops 4
foo 2
bar 4
oops 3
foo 1
bar 3
oops 2
bar 2
oops 1
bar 1
async def name(args, ...): ...
asyncio.run(coro)
await coro
リスト : コルーチンの簡単な例
import asyncio
import time
async def hello():
print('Hello ...')
await asyncio.sleep(1)
print('... World!')
return True
>>> asyncio.run(hello()) Hello ... ... World! True
asyncio.sleep(delay, result = None)
リスト : 複数のコルーチン (1)
async def hello2():
print('Hello2 ...')
await asyncio.sleep(2)
print('... World2')
return 2
async def hello3():
print('Hello3 ...')
await asyncio.sleep(3)
print('... World3')
return 3
async def hello4():
print('Hello4 ...')
await asyncio.sleep(4)
print('... World4')
return 4
async def test00():
s = time.time()
a = await hello4()
b = await hello3()
c = await hello2()
print(a, b, c)
print(time.time() - s)
>>> asyncio.run(test00()) Hello4 ... ... World4 Hello3 ... ... World3 Hello2 ... ... World2 4 3 2 9.057184934616089
async.create_task(coro) => Task object
リスト : 複数のコルーチン (2) async def test1(): t1 = asyncio.create_task(hello4()) t2 = asyncio.create_task(hello3()) t3 = asyncio.create_task(hello2()) s = time.time() a = await t1 b = await t2 c = await t3 print(a, b, c) print(time.time() - s)
>>> asyncio.run(test1()) Hello4 ... Hello3 ... Hello2 ... ... World2 ... World3 ... World4 4 3 2 4.002448081970215
asyncio.gather(coro, ...) => [result, ...]
リスト : 複数のコルーチン (3) async def test2(): s = time.time() result = await asyncio.gather(hello4(), hello3(), hello2()) print(result) print(time.time() - s)
>>> from async_test import * >>> asyncio.run(test2()) Hello4 ... Hello3 ... Hello2 ... ... World2 ... World3 ... World4 [4, 3, 2] 4.019838809967041
リスト : 複数のコルーチン (4)
async def test_sub(name, n):
while n > 0:
print(name, n)
await asyncio.sleep(0)
n -= 1
return False
async def test3():
result = await asyncio.gather(test_sub("foo", 5), test_sub("bar", 3), test_sub("baz", 7))
print(result)
>>> asyncio.run(test3()) foo 5 bar 3 baz 7 foo 4 bar 2 baz 6 foo 3 bar 1 baz 5 foo 2 baz 4 foo 1 baz 3 baz 2 baz 1 [False, False, False]
リスト : キューの使用例
async def send_color(color, n, queue):
while n > 0:
await queue.put(color)
await asyncio.sleep(0)
n -= 1
async def receive_color(n, queue):
while n > 0:
item = await queue.get()
print(item, end=" ")
n -= 1
async def test4():
q = asyncio.Queue(4)
await asyncio.gather(send_color("red", 9, q),
send_color("blue", 7, q),
send_color("yellow", 8, q),
receive_color(24, q))
>>> asyncio.run(test4()) red blue yellow red blue yellow red blue yellow red blue yellow red blue yellow red blue yellow red blue yellow red yellow red
リスト : 哲学者の食事
# フォークの初期化
def init_forks():
global forks
forks = [True for _ in range(5)]
# フォークの番号を求める
def fork_index(person, side):
return person if side == 'right' else (person + 1) % 5
# フォークがあるか
def isFork(person, side):
return forks[fork_index(person, side)]
# フォークを取る
async def get_fork(person, side):
while True:
if isFork(person, side):
forks[fork_index(person, side)] = False
break
await asyncio.sleep(1)
await asyncio.sleep(1)
return True
# フォークを置く
async def put_fork(person, side):
forks[fork_index(person, side)] = True
await asyncio.sleep(1)
return True
# 哲学者の動作 (デッドロック)
async def person0(n):
for _ in range(2):
print(f'Philosopher {n} is thinking')
await get_fork(n, 'right')
await get_fork(n, 'left')
print(f'Philosopher {n} is eating')
await asyncio.sleep(1)
await put_fork(n, 'right')
await put_fork(n, 'left')
print(f'Philosopher {n} is sleeping')
async def test50():
init_forks()
ps = [person0(n) for n in range(5)]
await asyncio.gather(*ps)
>>> asyncio.run(test50()) Philosopher 0 is thinking Philosopher 1 is thinking Philosopher 2 is thinking Philosopher 3 is thinking Philosopher 4 is thinking ^C
リスト : デッドロックの解消
async def person2(n):
for _ in range(2):
print(f'Philosopher {n} is thinking')
if n % 2 == 0:
await get_fork(n, 'right')
await get_fork(n, 'left')
else:
await get_fork(n, 'left')
await get_fork(n, 'right')
print(f'Philosopher {n} is eating')
await asyncio.sleep(1)
await put_fork(n, 'right')
await put_fork(n, 'left')
print(f'Philosopher {n} is sleeping')
async def test52():
init_forks()
ps = [person2(n) for n in range(5)]
await asyncio.gather(*ps)
>>> asyncio.run(test52()) Philosopher 0 is thinking Philosopher 1 is thinking Philosopher 2 is thinking Philosopher 3 is thinking Philosopher 4 is thinking Philosopher 0 is eating Philosopher 3 is eating Philosopher 0 is thinking Philosopher 1 is eating Philosopher 3 is thinking Philosopher 0 is eating Philosopher 1 is thinking Philosopher 2 is eating Philosopher 4 is eating Philosopher 0 is sleeping Philosopher 2 is thinking Philosopher 1 is eating Philosopher 4 is thinking Philosopher 3 is eating Philosopher 1 is sleeping Philosopher 2 is eating Philosopher 3 is sleeping Philosopher 4 is eating Philosopher 2 is sleeping Philosopher 4 is sleeping
async for item in agen:
...
await agen.send(value) => item
リスト : 非同期ジェネレータの簡単な使用例
import asyncio
# 非同期ジェネレータ
async def async_gen():
a = yield "foo"
print(a)
await asyncio.sleep(0.5)
b = yield "bar"
print(b)
await asyncio.sleep(0.5)
c = yield "baz"
print(c)
async def test0():
xs = []
async for x in async_gen():
xs.append(x)
return xs
async def test1():
return [x async for x in async_gen()]
async def test2():
g = async_gen()
try:
a = await g.asend(None)
print(a)
b = await g.asend(1)
print(b)
c = await g.asend(2)
print(c)
d = await g.asend(3)
print(d)
except StopAsyncIteration:
pass
return [a, b, c]
async def test3():
g = async_gen()
try:
a = await g.__anext__()
print(a)
b = await g.__anext__()
print(b)
c = await g.__anext__()
print(c)
d = await g.__anext__()
print(d)
except StopAsyncIteration:
pass
return [a, b, c]
>>> asyncio.run(test0()) None None None ['foo', 'bar', 'baz'] >>> asyncio.run(test1()) None None None ['foo', 'bar', 'baz'] >>> asyncio.run(test2()) foo 1 bar 2 baz 3 ['foo', 'bar', 'baz'] >>> asyncio.run(test3()) foo None bar None baz None ['foo', 'bar', 'baz']