Python関連小ネタ
これは、 Python Advent Calendar 2021 https://qiita.com/advent-calendar/2021/python の16日目の記事です
どうやら、Pythonを使った解析とかPythonを使って作ったアプリとかの投稿が多いようであるが、PythonのAdvent calendarということで、純粋にpythonについて学んだ(そうなるとは思っていたが、まさか本当にそうなるとは思った)ことを共有しておこうと思う。
Global変数をmodule間やmainとmoduleの間で共有する
あまり意識したことがない人も多いかもしれないが、実はPythonではimportしたmodule側とmain側ではglobal変数の名前空間が異なる。こうなっていないと、main側で、module側にある変数と同じ名前を変数を作ってしまった時に大惨事になってしまうので、当たり前といえば当たり前の仕様である。
一方で、「module側のglobal変数をmain側から参照することはできても、main側のglobal変数をmodule側から参照することができない!!」といった問題に直面することがある。 「え、そんな問題に直面することあるか?」と思われるかもしれないが、少なくとも筆者にはあったのだ。そして、優しいpython公式さんはちゃんと解決策を提示してくれていた。
ということで、module間やmodule側からmainの変数を参照したければ、公式ドキュメントに書いてあるようにお互いに共有する変数を保持するための共通のmoduleファイルを別に用意してあげればいい。**ドキュメントは素晴らしい!。みんなQiitaじゃなくて公式ドキュメントをちゃんと読もう!**となってしまうと、Qiitaの意味がなくなってしまうので、この記事では、この問題に対する別解(多分、あんまり好まれない)を紹介したい。
さて、先の公式ドキュメントを読んだときに、筆者はあることを思った。
「なるほどぉ。でも、ちょっと面倒だなぁ。うーーーーーーん。(N時間経過)。そうだ!こんな面倒なことしなくてもmain側のglobal変数の名前空間を直接渡せるんじゃね?」
さて、どうゆうことか。そもそも、pythonにはdefaultでglobals()という関数が用意されている。これはglobal空間にある、変数やメソッドを返してくれる関数だ。例えば、インタプリタでもNotebookでもなんでもいいんだが、
a=1
print(globals())
と実行すると、
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'a': 1}
みたいになる。「それぐらいは知ってるわ」という人も多いと思うが、実はこんなこともできる。
hoge = globals()
hoge["b"] = 1
print(b)
「うん?、何やってんの?bはhogeにkeyとして加えただけで変数としては未定義でしょ?」、って思われるかもしれないが、結果は
1
となる。そう、globals()の返り値である辞書に新しいkeyを追加すると、global変数として追加されるのだ。。(この仕様、いつか変わっちゃうんだろうなぁと思っていたのだが、どうやらpython2のときから同じ仕様になっているようなので、多分変わることはないと思いたい。)
ってことは、このglobalsの返り値はモジュール側の変数に代入したり、関数の引数にしたって、main側のglobal空間にアクセスできる辞書として機能するはずである。
実際にやってみた。
まず、importする用のファイルとしてhoge.pyを作成。
namespace_main = None
そして、hoge.pyのnamespace_main
に対して、main側のglobals()
をエイっと代入。main側でグローバル変数をfuga
から直接編集してみる。
import hoge
hoge.namespace_main = globals()
fuga = 100
#mainのglobal変数hogeにmy_funcからアクセス
print(hoge.namespace_main["fuga"])
hoge.namespace_main["fuga"]=200
print(fuga)
で、できてしまった。。。筆者が書くのもなんだが、実に気持ちが悪い動作である。。。
で何に使えるの?
さて、「こういう気持ちが悪い動作ができるのはわかったけど、これ何に使えるの?」と思われたかもしれない。
正直、私にもあまり使い道はわからない。しかし、例えば、こんなことができてしまう。
def add(x, y, z, namespace):
namespace[z]=namespace[x]+namespace[y]
print("グローバル変数{}とグローバル変数{}の和は{}です。".format(namespace[x],namespace[y],namespace[z]))
print("計算結果はグローバル変数{}に代入されました".format(namespace[z]))
>>> import hoge
>>> a = 10
>>> b = 20
>>> hoge.add("a", "b", "c", globals())
グローバル変数aとグローバル変数bの和は30です。
計算結果はグローバル変数cに代入されました。
>>> c
30
何ができたか、お分かりいただけただろうか。なんとmodule側から、main側のどのglobal変数が操作されたかと、どのglobal変数に代入が起こったのかを追跡できているのだ。 「それって、そんなにすごいか?」ってなりそうだが、これを他の方法で実現しようとすると意外と難しい、、、?気がする。
とにかく、上記の手法を応用すればmain側での書き方を工夫する必要は出てくるものの、mainでの動作をユーザが定義した変数名も含めて追跡、記録することもできるはずである。 しかし、自分で作っておいてなんだが、どこにもc =
書いていないのc
が勝手に作られて、a+b
の計算結果まで代入されているのは非常に気持ちがわるいなぁ。。。
というように、Pythonを使い始めて結構時間が経つのですが、まだまだ知らないことがたくさんあるなぁ、と思わされた1年でありました。