[解決!Python]exec関数やglobals関数を使って変数を動的に定義するには解決!Python

exec関数を使ってモジュールのトップレベルで動的に変数を定義したり、globals関数の返す辞書を使ってグローバル変数として動的に変数を定義したりする方法と、その注意点を紹介する。

» 2024年04月09日 05時00分 公開
[かわさきしんじDeep Insider編集部]
「解決!Python」のインデックス

連載目次

# exec関数を使って、変数を動的に定義
names = ['foo', 'bar', 'baz']
values = [0, 1, 2]

for n, v in zip(names, values):
    exec(f'{n} = {v}')

print(foo)  # 0
print(bar)  # 1
print(baz)  # 2

# 関数ブロックで同様なコードを書いても動かない
def func0():
    names = ['x', 'y', 'z']
    values = [0, 1, 2]

    for n, v in zip(names, values):
        exec(f'{n} = {v}')

    print(x)  # NameError: name 'x' is not defined

func0()

def func1():
    print(locals())  # {}
    exec('x=1'# 変数xを現在の関数スコープで定義
    print(locals())  # {'x': 1}
    # ローカルシンボルテーブルを表す辞書は変更されても
    # ローカルシンボルテーブルが更新されるわけではない
    print(x)  # NameError: name 'x' is not defined

func1()  

# 関数スコープ内で動的に変数を定義するには、それをグローバル変数にする
def func2():
    gl = globals()
    exec('x=1', gl)
    print(x)
    if 'x' in locals():
        print(locals()['x'])
    else:
        print('not found in locals()')
    if 'x' in globals():
        print(globals()['x'])

func2()
# 出力結果:
#1
#not found in locals()
#1
del# 次のコード用に変数xは削除しておく

def func3():
    names = ['x', 'y', 'z']
    values = [0, 1, 2]

    gl = globals()
    for n, v in zip(names, values):
        gl[n] = v

    print(x)  # 0
    print(y)  # 1
    print(z)  # 2

func3()
del x, y, z

# グローバル変数をやめて、辞書に格納することを考える
def func4():
    names = ['x', 'y', 'z']
    values = [0, 1, 2]

    d = {}
    for n, v in zip(names, values):
        d[n] = v

    print(d['x'])  # 0
    print(d['y'])  # 1
    print(d['z'])  # 2


変数を動的に定義するとは

 Pythonでは変数を使うには、それをまず定義する必要がある。

var = 1  # 変数varの定義
print(var)  # 変数varの使用

print(x)  # NameError:未定義の変数を使おうとした


 しかし、何らかの理由でコードの記述時には決定できない名前の変数を定義したいことがある。このようなときには、exec関数を使ってコードの実行時に動的に変数を定義できる。あるいは、globals関数が返す辞書を介して動的に変数を定義できる。以下ではこれを見ていく。

exec関数を使う

 exec関数は第0引数に「Pythonの文を記述した文字列」を渡すと、そのコードを実行する関数だ。以下に例を示す。

stmt = 's = "Python"; print("hello", s)'
exec(stmt)  # hello Python


 この例は変数への文字列の代入(代入文)と関数呼び出し(式文)の2つの文をexec関数に渡している。結果、2つの文が実行されて、画面に「hello Python」と表示される。

 この関数を使うと、次のように動的に変数を定義できる。

names = ['foo', 'bar', 'baz']
values = [0, 1, 2]

for n, v in zip(names, values):
    exec(f'{n} = {v}')

print(foo)  # 0
print(bar)  # 1
print(baz)  # 2


 この例では、変数名を要素とするリストと、その値を要素とするリストを作成して、forループ(とzip関数)でそれらをグルグルッと処理している。その結果、事前に記述された「foo = 0」「bar = 1」「baz = 2」というコードはないが、動的に3つの変数を定義して、それを使えている。

 ただし、これが可能なのはモジュールのトップレベル(グローバル)での話だ。関数ブロックで同じようなことをしようとすると例外が発生する。以下に例を示す(3つの変数は既にグローバルに定義されているので、以下のコードでは変数名を変更している)。

def func0():
    names = ['x', 'y', 'z']
    values = [0, 1, 2]

    for n, v in zip(names, values):
        exec(f'{n} = {v}')

    print(x)  # NameError: name 'x' is not defined

func0()


 exec関数のドキュメントには「オプションの部分が省略されると、コードは現在のスコープ内で実行されます」とある(上のコード例ではまさにオプションを省略している)。そして、関数ブロック内で実行されるコードに関してはローカルシンボルテーブル(ローカル変数を一覧するテーブル)を動的に変更できないからだ。

 locals関数を使うと、「現在のローカルシンボルテーブルを表す辞書」が手に入る。だが、ドキュメントには「この辞書の内容は変更してはいけません; 変更しても、インタプリタが使うローカル変数や自由変数の値には影響しません」と注釈が付いている。そこでlocals関数を使って調べてみよう。

def func1():
    print(locals())  # {}
    exec('x=1'# 変数xを現在の関数スコープで定義
    print(locals())  # {'x': 1}
    # ローカルシンボルテーブルを表す辞書は変更されても
    # ローカルシンボルテーブルが更新されるわけではない
    print(x)  # NameError: name 'x' is not defined

func1()


 これは先ほどのコードを改変して、話をシンプルにするために定義する変数は1つだけとして、同時にlocals関数をexec関数呼び出しの前後に実行して、返送される辞書の内容を確認しているものだ。

 func1関数を呼び出すと、まず現在のローカルシンボルテーブルを表す辞書は空「{}」なことが分かる。そして、「exec('x=1')」呼び出しを実行して、変数xを定義すると、辞書が更新されて「{'x': 1}」となる。にもかかわらず、「print(x)」呼び出し部分でNameError例外が発生する。つまり、locals関数が返す辞書はあくまでもローカルシンボルテーブルの現在の状態を表すものであって、ローカルシンボルテーブルそのものではないということだ。exec関数呼び出しによって辞書は更新されたが、ローカルシンボルテーブルが更新されたわけではない。よって、変数xは未定義となってしまう。

globals関数を使ってグローバル変数として定義する

 ローカルがダメならグローバルを試してみよう。ということで、exec関数の実行コンテキストとしてglobals関数の戻り値を与えてみたのが以下のコードだ。

def func2():
    gl = globals()
    exec('x=1', gl)
    print(x)
    if 'x' in locals():
        print(locals()['x'])
    else:
        print('not found in locals()')
    if 'x' in globals():
        print(globals()['x'])


 このコードでは、globals関数によりグローバルスコープを表す辞書を得て、それをexec関数に渡すようになっている。また、最後にはlocals関数とglobals関数の呼び出し結果から変数xの名前を探して、ローカルスコープとグローバルスコープのどちらに変数xが存在しているかを調べている。実行すると、その結果は次のようになる。

func2()
# 出力結果:
#1
#not found in locals()
#1
del# 次のコード用に変数xは削除しておく


 今度は「print(x)」呼び出しは例外とならず、変数xの値である「1」を表示する。また、locals関数の戻り値から'x'を検索したが、ローカルスコープに変数xがなかったことも分かる。そして、最後のif文では、確かにグローバルスコープにそれが存在していることが確認できた。

 あるいは次のように、globals関数の戻り値である辞書に対して、「gl[変数名] = 値」とすることで、グローバルスコープを表す辞書に変数を挿入できる。以下では'x'、'y'、'z'の3つの文字列とそれらの値を使って、グローバルスコープに変数を定義している。定義した変数はx、y、zのようにそのまま変数名として使えている点に注目しよう。

def func3():
    names = ['x', 'y', 'z']
    values = [0, 1, 2]

    gl = globals()
    for n, v in zip(names, values):
        gl[n] = v

    print(x)  # 0
    print(y)  # 1
    print(z)  # 2

func3()
del x, y, z


 このように、exec関数にグローバルスコープとしてglobals関数の戻り値を渡すことで、あるいは戻り値が辞書であることを使って「gl[変数名] = ……」のようにすることで、関数ブロック内部でも動的に変数を定義できる。しかし、グローバルスコープに変数を無理やり定義するのはよいかという疑問はある。データに名前を与えて変数のように使いたいというだけで、何でもかんでもグローバルにするのは適切だろうか。

 文字列のラベルを使って動的にデータを扱う変数的なものが必要であれば辞書を使うのが最適だろうし、文字列のラベルが必要なければリストを使うのがよいだろう。それらをうまく使えば、適切なスコープで適切なデータを扱えるはずだ。

 例えば、以下は辞書を使った場合のコードだ。上で見た「x」のようなシンプルな表記ではなく、「d['x']」のように少し面倒な表記にはなるが、これならスコープも適切なものにできるだろう。

def func4():
    names = ['x', 'y', 'z']
    values = [0, 1, 2]

    d = {}
    for n, v in zip(names, values):
        d[n] = v

    print(d['x'])  # 0
    print(d['y'])  # 1
    print(d['z'])  # 2


 やりたいこととできること、そのメリットとデメリットを考えた上で妥協点を決めるようにしよう。

「解決!Python」のインデックス

解決!Python

Copyright© Digital Advantage Corp. All Rights Reserved.

スポンサーからのお知らせPR

注目のテーマ

AI for エンジニアリング
「サプライチェーン攻撃」対策
1P情シスのための脆弱性管理/対策の現実解
OSSのサプライチェーン管理、取るべきアクションとは
Microsoft & Windows最前線2024
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。