Pay money To my Life

Spend time for myself, and you... "Knowledge was the only way to remember that your past is true"

Pythonの変数空間汚染との戦い・完結編 ~ noglobal

はじめに

本記事は、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::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の記事を執筆しています。そちらもご参照ください。

本質問についてざっくりと概要について触れると、「jupyter notebookでは、無秩序にグローバル変数が定義・使用されている。それによって、変数名が重複してしまったり、定義した関数内で意図せずに既に宣言されているグローバル変数を参照してしまったりすることで意図しない動作を生む恐れがある。これを回避するために、関数内ではグローバル変数は使用しない、というように制限をかけれないか?」という旨です。

それに関しては、@nyker_gotoさんからすぐに回答がありました。

ほう。そんな便利なものがあるのですね。都合上、このax3lさんによるnoglobalnoglobal_ver01としましょう。

しかしこれに関して、nyker_gotoさんへ本関数の存在を知らしめたまますさんから、追加の情報が。

さすがは現代、世は令和。早いもんです。このツイートに対してもすぐに回答がつきました。

どちらもax3lさんによるnoglobalをupdateしたもので、それぞれraven38さん、yoshiponさんによるものです。オリジナルのax3lさんによるnoglobalに倣って、これらをそれぞれver02ver03とします。

これをjupyter notebook内の適当なセル内で宣言することで、かなり快適になりました。どんな動作になるかについては、後述の実験の項目をみてもらってもいいし、前述の最終的に採択されたコード内の例をみてもらえればと思います。

Pythonコンパイル言語ではないこと、jupyter notebookを使用することでかなりインタラクティブにデータをいじったりコードを書いたりすることができること。これらがとてもメリットを有している反面、変数が乱立して名前空間が汚染されていきます(毎回.pyスクリプトで記述してしまえばいいのですが、なんやかんやで実験的にコードを書いていく時にはjupyterを使用するのが便利ですよね)。そんな中で、こういったものを使用することで、かなり保守的でバグの起こりにくいコードか書けるようになるのではないかなと思います。事実、ボクはそうです。

なお、このver03にはバグがあり、限定的な状況でTypeErrorが発生します。それは、noglobalwrapされる関数の引数にデフォルト値が代入されていない場合、です。これに関しては、その状況時にのみ局所的に対応できるようにif`文を一箇所に追加するだけで対処できたので、そこまで大きな問題ではないかなと思います。が、一応、対処したものはnoglobal::ver03-2_K-PTLとして区別することにします。

破:狭い用例への苦悶

人間なのです。結局、ボクは人間で、面倒くさがりで、サボりたくて。


面倒なのです。

毎回、jupyter notebookにnoglobalの定義、宣言を記述するのが。

スクリプトに書いてそれをimportしてしまえばもっと簡単に使えるようになるんじゃ?!と思うわけですね。importしてやれば別ファイルに書いたコードがインライン展開されていい感じになるんじゃ??と考えました。実行しました。

うまくいきません。

builtins関数が全て使用不可能になりました。

理由は自明で、c++includeと違って、pythonimportではそれをコード内にインライン展開するわけではないからなのですね。これが、非コンパイル言語の壁か。と強く感じました。

同じglobal変数空間でも、今メインで使用しているファイル(実行環境)内でのそれと、importしてくる関数(変数)が置かれている環境でのそれとでは、ものが違うのです。そのため、同様に使用したくてもできない。

これはpythonのバージョンを3.8系に上げたことによる(これまでは3.7系だった)弊害なのか?とinspectの挙動などを調査していましたが解決できず、ぼそっとTwitterでこぼしたところ、またまた登場。まますさんがやってきました。

いやぁ本当に助かった。助かりまくった。ここで紹介されているのは、これまでのnoglobalをさらに発展させた-importで使用できるように改良された-もみじあめさんによるnoglobalです。importできるようになったということで、これはかなりの進化です。これも便宜上、ver04と呼ぶことにしましょう。

あぁ、noglobalがこれでimportを介して使用できるようになりました。これは、先に述べたglobals()が実行環境とimport元のファイルとで異なっていることが問題なので、importした後に実行環境中のglobals()を渡すことで、builtinsに対しても問題なく使用できるようになったものです。

これによって、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__として保持され、その中身は となっています。また、importしたnumpyなど、グローバル変数等がそのままjson形式(dict)で羅列されています。これが、b.pyにおけるglobals()の中身です。

{
    '__name__':'__main__', 
    '__doc__': b.pydocstring,
    '__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.pydocstring,
    '__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の挙動を確認・比較するための簡単な実験です。興味のない方は読まなくても平気です。自分のメモも兼ねていますので、多少殴り書きになりますがご容赦ください。

実験概要

これから、各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の比較

各条件での比較結果を一気に表で示します。 f:id:K_PTL:20210225010136p:plain

ネストしない場合には、ver04以降を使用すれば問題ありません。ただし、このnestというのは、numpyなどの有名モジュールも含まれます。もしimport経由でnoglobalを定義する場合には、ver04では正常に動作しないので注意してください(これはいわゆる関数をnestした状態と見做せるので、ここでは実験していません。次の実験結果を参照してください)。

noglobalをネストした場合の挙動

f:id:K_PTL:20210225010202p:plain

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人、再び揃う日はいつになるのやら。

またね、ばいばい。