メソッドを局所的に上書きする

知ってそうで意外と知られていないperlの小技 10選という記事にある「モジュールのメソッドを上書きする」という項について考えてみました。記事にあるやり方では、

  1. 本来のメソッド(コード)のリファレンスを保存
  2. 型グロブによりメソッドを上書き
  3. 上書きしたメソッドを実行
  4. 保存していたコードリファレンスを更に上書き

となっていたのですが、こうするよりも、上書きが必要な箇所のみをブロック({ ... })で囲み、その中でlocalを使って上書きする方がよいのでは、と思ったのです。具体的に書くと以下の通り。

use strict;
use warnings;

#----------

package Foo;
sub something { print "something original.\n" };

#----------

package main;
Foo->something();
{
    no warnings 'redefine';
    local *Foo::something = sub { print "something overwitten.\n" };
    Foo->something();
}
Foo->something();

実行結果は以下の通り。

something original.
something overwitten.
something original.

こうすると本来のメソッドのリファレンスを保存する手間も省けるし、メソッド上書きと no warnings 'redefine' の効果の範囲も分かり易いのではないかと思います。

……ということを考えていたのですが、後日、Re: 知ってそうで意外と知られていないperlの小技 10選という別の記事で更に別のやり方が提示されていました。

事前にundefすると、 no warnings 'redefine' しなくとも警告は出ません。

use strict;
use warnings;

sub do_something { ... }

undef &do_something;
*do_something = sub { print "Implemented.\n" };

do_something;

サブルーチンをundefで未定義にできる、ということを知らなかったのでなるほどと思ったのですが、これをブロック内のlocal上書きでもできるだろうか、というところが気になり、試してみました。

use strict;
use warnings;

#----------

package Foo;
sub something { print "something original.\n" };

#----------

package main;
Foo->something();
{
    undef &Foo::something;
    local *Foo::something = sub { print "something overwitten.\n" };
    Foo->something();
}
Foo->something();

結果は以下の通り。

something original.
something overwitten.
Undefined subroutine &Foo::something called at b.pl line 18.

ブロック内であってもundefの効果はグローバルに及んでしまうので、本来のメソッドを保持できなくなっていました。

しかしlocalで宣言だけするなら、宣言した変数は未定義になることを思い出し、

    local *Foo::something;
    *Foo::something = sub { print "something overwitten.\n" };

とすればよいのでは、と思いつきました。これは一応意図通りに動いたのですが、型グロブでまとめてlocal宣言するということは、同じ名前を持つスカラ・配列・ハッシュ等の変数にも効果が及ぶということでもあります。以下のコードを試してみました。

use strict;
use warnings;

#----------

package Foo;
sub something { print "something original.\n" };
our $something = 'SOME STRING';

#----------

package main;
Foo->something();
print $Foo::something, "\n";
{
    local *Foo::something;
    *Foo::something = sub { print "something overwitten.\n" };
    Foo->something();
    print $Foo::something, "\n";
}
Foo->something();
print $Foo::something, "\n";

結果は以下の通り。

something original.
SOME STRING
something overwitten.
Use of uninitialized value $Foo::something in print at b.pl line 19.

something original.
SOME STRING

メソッドと同じ名前のスカラ変数($Foo::something)もまとめて未定義にされていました。なのでlocal宣言と上書きを分けて書くのは注意が必要かもしれません。

ちなみに、local宣言と同時にコードリファレンスを代入してやると、他の変数が上書きされるようなことにはなりませんでした。動作としてはやや不思議な気もしますが、これが一番問題の無い書き方のように思います。

use strict;
use warnings;

#----------

package Foo;
sub something { print "something original.\n" };
our $something = 'SOME STRING';

#----------

package main;
Foo->something();
print $Foo::something, "\n";
{
    no warnings 'redefine';
    local *Foo::something = sub { print "something overwitten.\n" };
    Foo->something();
    print $Foo::something, "\n";
}
Foo->something();
print $Foo::something, "\n";

結果は以下の通り。ブロック内でも$Foo::somethingの値は保持されていました。

something original.
SOME STRING
something overwitten.
SOME STRING
something original.
SOME STRING

関連: