1. ホーム
  2. R

R: 環境と変数のスコープ問題

2022-01-22 14:56:52


    R言語の文献では、関数はクロージャと呼ばれています。関数は、引数や関数本体だけでなく、quot;environment"も含んでいます。環境は、関数が作成されるときに現れるオブジェクトのセットで構成されています。

トップレベル環境

<スパン > w <- 12> f <- function( y ){。 <未定義

<スパン + d <- 8

<スパン + h <- function(){。 <未定義

<スパン + return ( d*(w+y) )

<スパン + }

+ return ( h() )

<スパン + }

<スパン > 環境( f )

<environment: R_GlobalEnv>

    この例では、関数f()はトップレベル(インタプリタのコマンドプロンプトの下)でビルドされているので、トップレベルの環境下にあります。トップレベル環境はRの出力ではR_GlobalEnvとして表現されますが、Rコードの.GlobalEnvと混同されることがよくあります。Rプログラムをバッチで実行した場合もトップレベルにあるとみなされる。

    ls() という関数は、環境内のすべてのオブジェクトをリストアップします。しかし、トップレベルで呼び出された場合は、トップレベルの環境内のオブジェクトのリストを取得します。より多くの情報を得るには、ls.str()を使用してください。

    関数内で引数なしでls()を呼び出すと、現在のローカル変数(引数を含む)が返されます。envir 引数を指定すると、ls は関数呼び出しの連鎖の中の任意のフレームのローカル変数名を出力します。

変数スコープの階層構造

    Rでは、関数はオブジェクトなので、関数の中に関数を定義することが可能であり、オブジェクト指向プログラミングのカプセル化のために、そうすることが望ましい場合もある。どこでもオブジェクトを作ることができるのです。

    変数dと同じように、h()はf()のローカルオブジェクトです。この場合、変数スコープの階層問題は理にかなっています。R言語の定義では、f()のローカル変数dは、h()に対してローカルである。R言語では、パラメータもローカル変数とみなされるため、パラメータyについても結論は同じである。同様に、変数のスコープが階層的であることから、wはf()のグローバル変数であるため、h()のグローバル変数でもあることがわかる。実際、関数h()の中でもwを使用している。

    環境という点では、h()の環境は、その生成時に定義されたすべてのオブジェクトを含む。つまり、f()が複数回呼ばれると、h()も複数回生成されるが、その後はf()の復帰とともに消滅する。h()はf()に対してローカルであり、トップレベルの環境には見えない。

    階層内で名前の衝突が起こる可能性があり、その場合は最も内側の変数が優先的に使用されます。このように継承された方法で作成された環境は、一般的にそのメモリアドレスを参照するために依存します。

<スパン > f <- function( y ){。 <未定義

<スパン + d <- 8

+ return ( h() )

<スパン + }

<スパン > h <- function(){。 <未定義

+ return ( d * ( w + y ) )

<スパン + }

<スパン > f( 1 ) <スパン

FUN(left, right)でのエラー:二項演算子への非数値引数

    h()はトップレベルで定義されているので、変数dはもはやh()の環境には存在せず、コードはエラーで実行されるのです。さらに悪いことに、トップレベルの環境に無関係の変数dがあったとしても、エラーヒントは得られず、ただエラー結果が返されるだけです。

    上の例のRが、なぜ変数yが存在しないことを示さないのか、不思議に思うかもしれません。これは、先に述べた慣性の原理、つまり、Rは必要なときにだけ変数を計算するからです。この例では、Rはすでにdによるエラーに遭遇しており、yの値を求め続けようとはしないのです。

    上記のコードは、dとyを以下のパラメータに設定することで修正されます。

<スパン > f <- function( y ){。 <未定義

<スパン + d <- 8

+ return ( h( d, y ) ) 。

<スパン + }

<スパン > h <- function( dd, yy ){。 <未定義

+ return ( dd * ( w + yy ) ) 。

<スパン + }

<スパン > f( 1 )

<スパン [1] 104

<スパン

    次のようなコードです。

<スパン > w <- 2

<スパン f <- function( y, hh ){。 <未定義

<スパン + d <- 8

<スパン + print( 環境( hh ) )

+ return ( hh( d, y ) )

<スパン + }

<スパン > h <- function( dd, yy ){。 <未定義

+ return ( dd * ( w + yy ) ) 。

<スパン + }

<スパン > f( 1, h )

<environment: R_GlobalEnv> [1] 24

    f()を実行すると、形式引数ddは実引数dと一致します。引数はローカル変数として扱われるので、ddの環境はトップレベル環境と異なることが推測されます。しかし、クロージャは環境を含むので、ddはhの環境と一致する。

副作用が(ほとんど)ない►機能

    関数型プログラミングの思想のもう一つの特徴は、関数が非ローカル変数を変更しないことである。つまり、一般に、関数を使っても副作用がないのです。関数のコードは読むことができますが、その非ローカル変数は書き込むことができません。コードはこれらの変数に新しいコピーを与えるように見えるかもしれませんが、実際にはこの動作はそのバックアップにのみ影響し、変数そのものには影響しません。 

<スパン 注:Rで任意の関数を呼び出すと、フレーム(枠)が作成されます。フレームには、関数内で作成されたローカル変数と、環境が複合的に評価されたときに作成される新しい環境が含まれます。

<スパン 12

<スパン > f <- function( y ){。 <未定義

<スパン + d <- 8

<スパン + w <- w + 1

<スパン + y <- y - 2

<スパン + print( w )

<スパン + h <- function(){。 <未定義

+ return ( d * ( w + y ) )

<スパン + }

+ return ( h() )

<スパン + }

<スパン 4

<スパン > f( t )

<スパン [1] 13 <スパン [1] 120

<スパン w

<スパン [1] 12

をクリックします。

<スパン [1] 4

    つまり、f()の中で変更されたように見えても、トップレベルのwは変更されていないのです。f()の中でwに続いているローカルだけが変更されているのです。同様に、トップレベル変数tは、それに関連するパラメータyが変更されているにもかかわらず、変更されていない。事実上、ローカル変数wは、ローカル変数の値が変更されるまで、対応するグローバル変数とメモリアドレスを共有しています。この場合、ローカル変数 w には新しいメモリアドレスが割り当てられる。

グローバル変数について

    R では、予想以上にグローバル変数が使用されています。R 自身が、C コードや R ルーチンの中で物理的に、内部的にグローバル変数を多用していることに驚かれることでしょう。例えば、スーパーアサインメント演算子 <<- は多くの R 関数で使用されています(ただし、通常は環境の上位レベルの変数にのみ書き込まれます)。スレッドコードと GPU コードは、グローバル変数を多用する傾向があり、グローバル変数は並列オブジェクトの主要な通信手段となります。どちらも高性能なプログラムを書くために使用されます。

クロージャについて。

    Rの"closure"には、関数への引数、関数の本体、呼び出される環境が含まれています。環境を含めるためにクロージャを使うプログラミング方法があり、このプログラミング方法も"クロージャ"と呼ばれる機能を使っています。

<スパン     クロージャは、ローカル変数を作成する関数と、その変数にアクセスできる別の関数を作成する関数を含んでいます。

<スパン