[Perl] my と local と our の違いについて

404 Blog Not Found:perl - myとourとscopeと

みんな難しく考えすぎです。

(例外については後で考えることにして)とりあえず以下の基本をおさえておけば混乱することはないと思います。

■ our 宣言について

our はグローバル変数をスマートに使用するための宣言です。our は use vars と等価と考えてください。

our $var;

use vars qw($var);

と等価。

our $var = 1;

use vars qw($var); $var = 1;

と同じ動きをするもの、と覚えておけばOKです。それ以上の違いは基本的にないと考えて構いません。

ひとつ実用上の違いがあるとすれば、our は Perl 5.6 以降の built-in なのに対して、use vars は vars.pm の Perlスクリプトで実装されています。したがって、use vars よりも our の方が高速でかつスマートです。

ちなみに、最新のPerl5.10でもvars.pmは存在しますので、use vars を使ったプログラムは our に書き換えなくてもそのまま動きます。

vars.pmの実装に興味のある人はこちらの解説もあわせて読んでみてください。→vars

■ local 宣言について

local はグローバル変数の値を一時的にスタック上に退避する命令です。スコープの範囲を外れると元の値が復元されます。
変数の値を実行時のスコープの範囲に限定することができます。

{
  local $var = 2;
  # foo
}

{
  local $var;
  $var = 2;
  # foo
}

と等価。動作としては

push @{__PACKAGE__.'::$var'}, \$var; # 変数の値をスタックに退避
{
  $var = 2;
  # foo
}
$var = ${pop @{__PACKAGE__.'::$var'}}; # スコープを抜けたら復元

という感じで、実行時にブロックで囲まれたスコープを抜けると宣言前の$varの値が復元されます。
(※実際には@{__PACKAGE__.'::$var'}というスタックの場所に値を保存しているわけではありませんが・・・便宜上ということで・・・)

変数の値をスタックに退避する、という動作なので、再帰呼出があってもスタックが許す限り元の値が段階的に復元されます。

追記:ちなみに、変数の値をスタックに退避するタイミングはlocal宣言をした場所なので、本当は

{
  push @{__PACKAGE__.'::$var'}, \$var; # 変数の値をスタックに退避
  $var = 2;
  # foo
}
$var = ${pop @{__PACKAGE__.'::$var'}}; # スコープを抜けたら復元

となるのですが、後で記述するサブルーチンを抜けたときの挙動をわかりやすく説明するため、スコープのちょうど境界外でのスタックの退避・復元と説明とさせてください。

(余談)特殊変数について

記号を含む特殊変数($_や$/など)はmy宣言できませんが、local宣言はできて、一時的にグローバル変数の値を安全に変更することができます。
local $_; や local $/ = 1; などの用法が Perl5 のプログラムでも使われるのはこのためです。

■ サブルーチン内のlocal宣言について

サブルーチン定義の sub func {}、無名関数 sub {} については、関数を宣言した時ではなく、関数を呼び出した時にブロックのスコープを持つと解釈すればOKです。
サブルーチン内で定義されたlocal変数は、func() の実行が終了すると呼び出し前の値に復元されます。

sub func {
  local $var = 3;
  # foo
}
$ret = func();

sub func {
  $var = 3;
  # foo
}
push @{__PACKAGE__.'::$var'}, \$var;
$ret = func();
$var = ${pop @{__PACKAGE__.'::$var'}};

と等価です。

再帰呼出とかクロージャについても特別な解釈はなく、関数が呼び出されるときに変数の値が一時的にスタックに退避され、関数終了時に元の値が復元される、と解釈していれば問題ありません。

ちなみに、サブルーチン内のブロックの中(ifやdoなど)でlocal宣言した場合、そのブロック内でのみ変数のスコープが有効になります。

■ my 宣言について

my はレシキカル変数の宣言です。これで宣言された変数はスコープを外れた場所から参照できなくなります。
既に同じ名前のグローバル変数が定義されている場合、my宣言するとそのスコープ構文の範囲内で有効な(実行時ではない)変数が新しく作られます。

■ レキシカルスコープとダイナミックスコープの違いについて

Scope (computer science) - Wikipedia より

myはレキシカルスコープ(変数の名前を見た目のスコープに限定)
$x = 0;
sub f { return $x; } # グロバール変数$xを参照
sub g { my $x = 1; return f(); } # レキシカル変数$xを別に定義→グロバール変数$xには影響しない
$f = g(); # 関数f()を実行したときのグロバール変数$xの値を返す
warn $f; # 0
warn $x; # 0
localはダイナミックスコープ(変数の値を実行時のスコープに限定)
$x = 0;
sub f { return $x; } # グロバール変数$xを参照
sub g { local $x = 1; return f(); } # グロバール変数$xを一時的に上書き
$f = g(); # 関数f()を実行したときのグロバール変数$xの値を返す
warn $f; # 1
warn $x; # 0

$x = 0;
sub f { return $x; } # グロバール変数$xを参照
sub g { $x = 1; return f(); } # グロバール変数$xを上書き
push @{__PACKAGE__.'::$x'}, \$x; # 一時的に$xの値をスタックに退避
$f = g(); # 関数f()を実行したときのグロバール変数$xの値を返す
$x = ${pop @{__PACKAGE__.'::$x'}}; # $xの値を復元
warn $f; # 1
warn $x; # 0

と等価である、と解釈することができます。

ダイナミックスコープはスクリプト言語の動きになるので、コンパイル言語に馴染んでいる人にとっては最初に違和感があるかもしれませんが、言語処理系の動作を理解していれば、すぐに慣れると思います。

追記:ちなみに、変数の値をスタックへ退避するタイミングはlocal宣言をした場所となるため、

$x = 0;
sub f { return $x; } # グロバール変数$xを参照
sub g { 
  push @{__PACKAGE__.'::$x'}, \$x; # 一時的に$xの値をスタックに退避
  $x = 1; # グロバール変数$xを上書き
  return f();
}
$f = g(); # 関数f()を実行したときのグロバール変数$xの値を返す
$x = ${pop @{__PACKAGE__.'::$x'}}; # $xの値を復元
warn $f; # 1
warn $x; # 0

厳密に書くとこのようなプログラムになります。

■ doブロックもスコープを持っている

単純な{}だけでなく、ifやdoブロック、for、foreach、while、switch、eval{}ブロックなども、そのブロック内で有効なスコープを持ちます。

id:amachangの元記事my と local のサンプル - IT戦記では、

ちなみに do {...} は return の扱いを除いて (sub {...})->() と等価だと考えていいです。まあ、関数をその場で呼び出すようなものですね。

と「関数をその場で呼び出すようなもの」と端折っていますが、ここの仮定が間違っていて、正しくは「一度doブロックを抜けてから関数を呼び出している」という動きになります。

id:amachangのサンプルは書き方が省略されているので見た目上誤解を生みやすいのですが、

our $foo = 0; 
do {
  local $foo = 1; 
  sub { print "$foo\n" } # 0 
}->();

our $foo = 0; 
my $func = do {
  local $foo = 1; 
  sub { print "$foo\n" }
};
$func->(); # 0 

と等価であるので、$func->()実行時には既にdoのスコープから外れているのです。

つまり、このlocal宣言はsubではなくdoブロックにかかっているということで、

our $foo = 0; 
push @{__PACKAGE__.'::$foo'}, \$foo; # 退避
my $func = do {
  $foo = 1; 
  sub { print "$foo\n" }
};
$foo = ${pop @{__PACKAGE__.'::$foo'}}; # 復元
$func->(); # 0

このように動作していると解釈すれば、すべてがスッキリすると思います。

■ スコープの有効範囲について

スコープの有効範囲は、宣言が置かれているブロック{}、eval、またはファイルの末尾までです。

最後にサンプルプログラムを書いて、スコープの有効範囲についてまとめます。

# foo.pl

local $a = -1;
warn $a; # -1 <- foo.pl内でlocal宣言して一時的に変数の値を変更する

# main.pl

our $a = 0;
warn $a; # 0
{
  local $a = 1;
  warn $a; # 1 <- localで一時的にスコープ内に値を限定
}
warn $a; # 0 <- localは元の値を復元する
{
  eval '$a = 2;';
  warn $a; # 2 <- eval内で変数の値を変更すると実行後も有効になる
  eval 'local $a = 3;';
  warn $a; # 2 <- eval内でlocal宣言するとeval後に元の値が復元される
}
our $a = 4;
warn $a; # 4 <- 既に宣言された変数にourしても問題ない
{
  our $a = 5;
  warn $a; # 5 <- スコープ内でもう一度ourしても問題ない
}
warn $a; # 5 <- ただし、ourはスコープを抜けても値を復元しない(localと違う挙動)
require 'foo.pl';
warn $a; # 5 <- foo.plのファイル末尾でスコープは終了したと解釈され元の値を復元する

以上。簡単でしょ?