この記事は、 Perl5 Advent Calendar 2016 21日目の記事です。
はじめまして、 mp0liiu と申します。
今日は僕が前から気になっていたAUTOLOADについて勉強がてら書いていきたいと思います!
AUTOLOADとは
パッケージに定義されていないサブルーチンを呼び出した時、定義していると代わりに呼び出されるサブルーチンです。
引数にはサブルーチンが呼び出された時のものがそのまま渡されます。
また、グローバル変数AUTOLOADを宣言していると、そこに呼びだそうとしたパッケージ名+サブルーチン名が格納されます。
use v5.24;
use warnings;
package Hoge {
use Data::Dumper;
sub AUTOLOAD {
# グローバル変数AUTOLOAD に呼びだそうとしたパッケージ名 + サブルーチン名が格納される
our $AUTOLOAD;
say "呼びだそうとしたサブルーチン : $AUTOLOAD";
say "引数 : " . Dumper \@_;
}
}
# 存在しないサブルーチンを呼び出す
Hoge::nothing('foo', 'bar');
呼びだそうとしたサブルーチン : Hoge::nothing
引数 : $VAR1 = [
'foo',
'bar'
];
主な使用例
定義されていないサブルーチンの代わりに別のサブルーチン呼ぶ
一番多いのはこれでしょうか。
AUTOLOADの中から代わりのサブルーチンを呼びだす場合は普通にサブルーチンを呼び出すのではなく、特殊なサブルーチン呼び出しを行うgoto関数を使うことが多いようです。
このgoto関数はラベルにジャンプするようなものではなく、
引数にサブルーチン名かコードリファレンスを渡してサブルーチンを呼び出します。
大体以下の様なコードと同じ動きをします。
sub hoge {
goto &subname;
}
sub hoge {
return subname(@_);
}
普通のサブルーチン呼び出しと違うのは、呼び出し元がスタックフレームに積まれないことです。
callerなどで呼び出し元を確認してみると、スタックフレームにgotoで呼び出したサブルーチンの呼び出し元がないことがわかります。
use v5.24;
use warnings;
use Carp qw/longmess/;
sub goto_from {
goto &hoge;
}
sub nomal_from {
hoge();
}
sub hoge {
say " stack trace " . longmess();
}
goto_from();
nomal_from();
stack trace at goto_test.pl line 14.
stack trace at goto_test.pl line 9.
main::nomal_from() called at goto_test.pl line 15.
メソッドの動的な定義に使う
$AUTOLOADに格納された呼びだそうとしたメソッドの名前を使って、動的にメソッドを定義することにも使えます。
例えば、動的にアクセッサを定義したりすることができます。
use v5.24;
use warnings;
use utf8;
binmode STDOUT, ':utf8';
package Fuga {
use Class::Accessor::Lite new => 1;
sub AUTOLOAD {
our $AUTOLOAD;
my ($method) = ($AUTOLOAD =~ /([^:']+$)/);
{
say "${method}を定義します";
no strict 'refs';
*{$method} = sub {
use strict 'refs';
my ($self, $val) = @_;
return $self->{$method} = $val if @_ == 2;
$self->{$method};
};
}
goto &$method;
}
sub DESTROY {}
}
my $obj = Fuga->new(name => 'who');
say $obj->name;
say $obj->name;
使用する際の注意点
DESTROYを定義していないとオブジェクトを破棄する際にAUTOLOADが呼ばれてしまう
Perlではオブジェクトの破棄時に(定義されていれば)DESTROYメソッドが呼ばれるようになっていますが、
AUTOLOADを定義している場合、DESTROYメソッドが定義されていないとオブジェクトを破棄する際にAUTOLOADが呼びだされてしまいます。
オブジェクト指向でモジュールを作っている場合は気をつけましょう。
これを避けるためには、何も処理をしないDESTROYメソッドを定義するか、AUTOLOADの中でDESTROYメソッドが呼ばれた時はreturnするようにすればいいようです。
なお、MooseやMouseでモジュールを作っている場合は呼び出されないので気にしなくても大丈夫です。
(Mooの場合はAUTOLOADが呼び出されてしまいます)
また、モジュールをuseするときに呼ばれるimport関数やunimport関数は、DESTROYと違って定義されていなくてもAUTOLOADは呼ばれません。
存在しないメソッドを呼び出してもエラーにならないようになる
存在しないメソッドを呼び出してもエラーにならないと困ることがあるので、そのような場合は自分でAUTOLOADの中で例外を投げるようにしないといけません。
AUTOLOADの中で定義されていないサブルーチンを呼び出すと再帰呼出しになる
そんなことをする人はいないと思いますが...!
他にも注意した方がいいことがあれば、コメントなどで教えていただけると嬉しいです。
ベンチマーク
便利なAUTOLOADですけど速度が気になるのでベンチマークをとってみました。
use v5.24;
use warnings;
use utf8;
use Benchmark qw/timethese cmpthese/;
binmode STDOUT, ':utf8';
package Nomal {
use Class::Accessor::Lite (
new => 1,
rw => ['hoge'],
);
}
package Autoload {
use Class::Accessor::Lite new => 1;
sub AUTOLOAD {
my $self = shift;
$self->{hoge};
}
sub DESTROY {}
}
my $nomal_obj = Nomal->new(hoge => 'fuga');
my $autoload_obj = Autoload->new(hoge => 'fuga');
cmpthese(
timethese(1000000 => {
nomal => sub { $nomal_obj->hoge },
autoload => sub { $autoload_obj->hoge },
})
);
Benchmark: timing 1000000 iterations of autoload, nomal...
autoload: 1 wallclock secs ( 0.57 usr + 0.00 sys = 0.57 CPU) @ 1754385.96/s (n=1000000)
nomal: 0 wallclock secs ( 0.16 usr + 0.00 sys = 0.16 CPU) @ 6250000.00/s (n=1000000)
(warning: too few iterations for a reliable count)
Rate autoload nomal
autoload 1754386/s -- -72%
nomal 6250000/s 256% --
やっぱり普通に呼び出すより遅くなるんですねー。
実際に使うときはAUTOLOADの中でもっといろいろすると思うので、更に速度が落ちそうです。
速度を気にするなら動的にメソッドを定義したりしたほうが良さそうですね。
おわりに
いろんなことができそうで面白いですね!
複雑になりすぎない程度に活用してみようと思いました。