Pythonのサブインタープリターって、execと何が違うんです？

# 記事を書く動機と目的

Python の新しい機能（サブインタープリター）は、「GIL の制約を受けずに並列処理を行うための機能」です。

ただ、速度の話をわきに置くとすれば、「文字列として渡されたスクリプトを実行する」機能でもあります。もともと存在する exec 関数と同じことができるものです。

exec はセキュリティの都合から避けられることが多い関数です。サブインタープリターと exec には、どのような違いがあって、どのようにセキュリティの不都合が変わったのかを、この記事では検証します。

# 記事の結論を先に

- exec とサブインタープリターは、おおまかに見れば同じことができる
- ただ、参照できるデータの範囲、呼び出し元に返せるものが大きく異なる

記事の最後では、実現できそうなサブインタープリターの利用例を載せます。

- Python上にサンドボックスの実行環境を作成する
- 関数の状態を永続化する
- 単体テストを関数単位でサンドボックス化する

# 実行環境

この記事では Python 3.13.2 を利用して検証します。  
※3.12 と 3.13 ではサブインタープリターの関数仕様が大きく違うため、3.13 でないと動きません。

# 検証

## 基本的な書き方を比べてみる

### サブインタープリターの場合

サブインタープリターは、次のようなソースで実行できます。

```python: test-execuse.py
from _interpreters import run_string, create, destroy, new_config

# サブインタープリタの設定を作成
config = new_config()
# サブインタープリタを作成
new_interpreter = create(config)
# サブインタープリタで実行（実行したい関数を渡す）
run_string(new_interpreter, 'print("Hello, Sub-Interpreters")')
# サブインタープリタを終了
destroy(new_interpreter)
```

Python で実行すると、以下のような結果になります。

```bash: コマンドプロンプトで実行
(.venv) D:\2025-03-15>python test-execuse.py
Hello, Sub-Interpreters
```

run_string に実行したい Python スクリプトを渡すことで実行できます。
スクリプトの型は文字列型です。

### exec の場合

では、exec で実行する場合のソースと比べてみます。

```python: test-execuse-exec.py
exec('print("Hello, Exec-Function")')
```

実行すると、以下のような結果になります。

```bash: コマンドプロンプトで実行
(.venv) D:\2025-03-15>python text-execuse-exec.py
Hello, Exec-Function
```

スクリプトの文字列を渡すことで実行できる、という意味で、サブインタープリターの run_string と exec は同じです。

### サブインタープリターで実行元に値を返す

実行元に値を返すこともできます。
サブインタープリターで値を実行元に返すには、次のようなソースを書きます。

```python: サブインタープリターで値を返す
from _interpreters import (
    run_string,
    create,
    destroy,
    new_config,
)
from _interpchannels import (
    create as create_channels,
    destroy as destroy_channels,
    recv,
)

# 実行するスクリプトを定義する
SUBINTERPREP_SCRIPT = """
# sendをインポートする
from _interpchannels import send

# 値を返す
return_value = "ABCDEFG"
send(channel, return_value, blocking=False)
"""

# データの受信チャネルを作成
channel = create_channels(unboundop=1)
# サブインタープリタの設定を作成
config = new_config()
# サブインタープリタを作成
new_interpreter = create(config)
# サブインタープリタで実行
run_string(new_interpreter, SUBINTERPREP_SCRIPT, shared={"channel": channel})
# サブインタープリターから、sendを通して送信された結果を受け取る
print(recv(channel))
# サブインタープリタを終了
destroy(new_interpreter)
# チャネルを終了
destroy_channels(channel)
```

channels を作成、サブインタープリターの中から send 関数を実行して、recv で受け取ります。
このソースの実行結果は次の通りです

```bash
(.venv) D:\2025-03-15>python app.py
('ABCDEFG', None)
```

サブインタープリターの返した"ABCDEFG"が実行元に届いています

## それぞれの実行環境から見えるものを比べる

それぞれの実行環境にはどのような違いがあるでしょうか。
現時点の違いを対照表にすると以下の通りです。

| 分類         | 観点                 | サブインタープリター  | exec 関数          |
| :----------- | :------------------- | :-------------------- | :----------------- |
| 実行環境     | 環境変数             | 実行元と共有          | 実行元と共有       |
| 実行環境     | グローバル変数、関数 | 共有しない            | 実行元と共有       |
| 実行環境     | globals              | 必ず初期化される      | 実行時に連携される |
| 実行環境     | unittest のモック    | 適用されない          | 適用される         |
| 実行環境     | インポートできる OSS | 一部不可（例：numpy） | 制限なし           |
| 引数と戻り値 | 実行元に返せるもの   | 文字列、数値など      | 制限なし           |
| 引数と戻り値 | 実行元から渡せるもの | 文字列、数字など      | 制限なし           |

それぞれについて、細かく検証結果を載せていきます。

### 実行環境の違い

#### 環境変数

setdefault で指定した環境変数が、それぞれの環境でどう見えるのかを比べます。

```python
from os import environ

# 環境変数を設定する
environ.setdefault("ENVIRON_VALUE", "123456789")

# 実行するスクリプトを定義する
SCRIPT = """
from os import environ
print(environ["ENVIRON_VALUE"])
"""
```

環境変数を表示する SCRIPT を、run_string と exec のそれぞれに渡します。
実行結果は以下のようになります

```bash
D:\2025-03-15>python perm.py
123456789 # <-- サブインタープリターの実行結果
123456789 # <-- execの実行結果
```

どちらも呼び出し元の setdeault で指定した環境変数が見えています。
なお、setdefault の代わりに putenv を使うことで、環境変数を上書きすることができます。

#### グローバル変数

次のようなファイルを作ります

```python: variable.py
# グローバル変数を定義する
global_variable = 0


def get_global_variable():
    """ Getterを定義する """
    return global_variable


def set_global_variable(value):
    """ Setterを定義する """
    global global_variable
    global_variable = value
```

次のようなスクリプト文字列を定義します

```python
# グローバル変数を取得するスクリプト
SUBINTERPREP_SCRIPT = """
from variable import get_global_variable
print(f"Global Variable: {get_global_variable()}")
"""
```

get_global_variable を表示するスクリプトを実行する前に、set_global_variable でグローバル変数を書き変えるような処理を挟みます。

```python
set_global_variable(12345622)
...run_stringでサブインタープリターを実行する

set_global_variable(22334455)
...execを実行する
```

実行結果は以下のようになります。

```bash
(.venv) D:\2025-03-15>python perm.py
Global Variable: 0 # <-- サブインタープリターの実行結果
Global Variable: 22334455 # <-- execの実行結果
```

exec は set_global_variable で変更した値が表示されますが、サブインタープリターは set_global_variable が反映されません。

サブインタープリター内で import したライブラリは、外側とは別のライブラリとして扱われます。

**サブインタープリターの import を深堀りする**

ちなみに、変数やインポートしたライブラリはインタープリター単位で管理しているため、インタープリターにキャッシュさせることもできます。

```python
# boto3をインポートするだけの処理
IMPORT_PROC = """
import boto3
"""

# IMPORT_PROCでキャッシュされたboto3を利用する
EXEC_PROC = """
import boto3
print(boto3.__version__)
"""

# サブインタープリタの設定を作成
config = new_config()
# サブインタープリタを作成
new_interpreter = create(config)
# サブインタープリタで実行（インポートだけ実行）
run_string(new_interpreter, IMPORT_PROC)
# サブインタープリターで実行（実際の処理を実行）
run_string(new_interpreter, EXEC_PROC)
# サブインタープリタを終了
destroy(new_interpreter)
```

手元の環境で実行時間を計測すると、IMPORT_PROC の run_string は 288 ミリ秒かかりましたが、EXEC_PROC の run_string は 1 ミリ秒で完了しました。

#### モック化する場合

では、unittest のモックを適用するとどのように見えるでしょうか

```python
SCRIPT = """
from os import getpid
from _interpreters import get_current
# PIDを表示する
print(f"PID:: {getpid()}")
# 実行中のインタープリターを表示する
print(f"Interpreter: {get_current()}")
"""
```

このスクリプトを、以下のようにモックしてから実行してみます。

```python
# getpidの関数をモック化、99999を返すようにする
with patch("os.getpid", MagicMock(return_value=99999)):
    ...実行ソース
```

exec では、モックした値が返りました。

```bash
# execの実行結果
PID:: 99999
Interpreter: (0, 1)
```

サブインタープリターでは、モックを無視した値が返りました

```bash
# サブインタープリターの実行結果
PID:: 22396
Interpreter: (1, 5)
```

もしサブインタープリターのスクリプトを単体テストするとしたら、単体テストでモックが使えないため、run_string の代わりに exec を使って単体テストを実行するか、replace で import 文を書き変えて実行する必要があります。

#### OSS をインポートする

また、サブインタープリターは、一部のライブラリをインポートできません。
たとえば numpy（※現時点の最新は 2.2.3）はインポートできません。

```python
IMPORT_PROCESS = """
import numpy
value = numpy.array([1, 2, 3, 4, 5])
print(value)
"""
```

exec では問題なく実行することができます。

```bash
(.venv) D:\2025-03-15>python execute-numpy.py
[1 2 3 4 5]
```

サブインタープリターでは、このスクリプトを実行するとエラーが返ります

```bash
(.venv) D:\2025-03-15>python execute-numpy.py
CPU dispatcher tracer already initlized
```

ライブラリが対応しているかどうかですので、今後インポートできるライブラリは増えていくだろうと思います。検証をした 2025-03-15 の時点で、boto3 は対応していました。

#### 値や環境変数

グローバル関数とグローバル変数を確認するために、以下の関数をそれぞれの環境で呼び出してみます。

```python
TEXT_PROCESS = "print(globals())"
```

実行結果は以下の通りでした。

**サブインタープリターの場合**

```python: globalsの戻り値
{
    '__name__': '__main__',
    '__doc__': None,
    '__package__': None,
    '__loader__': <class '_frozen_importlib.BuiltinImporter'>,
    '__spec__': None,
    '__annotations__': {},
    '__builtins__': <module 'builtins' (built-in)>
}
```

**exec の場合**

```python: globalsの戻り値
{
    '__name__': '__main__',
    '__doc__': None,
    '__package__': None,
    '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x0000019113419E00>,
    '__spec__': None,
    '__annotations__': {},
    '__builtins__': <module 'builtins' (built-in)>,
    '__file__': 'D:\\2025-03-15\\app.py',
    '__cached__': None,
    'run_string': <built-in function run_string>,
    'create': <built-in function create>,
    'destroy': <built-in function destroy>,
    'new_config': <built-in function new_config>,
    'TEXT_PROCESS': 'print(globals())',
    'call_by_subinterpreter': <function call_by_subinterpreter at 0x00000191135BF920>,
    'call_by_exec': <function call_by_exec at 0x00000191135D45E0>,
    'call_by_eval': <function call_by_eval at 0x000001911363C0E0>
}
```

サブインタープリターは globals の引数が初期化された状態で実行されています。

exec はデフォルトでは呼び出し元の状態を引き継ぎます。実行時の引数に globals を指定することで、データを渡したり、受け取ったりすることもできます。

```python
SCRIPT = """
print(input_value)
exports = 123
"""

# グローバルを新規作成する
gbl = globals()
# グローバルに新しい変数を定義する
gbl["input_value"] = 999
# スクリプトを実行する
exec(SCRIPT, gbl) # <- 実行時に999が出力される
# グローバルに書き込まれた変数を取り出す
value = gbl["exports"] # <- 123が取得できる
```

サブインタープリターは引数に globals を取らないため、このような globals を通したデータのやり取りはできません。

### 戻り値や引数の違い

戻り値の違いを説明する前に、スクリプト内でできることを説明します。exec もサブインタープリターも、スクリプトの中で関数やクラスを作ること、実行元のクラスのサブクラスを動的に作ることができます。

実行元のソースにベースクラスを作成します

```python: base_class.py
class BaseClass:
    """ベースクラス"""
    def abs_method(self):
        """継承させるメソッド"""
        return "my name is base class"

    def method_call(self):
        """abs_methodを実行するメソッド"""
        print(f"CustomClass {self.abs_method()}")
```

このクラスを、スクリプト文字列の中で継承します。

```python: スクリプトの中で動的に継承する
SCRIPTS = """
from base_class import BaseClass

class CustomClass(BaseClass):
    def abs_method(self):
        return "dynamic method"

print(CustomClass().method_call())
"""
```

実行結果は以下の通りです。
ここまではサブインタープリター、exec、どちらで実行しても同じ結果になります。

**サブインタープリター、exec の実行結果**

```bash
python app.py
CustomClass dynamic method
```

#### 戻り値の違い

では、動的に継承したクラスを呼び出し元に返します。

**exec で実行する**

exports でクラス定義を返す、JavaScript のモジュールみたいなソースを書きます。

```python: スクリプトの中で動的に継承する
SCRIPTS = """
from base_class import BaseClass

class CustomClass(BaseClass):
    def abs_method(self):
        return "dynamic method"

exports = CustomClass
"""
```

このクラス定義のスクリプト文字列を、exec で実行します。

```python: 動的継承クラスを受け取る
from base_class import BaseClass

# グローバル変数を確保する
gbl = globals()
# exec関数で実行する
exec(SCRIPTS, gbl)
# グローバル変数から、動的に継承したメソッドを取得する
CustomClass: BaseClass = gbl["exports"]
# 動的定義したクラスを呼び出す
print(CustomClass().method_call())
```

実行結果は以下の通りです。

```bash
(.venv) D:\2025-03-15>python app.py
CustomClass dynamic method
```

文字列内で定義したクラスを受け取って、インスタンス化することができています。
exec は、定義したクラスやインスタンスそのものを実行元に返すことができます。

つまり、こういう処理も返すことができます。

```python: スクリプトの中でstrに擬態したオブジェクトを作る
SCRIPTS = """
class EvilStr(str):
    # strを継承したクラス

    _evil_script = "DROP TABLE db;"
    _counts = 0

    def __str__(self):
        # 参照された回数を記録
        self._counts += 1
        if self._counts > 1:
            # 2回目の参照は悪意のあるSQLを返す
            return self._evil_script

        # 1回目の参照は普通のSQLを返す
        return "SELECT * FROM db;"

exports = EvilStr()
"""
```

このスクリプトを実行します。
ただ実行するわけではなく、実行結果が str 型かどうかを検証してから実行します。

```python: 実行する
gbl = globals()
exec(SCRIPTS, gbl)

# スクリプトを実行してSQLを取得する
input_sql = gbl["exports"]

# 実行結果が文字列であれば処理をする
if isinstance(input_sql, str):
    print(input_sql) # <- ログに出す
    print(input_sql) # <- 実行する
else:
    print("受け取った値は str ではありません。")
```

実行結果は次のようになります。

```bash
(.venv) D:\2025-03-15>python app.py
SELECT * FROM db; # <- 1回目のprint(input_sql)の出力結果
DROP TABLE db; # <- 2回目のprint(input_sql)の出力結果
```

実行元の str 型チェックはすり抜けます。

それだけではなく、1 回目の参照で無害な SQL としてふるまった後、2 回目の参照で悪意のある SQL にすり替わっています。

**サブインタープリターの場合**

では、サブインタープリターの場合はどうでしょうか。
send 関数に、exec の時のようにサブクラスを指定してみます

```python: 実行するスクリプト文字列を変更
SUBINTERPREP_SCRIPT = """
from _interpchannels import send
from base_class import BaseClass

class CustomClass(BaseClass):
    def abs_method(self):
        return "dynamic method"

send(channel, CustomClass, blocking=False)
"""
```

実行結果すると、エラーになります。

```bash
(.venv) D:\2025-03-15>python app.py
Traceback (most recent call last):
  File "D:\2025-03-15\app.py", line 82, in <module>
    call_by_subinterpreter()
    ~~~~~~~~~~~~~~~~~~~~~~^^
  File "D:\2025-03-15\app.py", line 60, in call_by_subinterpreter
    print(recv(channel))
          ~~~~^^^^^^^^^
_interpchannels.ChannelEmptyError: channel 0 is empty
```

send で受け渡すことができるのは、共有可能オブジェクトに限定されます。  
https://peps.python.org/pep-0734/#shareable-objects

共有可能オブジェクトは以下の通りです。

| 型                 | 備考         |
| :----------------- | :----------- |
| str                | 文字列型     |
| bytes              | バイト型     |
| int                | 整数         |
| float              | 小数         |
| bool               | 真偽値       |
| tuple              | タプル       |
| interpreters.Queue | キュー       |
| memoryview         | メモリビュー |

exec のように、クラスそのもの動的に定義して実行元に返すようなことはできません。
数値や文字列を直接送信するか、json や pickle を通してデータをやり取りする形になります。

## 利用例

では、サブインタープリターの並列処理以外の利用例を考えてみます。

### 利用例 1: 実行中の権限を絞ってみる

サブインタープリターを使って、サンドボックスを作成します。
以下のようなファイル「assume_role.py」を作成します。

```python: assume_role.py
import os
import boto3

client = boto3.client("sts")


class AssumeRole:
    """
    Withの間だけ、AssumeRoleを実行して、putenvで環境変数を書き変える
    """

    _original: dict
    _assume_role: dict

    def __init__(self, role_arn: str, role_session_name: str):
        response = client.assume_role(
            RoleArn=role_arn,
            RoleSessionName=role_session_name,
        )

        self._assume_role = {
            "AWS_ACCESS_KEY_ID": response["Credentials"]["AccessKeyId"],
            "AWS_SECRET_ACCESS_KEY": response["Credentials"]["SecretAccessKey"],
            "AWS_SESSION_TOKEN": response["Credentials"]["SessionToken"],
        }

    def __enter__(self):
        self._original = {
            "AWS_ACCESS_KEY_ID": os.environ.get("AWS_ACCESS_KEY_ID", ""),
            "AWS_SECRET_ACCESS_KEY": os.environ.get("AWS_SECRET_ACCESS_KEY", ""),
            "AWS_SESSION_TOKEN": os.environ.get("AWS_SESSION_TOKEN", ""),
        }
        os.putenv("AWS_ACCESS_KEY_ID", self._assume_role["AWS_ACCESS_KEY_ID"])
        os.putenv("AWS_SECRET_ACCESS_KEY", self._assume_role["AWS_SECRET_ACCESS_KEY"])
        os.putenv("AWS_SESSION_TOKEN", self._assume_role["AWS_SESSION_TOKEN"])
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        os.putenv("AWS_ACCESS_KEY_ID", self._original["AWS_ACCESS_KEY_ID"])
        os.putenv("AWS_SECRET_ACCESS_KEY", self._original["AWS_SECRET_ACCESS_KEY"])
        os.putenv("AWS_SESSION_TOKEN", self._original["AWS_SESSION_TOKEN"])
        return False
```

putenv で環境変数を書き変えるクラスです。
次のソースコードで、サブインタープリターを実行します。

```python
from assume_role import AssumeRole

# boto3の実行環境を表示する
TEXT_PROCESS = """
import boto3
print(boto3.client("sts").get_caller_identity())
"""

# 一時的にAWSの権限を切り替えて、環境変数に反映する
with AssumeRole(
    role_arn="arn:aws:iam::xxxxxxxxxxx:role/AssumeRoleUserRole",
    role_session_name="session_name",
):
    # サブインタープリタの設定を作成
    config = new_config()
    # サブインタープリタを作成
    new_interpreter = create(config)
    # サブインタープリタで実行
    run_string(new_interpreter, TEXT_PROCESS)
    # サブインタープリタを終了
    destroy(new_interpreter)
```

実行結果は以下の通りです。
元々の実行環境の権限ではなく、AssumeRole された権限になっています。

```bash: サブインタープリター内のAWSの権限
{
    'UserId': 'XXXXXXXXXXXXXXXXXXX:session_name',
    'Account': 'xxxxxxxxxxxx',
    'Arn': 'arn:aws:sts::xxxxxxxxxxxxxxx:assumed-role/AssumeRoleUserRole/session_name',
    'ResponseMetadata': {
        'RequestId': '66a200ed-dee9-4518-bbfc-116fc468b4f6',
        'HTTPStatusCode': 200,
        'HTTPHeaders': {
            'x-amzn-requestid': '66a200ed-dee9-4518-bbfc-116fc468b4f6',
            'content-type': 'text/xml',
            'content-length': '449',
            'date': 'Sat, 15 Mar 2025 14:32:16 GMT'
        },
        'RetryAttempts': 0
    }
}
```

ちなみに、exec で実行した場合は AssumeRole した権限になりません。

```python
from assume_role import AssumeRole

TEXT_PROCESS = """
import boto3
print(boto3.client("sts").get_caller_identity())
"""

with AssumeRole(
    role_arn="arn:aws:iam::xxxxxxxxxxx:role/AssumeRoleUserRole",
    role_session_name="session_name",
):
    exec(TEXT_PROCESS)
```

実行結果は以下のようになります。

```bash
{
    'UserId': 'xxxxxxxxxxxxxx',
    'Account': 'xxxxxxxxxxxxx',
    'Arn': 'arn:aws:iam::xxxxxxxxxxxxx:user/AdminUser',
    ...以下略
```

boto3 が環境変数を読み込むのは、最初に boto3 で client を作ったタイミングだからです。

仮にもし上手く環境変数から取れたとしても、exec では環境変数を無視して権限を取ることができます。以下のように書くことで、AssumeRole するために使った boto3 のインスタンス自体にアクセスできます。

```python
TEXT_PROCESS = """
from assume_role import client
print(client.get_caller_identity())
"""
```

### 利用例 2: 関数や実行環境をハイバネートする

サブインタープリターは、インタープリターごとに独立して変数の状態を持っています。保持している変数は globals で参照することが可能で、変数の状態は、`set___main___attrs`関数を使うことで変更することができます。

例としては、このようなソースコードになります。

```python: サスペンドするソースコード
from _interpreters import run_string, create, destroy, new_config, set___main___attrs
import pickle

# サブインタープリターの処理
EXECUTE_PROCESS = """
value += 1
print(value)
"""

# インタープリターの状態を永続化するスクリプト
SUSPENDS = """
import json
import pickle
from _interpreters import is_shareable

def defaults(obj):
    return None

with open("suspend.pkl", mode="wb") as fp:
    fp.write(pickle.dumps({
        k: v
        for k, v in json.loads(json.dumps(globals(), default=defaults)).items()
        if v is not None and is_shareable(v)
    }))
"""

# サブインタープリタの設定を作成
config = new_config()
# サブインタープリタを作成
new_interpreter = create(config)
# サブインタープリタで実行
with open("suspend.pkl", mode="rb") as fp:
    # 永続化した状態をサブインタープリターに適用する
    set___main___attrs(new_interpreter, pickle.loads(fp.read()))
# 処理を実行する
run_string(new_interpreter, EXECUTE_PROCESS)
# インタープリターの状態を永続化する
run_string(new_interpreter, SUSPENDS)

# サブインタープリタを終了
destroy(new_interpreter)
```

この処理を実行すると、以下のようになります。

```bash
# 実行すると、数字が表示される
(.venv) D:\2025-03-15>python pickle-proc.py
128

# さらに実行すると、実行するたびに数が増える
(.venv) D:\2025-03-15>python pickle-proc.py
129
```

関数や実行環境そのものを中断した状態で保存して、続きから再実行することができます。

### 利用例 3: 単体テストに利用する

ライブラリの読み込みが独立しているため、他のテストケースに干渉させず、キャッシュやインポート状態に残らない単体テストを実行できます。

例としては、このようなソースコードになります。

```python
from _interpreters import run_string, create, destroy, new_config
from _interpchannels import create as create_channels, destroy as destroy_channels, recv
from string import Template
from os import environ
from pathlib import Path
from textwrap import indent

# 対象の関数にパスを通すテンプレート
TEST_TEMPLATE = """
import os
from sys import path
from pathlib import Path
from _interpchannels import send
# パスを適用する
path.append(os.environ.get("PYTHONPATH"))
# テストを実行する
try:
    # テストソースのインデントはtextwrap.indentで設定する
${test_code}
    # テストの結果を返す
    send(channel, True, blocking=False)
except Exception as e:
    # テストの結果を返す
    send(channel, False, blocking=False)
"""

# テスト対象のコードのルートディレクトリを指定する
# サンプルとしてsrcディレクトリを指定
environ.setdefault("PYTHONPATH", str(Path(__file__).parent / "src"))


# テストの実行ソース
def test_code():
    # テストを定義
    TEST_PROCESS = """
from os import environ
from test_target import lambda_handler

res = lambda_handler({}, {})
assert res["statusCode"] == 200
"""

    # データの受信チャネルを作成
    channel = create_channels(unboundop=1)
    # サブインタープリタの設定を作成
    config = new_config()
    # サブインタープリタを作成
    new_interpreter = create(config)
    # サブインタープリタで実行
    run_string(
        new_interpreter,
        Template(TEST_TEMPLATE).substitute(
            test_code=indent(TEST_PROCESS, "    "),
        ),
        shared={"channel": channel},
    )
    # sendで送信された結果を受け取る
    assert recv(channel)[0]
    # サブインタープリタを終了
    destroy(new_interpreter)
    # チャネルを終了
    destroy_channels(channel)
```

サブインタープリターの仕様がまだ不安定なため、すぐに利用できるようにはならないと思います。

ただ、関数単位でモックやグローバル変数の状態を分離できるようになり、サブインタープリターを再生成するだけで初期状態に戻すことができるため、テストは書きやすくなるだろうと思います。

---

# まとめと結論

サブインタープリターによるソースの実行は、exec に比べて安全です。

「何も考えずに未検証のコードを実行することは安全なのか」について、よく上がる例として、execで実行できる次のようなコードがあります。

```python
SUBINTERPRETER_SCRIPT = """
import os
# ファイルを削除する
os.remove(削除されたくないファイル名)

# 全ての環境変数を出力する
print(os.environ)
"""
```

サブインタープリターのスクリプトを実行すれば、ファイルを削除すること、環境変数を取ること、サーバにリクエストを投げることなど、様々なことができます。

ただ、最近のサーバーレスの環境なら、実行環境に渡される権限が限定されていますし、実行環境のファイルシステム内に流出させたくないファイルが置いてあるわけでもありません。

たとえば、AWS の Lambda では、/tmp ディレクトリ以外への書き込みができませんし、NAT のないプライベートサブネットで実行するのなら、実行環境からインターネットに接続できません。その環境で「os.remove が実行できること」は、無条件で利用を避けるほどの致命的な問題になるとは思いません。

もちろん、exec のように、受け取った結果が別のオブジェクトに偽装されたり、動いているインスタンス変数を直接参照されるのは大きな問題があります。その穴はサブインタープリターが塞いでいます。適切に実装さえすれば、意図しないデータが共有されることもありません。

サブインタープリターを適切な環境で適切に使うのなら、execに比べてもう少し踏み込んだ使い方ができるのではないかと思います。