問題点:
- エラー内容: 予約語 await は async で定義された関数内でしか使えません。※注1
これは、exec(code) が呼ばれた async def on_message
の事ではなく、
exec に渡される code 内のコードの事を指します。
解決策: 単一の「式」で良いなら、await は外側に出して、
await に渡す式のみを評価対象にしましょう。
Pythonにおける式(expression)と文(statement)の違いが判らない場合、
eval と exec の違いを調べて下さい。戻り値が欲しい場合は、eval を用います。
python
1@client.event
2async def on_message(message):
3 if message.content.startswith("/eval await "):
4 src = message.content.split(" ", 2)[-1].lstrip()
5 await eval(src)
6 elif message.content.startswith("/eval"):
7 ...
※ 注1
コルーチンの関数内でなくても使えるようにしようという動きはあって、
Python 3.8 では、コンパイル時のフラグによりこれを可能にできます。
exec/eval ではこのオプションを指定できないので、
compile() 関数を使って コードオブジェクトを生成します。
python
1import ast
2import functools
3
4async_compile = functools.partial(compile,
5 mode="exec",
6 filename="<discord>",
7 flags=ast.PyCF_ALLOW_TOP_LEVEL_AWAIT)
8
9
10@client.event
11async def on_message(message, _eval_prefix="/eval "):
12 if message.content.startswith(_eval_prefix):
13 src = message.content[len(_eval_prefix):].lstrip()
14 code = async_compile(src)
15 await eval(code)
※ 実際に discord.py ではテストしてません。
ひとつ問題があって、await eval(code) ではコルーチンを期待するので、
awaitを含むコード(コルーチン)と、それ以外の普通のコードを区別する必要があります。
print("test")
等は None を返すので、await None
ではエラーになります。
これの解決策は下のコードにて。
動作確認に使ったコード (win10/Python3.8で動作確認)
#!/usr/bin/env python3.8
import asyncio
import ast
from functools import partial
async_compile = partial(compile,
filename="<stdin>",
mode="exec",
flags=ast.PyCF_ALLOW_TOP_LEVEL_AWAIT)
def async_eval(src, variables=None):
if not variables:
variables = {}
# XXX: コルーチンにするには、最低限一つの await を含む必要が有る為
# コンパイル対象のコードの末尾に await asyncio.sleep(0) を追加してます。
# ここは、他により良い解決策があるかもしれません。
return eval(async_compile("\n".join([src, "await asyncio.sleep(0)"])), globals(), variables)
async def main():
from textwrap import dedent
A = 10
B = 20
# ローカル変数を渡す例。see also eval/exec の第二第三引数
await async_eval('print("A + B = ", A + B)', locals())
# awaitを含む式のテスト。
await async_eval("await asyncio.sleep(1)")
# 複数の await を含むコードのテスト。
# dedentはコードを読みやすくするためのもので、
# 各行頭のインデントを削除した文字列がasync_evalへ渡されます。
await async_eval(dedent("""
print(1)
await asyncio.sleep(1)
print(2)
await asyncio.sleep(1)
print(3)
"""))
await asyncio.sleep(1)
print("END")
if __name__ == '__main__':
asyncio.run(main())