Help us understand the problem. What is going on with this article?

Python eval/execによる動的な変数定義と返り値

0. はじめに

本項は以下について述べます。

  • Pythonにおける動的な変数定義
  • 動的に定義した変数のreturnの取り方

念のため当方環境。

>>> python --version
Python 3.7.3

[前提知識]

組み込み関数に eval exec があります。
eval execはいずれも第一引数に文字列、第二引数にグローバル名前空間、第三引数にローカル名前空間をそれぞれとります。
文字列をわたすと、evalはそれを式として評価、execは文として実行してくれます。

eval('type("viscera")')
# >> str

exec('x = "squirm"; print(x)')
# >> squirm

基本的なことは以下を参照。
- Python の eval と exec - Qiita @kyoshidajp
- 組み込み関数 — Python 3.7.4 ドキュメント

1. 動的な変数定義

本節では、任意の変数に格納されている文字列を変数名として新たに変数を定義する、動的な変数定義のやり方について述べます。

挙動についての留意点を列挙します。

まずはグローバル変数を定義する場合です。
この場合は思考停止で問題ありません。

def sample_a():
    exec('a=3', globals())
    print(a)

sample_a()
print(a)
# >> 3
# >> 3

値の変更も問題なくできます。

b = 1
def sample_b():
    exec('b=3', globals())
    print(b)

sample_b()
print(b)
# >> 3
# >> 3

が、ローカル変数を定義する場合には注意が必要です。

def sample_c():
    exec('c=3', {}, locals())
    print(c)

sample_c()
# >> NameError: name 'c' is not defined

第三引数にlocals()をわたしても、関数 sample_c 内のローカル変数を定義することはできません。従って以下のように閉じた名前空間を新たに定義する必要があります。

def sample_d():
    D = {} # インスタントな名前空間
    exec('d=3', {}, D)
    exec('print(d)', {}, D)

sample_d()
# >> 3

この挙動の詳細については次記事を参照。
Pythonのlocals() - Qiita @amedama

以上を踏まえ、動的な変数定義が以下のようにして可能です。

def sample_e():
    e = "edifice"
    E = {}
    exec(e + '= "a building, especially a large one"', {}, E)
    exec('print(edifice)', {}, E)

sample_e()
# >> a building, especially a large one

2. return の取り方

動的な変数を定義する場合、コーディング時に変数名は未知です。
よってこの変数から返り値を取るには、

  1. 新たに変数を定義する。
  2. 定義された変数の変数名を取得し、文字列として格納する。
  3. 2を参照し、文字列を変数名として認識させてreturnさせる。

以上3工程が要求されます。

実装にあたり、returneval で変数名を与えればよさそうだと直感的にわかりますが、新たに定義された変数名を取得するには一工夫が要りそうです。

今回は、新たに変数が定義されると、辞書である名前空間に key が追加されることを利用します。次のようにして辞書の差分をとります。

from copy import deepcopy
f = "fertile"
FG = {}
checkpointBefore = deepcopy(FG)
exec(f + '= "able to produce good crops"', {}, FG)
checkpointAfter  = deepcopy(FG)
detector = list(filter(lambda x: x not in checkpointBefore.keys(), checkpointAfter.keys()))

例によって exec 文で変数を定義しますが、その前後の名前空間を checkpoint として deepcopy をとり、比較して新たに増えたkey値をリスト detector に格納します。
これを用いて、returnを取ってみます。

from copy import deepcopy
def sample_fg():
    f = "fertile"
    g = "germ"
    FG = {}
    checkpointBefore = deepcopy(FG)
    exec(f + '= "able to produce good crops"', {}, FG)
    exec(g + '= "a very small living thing that can make you ill"', {}, FG)
    checkpointAfter  = deepcopy(FG)
    detector = list(filter(lambda x: x not in checkpointBefore.keys(), checkpointAfter.keys()))
    return eval(detector[0], {}, FG), eval(detector[1], {}, FG)

print(sample_fg())
# >> ('able to produce good crops', 'a very small living thing that can make you ill')

ここではローカルの名前空間を例にしましたが、checkpointglobals() をわたすことでグローバルの名前空間についても同様に扱うことができます。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away