はじめに
本記事は、Pythonにおける変数、特にグローバル変数による変数の汚染との戦いへ終止符を打つ、完結編となっています。ちなみに、前回までの話は特にありません。一話完結型です。
なお、最終的なコードや本記事で実験に使用したコードらは、Githubに上げています。
今回の目的は、noglobal
を完成させること。それだけです。では参りましょう。
謝辞
本編に入る前にまず、今回の執筆に至るまでの経緯と関連する方々への謝辞を。
最初に問題提起をしnoglobalの存在を知るきっかけになった@fkubotaさん、fkubotaへ回答を投げた@nyker_gotoさん、さらに回答を広げてくれたまますさん、そして最終的なnoglobalが完成するきっかけをくれたもみじあめさんには感謝します。また、ベースとなるコードを作成してくださったax3lさん、raven38さん、yoshiponさんにも御礼申し上げます。とても勉強になりました。
最終的に完成したコードはGithubに上げていますが、可能ならPyPIに、pip経由でinstallできるようにしておいてもいいのではないかと思います。しかしそれはまた別のお話なので、別の機会に。
最終的に採択されたコード
以下のコードを採用しました。コメント等は省略しています。詳細は、後述の本文をみるか、Github/noglobal.pyを参照してください。
以下のコードを適当に、noglobal.py
というファイルにでも保存して、それをfrom noglobal import noglobal
とするだけで使用できます。
このnoglobalは、以下のような挙動をします。
- 任意の関数にデコレータとして
@noglobal()
とつけることで、その関数内からはグローバル変数を参照できないようにする - グローバルな関数については参照可能(従来通り関数を使用できる)
- builtins関数(つまり、
print
とかlen
とか)も使用可能 @noglobal(excepts=["hoge"])
に渡すことで、使用したいグローバル変数を指定して使用することが可能
なお、このnoglobalは、本記事中ではnoglobal::ver05
という位置付けであり、noglobal::ver04_momijiameに数行付け足したものです。
各versionについては後述しますが、各versionのオリジナルへのリンクを掲載しておきます。
- noglobal::ver01_ax3l
- noglobal::ver02_raven38
- noglobal::ver03_yoshipon
- noglobal::ver03-2_K-PTL
- noglobal::ver04_momijiame
- noglobal::ver05_K-PTL
- noglobal::ver05-2_K-PTL はスクリプト自体はver05と同じ、使用方法(importからの定義)が少し違う
noglobal::ver05-2は、ver5よりもimportが楽になるというメリットがあります。個人的には、ver05-2があれば十分です。特別理由がなければ、ver05-2を使用しましょう。
ただ、今回アップデートしたver05とver05-2は、あくまでver04をベースにしているため、ver04とは完全に互換性があります。ver04で使用されている関数名を変更していないので、そのままnoglobal.pyを置き換えるだけでver05に更新することができます。また、ver03以前のものについては、そもそもver04以降はデコレータとして使用する際に()
が必要になったので、そこの改変が必要です。そればかりは、仕方ない。そこまで面倒みる力はありません。
ちなみに、今回紹介したいnoglobal::ver05では、従来のnoglobalと比較して以下の点が改善されています。
- 他のファイルで宣言されたnoglobalをimportで呼び出して使用できるように (これがver04での改善点)
- それを拡張し、importを経由して繋がる全ての他のファイル内でもnoglobalが使用可能に (ver05での改善点)
- noglobalを使用するための定義が、import文だけで完結するように (ver05-2に限る)
注意事項も記載しましたので、使用例までぜひみていってください。
最新のnoglobal.pyのコード
from functools import partial import inspect from typing import List, Dict, Optional, Callable, Any from types import FunctionType def globals_with_module_and_callable(globals_: Optional[Dict[str, Any]] = None, excepts: Optional[List[str]] = None) -> Dict[str, Any]: def need(name, attr): if name in excepts: return True if inspect.ismodule(attr): return True if callable(attr): return True return False if globals_ is None: globals_ = globals() if excepts is None: excepts = [] filtered_globals = { name: attr for name, attr in globals_.items() if need(name, attr) } if not inspect.ismodule(globals_["__builtins__"]): for name, attr in globals_["__builtins__"].items(): if need(name, attr): filtered_globals[name] = attr return filtered_globals def bind_globals(globals_: Dict[str, Any]) -> Callable: def _bind_globals(func: FunctionType) -> FunctionType: bound_func = FunctionType(code=func.__code__, globals=globals_, name=func.__name__, argdefs=func.__defaults__, closure=func.__closure__, ) return bound_func return _bind_globals def no_global_variable_decorator(globals_: Optional[Dict[str, Any]] = None): partialled = partial(globals_with_module_and_callable, globals_=globals_) def _no_global_variable(excepts: Optional[List[str]] = None): partialled_globals_ = partialled(excepts=excepts) bound_func = bind_globals(globals_=partialled_globals_) return bound_func return _no_global_variable global noglobal class noglobal: def __init__(self, excepts=None): self.excepts = excepts def __call__(self, _func): return no_global_variable_decorator( globals_=_func.__globals__ )(excepts=self.excepts # arg of _no_global_variable )(func=_func) # arg of _bind_globals
使用例
ここで示すのは、noglobal::ver05とnoglobal::ver05-2です。この2つは、noglobalのimportを介した定義方法が異なるだけで、内部的な処理には違いはありません。
注意したい挙動なのが、最後のfunc_use_excepts
です。デコレータとして、@noglobal(excepts=["a"])
とすることで、func_use_excepts
内ではグローバル変数であるa
を特例的に使用することを許可しています。しかし、そのfunc_use_excepts
内にネストされている関数func_nest
は、宣言時にグローバル変数の使用に関して特例を認めていません。そのため、func_use_excepts
内ではグローバル変数a
を参照できるけど、ネストされている関数func_nest
ではa
を参照できない、という挙動になります。
noglobal()による変数名前空間の限定は、関数の宣言時にfixされます。この辺りには注意する必要があります(使用したければ、明示的に引数として渡す、などが必要です。むしろこのstrictさこそがnoglobalの存在意義なのですが)。
# どちらで宣言しても挙動は同一 # use as ver05 # from noglobal import no_global_variable_decorator # noglobal = no_global_variable_decorator(globals()) # use as ver05-2 <- 特別な理由がなければこちら推奨 from noglobal import noglobal a = "hoge" def run(f): try: f() except NameError as ne: print(ne) print() @noglobal() def func_usual(): print("This is func_usual") print(a) import numpy as np @noglobal() def func_nest(): print("This is func_nest") print(np.arange(0,10)) print(a) run(func_usual()) @noglobal(excepts=["a"]) def func_use_excepts(): print("This is func_use_excepts") print(np.arange(0,10)) print(a) run(func_nest()) # Raised NameError because func_nest # does not allow to use global variable; a. >>> run(func_usual) This is func_usual name 'a' is not defined >>> run(func_nest) This is func_nest [0 1 2 3 4 5 6 7 8 9] name 'a' is not defined >>> run(func_use_excepts) This is func_use_excepts [0 1 2 3 4 5 6 7 8 9] hoge This is func_nest [0 1 2 3 4 5 6 7 8 9] name 'a' is not defined
noglobalをめぐる大まかな流れ
まずは、これから作成していくnoglobalってなんぞや?何がしたいんや?っていうあたりの話を進めていきましょう。歴史を振り返ります。
序:先人たちの変数汚染との戦いの記録
今回のターゲット、noglobal
を最初に知ったきっかけは、とあるTwitterでの質問に遡ります。質問をした@fkubotaさんがQiitaの記事を執筆しています。そちらもご参照ください。
aaa = 1
— fkubota 🦉 (@fkubota_) 2020年12月6日
def hoge(b):
c = aaa + b
return c
print(hoge(2))
↑これ動くの嫌なんですが、どうにかしてエラー吐かせるようにできないですか?
与えた引数の変数しか使えないようにしたいです…
本質問についてざっくりと概要について触れると、「jupyter notebookでは、無秩序にグローバル変数が定義・使用されている。それによって、変数名が重複してしまったり、定義した関数内で意図せずに既に宣言されているグローバル変数を参照してしまったりすることで意図しない動作を生む恐れがある。これを回避するために、関数内ではグローバル変数は使用しない、というように制限をかけれないか?」という旨です。
それに関しては、@nyker_gotoさんからすぐに回答がありました。
https://t.co/5SYejzWdkO こちらの方法はどうでしょうか。むかしまますさんに教えていただいたものです。
— nyker_goto (@nyker_goto) December 6, 2020
ほう。そんな便利なものがあるのですね。都合上、このax3lさんによるnoglobalを noglobal_ver01としましょう。
しかしこれに関して、nyker_gotoさんへ本関数の存在を知らしめたまますさんから、追加の情報が。
実はこれ使うとデフォルト引数が使えないっていう問題があるんですが、誰かpythonの言語機能に詳しい人使える方法思いついたりしないですかね。。。。。 https://t.co/6DC7xP5O5b
— まます (@mamas16k) December 6, 2020
さすがは現代、世は令和。早いもんです。このツイートに対してもすぐに回答がつきました。
こんなのでもできそうですhttps://t.co/aDLDUX9aWZ
— raven (@raven_38_) December 6, 2020
こんにちは.こちらで如何ですか?https://t.co/BCVbzamUZC
— Yoshipon (@yoshipon0520) December 6, 2020
どちらもax3lさんによるnoglobalをupdateしたもので、それぞれraven38さん、yoshiponさんによるものです。オリジナルのax3lさんによるnoglobalに倣って、これらをそれぞれver02、ver03とします。
これをjupyter notebook内の適当なセル内で宣言することで、かなり快適になりました。どんな動作になるかについては、後述の実験の項目をみてもらってもいいし、前述の最終的に採択されたコード内の例をみてもらえればと思います。
Pythonはコンパイル言語ではないこと、jupyter notebookを使用することでかなりインタラクティブにデータをいじったりコードを書いたりすることができること。これらがとてもメリットを有している反面、変数が乱立して名前空間が汚染されていきます(毎回.pyスクリプトで記述してしまえばいいのですが、なんやかんやで実験的にコードを書いていく時にはjupyterを使用するのが便利ですよね)。そんな中で、こういったものを使用することで、かなり保守的でバグの起こりにくいコードか書けるようになるのではないかなと思います。事実、ボクはそうです。
なお、このver03にはバグがあり、限定的な状況でTypeErrorが発生します。それは、noglobal
でwrapされる関数の引数にデフォルト値が代入されていない場合、です。これに関しては、その状況時にのみ局所的に対応できるように
if`文を一箇所に追加するだけで対処できたので、そこまで大きな問題ではないかなと思います。が、一応、対処したものはnoglobal::ver03-2_K-PTLとして区別することにします。
破:狭い用例への苦悶
面倒なのです。
毎回、jupyter notebookにnoglobalの定義、宣言を記述するのが。
別スクリプトに書いてそれをimport
してしまえばもっと簡単に使えるようになるんじゃ?!と思うわけですね。import
してやれば別ファイルに書いたコードがインライン展開されていい感じになるんじゃ??と考えました。実行しました。
うまくいきません。
builtins関数が全て使用不可能になりました。
理由は自明で、c++のinclude
と違って、pythonのimport
ではそれをコード内にインライン展開するわけではないからなのですね。これが、非コンパイル言語の壁か。と強く感じました。
同じglobal変数空間でも、今メインで使用しているファイル(実行環境)内でのそれと、import
してくる関数(変数)が置かれている環境でのそれとでは、ものが違うのです。そのため、同様に使用したくてもできない。
これはpythonのバージョンを3.8系に上げたことによる(これまでは3.7系だった)弊害なのか?とinspect
の挙動などを調査していましたが解決できず、ぼそっとTwitterでこぼしたところ、またまた登場。まますさんがやってきました。
僕のpython知識がポンコツ過ぎてマジでどうしようも出来ないんですが、これだとどうですか?importの仕方も書いてある気がしますhttps://t.co/xH5iJbIZHN
— まます (@mamas16k) February 14, 2021
いやぁ本当に助かった。助かりまくった。ここで紹介されているのは、これまでのnoglobalをさらに発展させた-importで使用できるように改良された-もみじあめさんによるnoglobalです。import
できるようになったということで、これはかなりの進化です。これも便宜上、ver04と呼ぶことにしましょう。
あぁ、noglobal
がこれでimportを介して使用できるようになりました。これは、先に述べたglobals()が実行環境とimport元のファイルとで異なっていることが問題なので、importした後に実行環境中のglobals()を渡すことで、builtinsに対しても問題なく使用できるようになったものです。
おそらく既存の noglobal 系で、別モジュールに定義して動くのは私のコードだけだと思います。globals() は呼び出したモジュールのグローバルシンボルテーブルしか返せないので、ここはどうしても使う所で呼ばないといけないんですよね。
— もみじあめ (@momijiame) February 15, 2021
これによって、noglobal
は以下のたった2行を記述するだけで実現するようになったのです。素晴らしい。
from noglobal import no_global_variable_decorator noglobal = no_global_variable_decorator(globals_=globals())
importで気楽に使用できるnoglobal
を手に入れた私は、バンバン使いまくります。
jupyterを使用してコードを記述しているときはもちろん、.pyスクリプトで記述し、そのまま実行するときさえもnoglobal
をつけて、変にグローバル変数の影響を受けないようにコードを記述していくようになります(実際、本当にコード内で「この変数どこで宣言したっけ?」とか、「既にこの変数名使用したっけ?」とか、そういった問題もなく、かなり各コードの質が上がったように感じます)。
ただ、この使い方をしていると、新たな問題にぶつかりました。またか、と。
ただ、この問題を解決すると同時に、このnoglobal
は縛られることのない、自由を手に入れることになるのです。
急:完全体の目醒め 〜 no global, yes python
ぶつかった問題は、ここで書くには些か面倒なのですが、以下のようなフォルダ構成だったとしましょう。
~./ |- a.py |- b.py |- noglobal.py
a.py
from noglobal import no_global_variable_decorator noglobal = no_global_variable_decorator(globals_=globals()) @noglobal() def hoge(): print("hoge")
b.py
from noglobal import no_global_variable_decorator noglobal = no_global_variable_decorator(globals_=globals()) from a import hoge @noglobal() def fuga(): hoge()
こんな状況です。a.py内で宣言された、noglobalでwrapされた関数をb.pyで使用しています。するとどうでしょう。NameErrorが発生します。なんのって?
NameError: name `print` is not defined
builtins関数が使用できなくなりました。オーマイガー。一瞬、一瞬ですが絶望しました。まさかこんなことになるとは。
この問題に関して、実際にコードを走らせて調べてみました。実際の詳細は省略します(気になったら手元で調べてみてください)が、原因はglobals()
そのものにありました。
現在実行しようとしているb.py上でのglobals()
は、以下のような中身になっています。builtinsは__builtins__
として保持され、その中身は globals()
の中身です。
{ '__name__':'__main__', '__doc__': b.pyのdocstring, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000001CEE11E3FD0>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'b.py', '__cached__': None, 'np': <module 'numpy' from '~/Python/Python38/site-packages/numpy/__init__.py'>, ... }
それに対して、b.py実行時のa.pyにおけるglobals()
はどうなっているのでしょうか。確認すると、以下のようでした。そうです。builtinsがモジュールではなく、展開された形(つまり、グローバル変数と同じように)保持されているのです。これが、両者の大きな違いです。
{ '__name__':'__main__', '__doc__': b.pyのdocstring, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000001CEE11E3FD0>, '__spec__': ModuleSpec(name='a', loader=<_frozen_importlib_external.SourceFileLoader object at 0x00000224BAC376A0>, origin='~/a.py'), '__annotations__': {}, '__builtins__': { '__name__': 'builtins', '__doc__': "Built-in functions, exceptions, and other objects.\n\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.", '__package__': '', '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': ModuleSpec(name='builtins', loader=<class '_frozen_importlib.BuiltinImporter'>), '__build_class__': <built-in function __build_class__>, '__import__': <built-in function __import__>, 'abs': <built-in function abs>, 'all': <built-in function all>, 'any': <built-in function any>, 'ascii': <built-in function ascii>, 'bin': <built-in function bin>, 'callable': <built-in function callable>, 'chr': <built-in function chr>, 'compile': <built-in function compile>, 'delattr': <built-in function delattr>, 'dir': <built-in function dir>, 'divmod': <built-in function divmod>, 'eval': <built-in function eval>, 'exec': <built-in function exec>, ... '__file__': 'a.py', '__cached__': None, 'np': <module 'numpy' from '~/Python/Python38/site-packages/numpy/__init__.py'>, ... }
これらの変数らに対して、弾く対象の変数であるかどうかを判定しているnoglobal
のコードは、以下の部分です(「最終的に採択されたコード」で示したコードの抜粋です)。globals()
内のキーを走査し、それがexceptsに入っているとき(name in excepts
)、モジュールであるとき(inspect.ismodule(attr)
)、呼び出し可能なオブジェクトであるとき(callable(attr)
)にwrapされた関数内でも使用可能な変数である、と判定しています。つまり、builtinsは実行環境中ではmoduleとして認識されていますが、importされる時にはその中ではmoduleとしてではなく、それぞれが独立した変数として動作していたため、この判定の部分で弾かれてしまったのです。
def need(name, attr): if name in excepts: return True if inspect.ismodule(attr): return True if callable(attr): return True return False if globals_ is None: globals_ = globals() if excepts is None: excepts = [] filtered_globals = { name: attr for name, attr in globals_.items() if need(name, attr) }
というわけで、これに対する処理を愚直に足します。追加するのは、以下の3行だけです。
for
を使用しているため、処理としては決して最速なものではないですが、対応としては十分だと思います(ベターな手法があったら、いつでもPRお待ちしています)。
最初のif not inspect.ismodule(globals_["__builtins__"])
で、globals()
内におけるbuiltins
がモジュールなのか、辞書型なのかを判別します。余談ですが、今回のこのnoglobalと闘う中でinspect
というライブラリを知ったのですが、かなりこの子便利ですね。早速、結構活用しています。
余談は置いておいて、もしbuiltins
がモジュールでない場合、これはimportされるpythonスクリプト内であることを意味します。そしてその時、builtins関数は辞書としてglobals()["builtins"]
でアクセスできますので、それらに対して要素を走査します。最初のglobals()
に対してと同様に、need()
関数でbooleanの判定をします。
if not inspect.ismodule(globals_["__builtins__"]): for name, attr in globals_["__builtins__"].items(): if need(name, attr): filtered_globals[name] = attr
これで、importするpythonスクリプト内でnoglobal
を使用していたとしても、問題なく運用できるようになりました。これも便宜上、番号で管理しましょう。noglobal::ver05_K-PTLとします。素晴らしいですね。もう欠点が見当たりません。最強です。
、、、とはならないんですね。まだやれることがあります。それは、使うまでのプロセスがまだ一手間多い、という面倒臭さに対する対処です。ちなみに、現在、使用するために必要な記述はこんな感じです。
from noglobal import no_global_variable_decorator noglobal = no_global_variable_decorator(globals_=globals())
importする対象のスクリプト内でnoglobal()
を使用していても、問題なく現在の実行スクリプト内でnoglobal()
が機能するのです。ならこれ、逆も成立すると思いませんか?
つまり、importするpythonスクリプト内でnoglobal()
をdeclareして、そのnoglobal()
自体をimportしても、現在の実行スクリプト内で問題なく動くと思いませんか?
実は、これはできません。というのも、これは全くワケが違うからです。実行中のスクリプトで参照されるglobals()
と、import対象となるpythonスクリプト上でのglobals()
とでは、参照範囲が異なるglobal symbol tableになっているのです。つまり、実行中のスクリプトでimportしてきた関数やオブジェクトは、callableであろうともimport対象となるスクリプト内のglobal symbol tableからは見えないため、例えばnumpyやpandasと言ったライブラリも使用できなくなってしまいます。とても不便です。
でも、やはりimportだけで運用できるようにならないと、現実的に手間が多く面倒です。
そこで、以下のような革新的なnoglobalを定義します。classとしてnoglobalを保持し、デコレータとして書いた時の引数は__init__
へ渡され、wrapされた関数は__call__
へと渡される仕組みです。よく思いついた、ボク。褒めて欲しい。すでに記載した関数に関しては、関数名だけの記載としています。
noglobal.py
from functools import partial import inspect from typing import List, Dict, Optional, Callable, Any, FunctionType # Function prototype global noglobal def globals_with_module_and_callable(globals_: Optional[Dict[str, Any]] = None, excepts: Optional[List[str]] = None) -> Dict[str, Any]: def bind_globals(globals_: Dict[str, Any]) -> Callable: def no_global_variable_decorator(globals_: Optional[Dict[str, Any]] = None): # substance of noglobal function class noglobal: def __init__(self, excepts=None): self.excepts = excepts def __call__(self, _func): return no_global_variable_decorator( globals_=_func.__globals__ )(excepts=self.excepts # arg of _no_global_variable )(func=_func) # arg of _bind_globals
a.py
from noglobal import noglobal glb = "global variable" @noglobal() def hoge(): print("hoge") print(glb) <- NameError: name glb is not defined @noglobal(excepts=["glb"]) def fuga(): print("fuga") print(glb) <- No Error
通常のライブラリ同様、import
文だけでnoglobal()
が使用できるようになりました。このままb.pyに読み込ませても、問題なく動作します。これが、ボクの中で最新最善のnoglobalです。これをnoglobal::ver05-2とします(使用する関数は基本的に同一なので、ver06とはしませんでしたが、個人的なおすすめは圧倒的にver05-2です。ただ、ver05までの変数名には特別手を加えていないので、これまで使用していたnoglobal.pyを今回の最新のverにそのまま置き換えて、これから新たに使用する時にはver05-2の使用方法にすれば問題なく動作すると思います)。
ただ、現時点で全てのケースに対してテストできた訳ではありません。もしかしたら、正常に動作しない状況が存在するかもしれません。その際には、連絡してくださると幸いです(なんならPRしてください)。
以上をまとめると、今回紹介したいnoglobal::ver05では、従来のnoglobalと比較して以下の点が改善されています。
- 他のファイルで宣言されたnoglobalをimportで呼び出して使用できるように (ver04での改善点)
- それを拡張し、importを経由して繋がる全ての他のファイル内でもnoglobalが使用可能に (ver05での改善点)
- noglobalを使用するための定義が、import文だけで完結するように (ver05-2に限る)
(再掲)
検証
noglobalのversion一覧
ここまで、先人たちのnoglobalを発展させて、最終的に得られたnoglobal::ver05を示しましたが、これまでの全バージョンへのリンクを再掲しておきます。ここからは、各versionの挙動を確認・比較するための簡単な実験です。興味のない方は読まなくても平気です。自分のメモも兼ねていますので、多少殴り書きになりますがご容赦ください。
- noglobal::ver01_ax3l
- noglobal::ver02_raven38
- noglobal::ver03_yoshipon
- noglobal::ver03-2_K-PTL
- noglobal::ver04_momijiame
- noglobal::ver05_K-PTL
- noglobal::ver05-2_K-PTL はコード自体はver05と同じ、importの仕方が違う
実験概要
これから、各versionのnoglobal
を比較していきます。これらの関数は、importして呼び出されたり、inlineにベタ書きされていてそのまま実行されたりします。実行環境も、python scriptを直接実行するか、jupyter notebookを介するかで分かれます。さらに、wrapに使用するnoglobal
も、前述のように複数種類存在します。そのため、実験のパターンとしては結構な数になってしまいます。
そこで、簡潔に比較するために、共通した関数を使用します。その関数にはそれぞれの関数の使用される状況(importされるのかinlineで書かれているのか、どのnoglobalでwrapされているのか、script上で実行するのか、notebook上で実行するのか)が分かるように命名します。
いずれの関数を実行するにしても、想定される結果は以下の3通りです。発生する例外はNameError
もしくはTypeError
以外あり得ないので、それらの例外をキャプチャする関数 run_function
に関数を渡して実行することにします。この関数にはnoglobal
を付けないので、グローバル名前空間へ容易にアクセスできる状態のものです。
使用する関数の雛形
def run_function(func): try: func() except NameError as ne: print(f"got NameError with {func.__name__}; ") print('\t', ne) except TypeError as te: # noglobal::ver03 でのみ発生 print(f"got TypeError with {func.__name__}; ") print('\t', te) return None val_global = "This is a global variable" # グローバル変数 @noglobal5() # wrapするnoglobalの種類は、ここでの記述を変えることで切り替えます def script_import_func5(): print("This is func5 at script wrapped by import `noglobal5`") # Description of this function ; どういった関数なのか、簡単にその説明を出力します val_local = "This is a local variable" # ローカル変数 print(val_local) # ローカル変数の出力 print(val_global) # グローバル変数の出力;noglobalが機能すればNameErrorが発生する script_import_func4() # 関数をネストする場合はこんな感じに関数内部に記載する run_function(script_import_func5)
想定される結果
# 1. script_funcがnoglobalでwrapされていない場合、もしくはnoglobalが機能していない場合 >>> run_function(script_func) This is func5 at script wrapped by import `noglobal5` This is a local variable This is a global variable # 2. script_funcがnoglobalでwrapされていて、正常に動作した場合 >>> run_function(script_func) This is func5 at script wrapped by import `noglobal5` This is a local variable got NameError with script_func; name 'val_global' is not defined # 3. script_funcがnoglobalでwrapされているが、builtins関数を認識しない場合 >>> run_function(script_func) got NameError with wrapper; name 'print' is not defined
検証実験のケース一覧
大きく分けて、検証実験は2種類を実施します。
- noglobal のversionごとの動作の比較
以下のケースに対応するように、関数には以下の雛形に則って命名します。なお、versionの- (ハイフン)は関数名として使用できないので、_ (アンダースコア)に変換します。
{$実行環境}_{$noglobalの宣言方法}_func{$noglobal.__version__}
実行環境 | noglobal.__version__ | noglobalの宣言方法 |
---|---|---|
notebook | {無し, 1, 2, 3, 3-2, 4, 5, 5-2} | {import, inline} |
script | {無し, 1, 2, 3, 3-2, 4, 5, 5-2} | {import, inline} |
- noglobalでwrapされた関数をimportして、それをnoglobalでwrapされた関数内部で呼び出す(noglobalがネストされる)場合
以下のケースに対応するように、関数には以下の雛形に則って命名します。なお、versionの- (ハイフン)は関数名として使用できないので、_ (アンダースコア)に変換します。
nest_{$実行環境}_{$noglobalの宣言方法}_func{$noglobal.__version__}
実行環境 | noglobal.__version__ | noglobalの宣言方法 |
---|---|---|
notebook | {4, 5, 5-2} | {import} |
script | {4, 5, 5-2} | {import} |
実験結果
それぞれの実験に対して、実行結果がどうであったかを「想定される結果」に記載した「1 ~ 3」の番号で示します。実行結果の詳細に関しては、Githubに実験に使用したpyスクリプトとJupyter notebookを上げておりますので、そちらをご参照ください。
各verのnoglobalの比較
各条件での比較結果を一気に表で示します。
ネストしない場合には、ver04以降を使用すれば問題ありません。ただし、このnestというのは、numpy
などの有名モジュールも含まれます。もしimport経由でnoglobalを定義する場合には、ver04では正常に動作しないので注意してください(これはいわゆる関数をnestした状態と見做せるので、ここでは実験していません。次の実験結果を参照してください)。
noglobalをネストした場合の挙動
ver04からver05にかけて、nestした際の挙動が大きく改善できています。ver05に関しては、ver05だろうとver05-2だろうと、正直非の打ちどころがありません。問題なく使用できることでしょう。
実験・まとめ
各versionmのnoglobbal等を、同一条件でひたすらに比較しました。歴史を追うと、一歩一歩改善されて行っているのが目に見えて、とても興味深かったですね。今回新たに作成したnoglobal::ver05ではimport時にもnestして使用する場合にも問題なく挙動することを確認しました。特に、importを容易にするためにclassを活用したnoglobal::ver05-2は、いずれのversionよりも正常に機能し、かつ導入方法も最も容易であることが改めて示されたことでしょう。
おわりに
ひょんなことから、noglobalの問題点が目についてしまって、もっと良くならないか、もっと、もっと、と試行錯誤をしていたら、最終的に今回のver05-2を完成させることができました。地道に考え続けることを辞めないでよかったと心から思います。
また、冒頭にも述べましたが、雛形のコードは先人等が作り上げてくれたものです。感謝します。
これにて、noglobalをめぐる戦いが、それぞれの中で終結を迎えられんことを祈ります。
次のフェーズで、またお会いしましょうね。
”今日お前が歌った唄は、お前が未来のお前に向けて歌った唄なんだよ”
”未来で辛い時苦しい時、お前あの時あんなに、心込めて、良い声で、生きてる証拠を歌ってたじゃねぇかよって”
- BUMP OF CHICKEN 2019 Aurora Ark Tour Final @東京ドーム MCより
(せーのっ)
\\\ Flare //
ここだけのハナシ、flareをある言語からある言語に翻訳するとCHAMAになるんですよ。BUMPが4人、再び揃う日はいつになるのやら。
またね、ばいばい。