ソート中にソートキーを再計算しない
- perlのソートはマージソートなので、sortの度にsortブロックが
回呼び出される。
以下に記載されていた方法を紹介するが、それぞれ得手不得手があるので、使うときにベンチマークをすることが重要。
(8.8節では、ソートを最適化するための方法をもう一つ紹介されている。)
一度計算した結果をハッシュに登録しておく
Orcishと呼ばれる手法(キャッシュをOrするが名前の由来)
#スクリプトのSHA-512によるソート use Digest::SHA qw( sha512 ); @sorted_scripts = do { my %sha512_of; #ハッシュを参照して、ハッシュに登録されていなければ、$sha512($a)を計算して、結果をハッシュに登録 sort { ($sha512_of{$a} ||= ($sha512($a)) cmp ($sha512_of{$b} ||= ($sha512($b)) } @scripts; };
他でも計算結果を使い回す場合は%sha512_of宣言をソートの外に出して、スコープを変える。またその場合はdoブロックは要らない。
予めダイジェストを計算しておき、ソートする
この場合、ハッシュスライスが使える!
@sha512_of{@script} = map { sha512($_) } @scripts;
ただし、ソートから例外が出されて、ソートが途中で終わる可能性があるときは、予め計算したSHA-512ダイジェストが無駄になる。
schwartzian変換
このような方法もあるが、若干低速らしい。
#sortした値自体が欲しくないときに使う。 @sorted_scripts = map { $_->[0] } #スクリプトを抽出 sort { $a->[1] cmp $b->[1] } #ダイジェストでソート map { [$_, sha512($_)] } #予め対応するスクリプトとダイジェストの結果を格納する。 @scripts;
サブルーチンの結果を記憶する
一番分かりやすいのは、サブルーチンの結果を記憶すること。(ただし前述のものよりは若干低速。schwartzianとはどうだろう・・・)
どういうことかと言うと、一度サブルーチンを呼び出すと、そのときの引数と結果を記憶していて、同じ引数で再度サブルーチンが呼び出された場合は、記憶している結果をそのまま使用するというもの。
PerlではMemoizeモジュールで簡単に実現できる。
use Digest::SHA qw( sha512 ); #SHA-512ダイジェスト関数の自己キャッシュ use Memoize; memoize('sha512'); #自動的にキャッシュされたSHA-512ダイジェストによるソート @sorted_scripts = sort { sha512($a) cmp cha512($b) } @scripts;
リストの反転にはreverseを使う
分かりやすいし、早い。
@sorted_results = sort {$b cmp $ a} @unsorted_results;
より
@sorted_results = reverse sort @unsorted_results;
とした方が早いことすらある。
また、forループを逆順にループするときに使用すると、簡潔に書ける。
for my $remaining (reverse $MIN..$MAX) { print "T minus $remaining, and counting...\n"; sleep $INTERVAL; }
これをCスタイルループのデクリメントで実現しようとすると、前にも紹介した
Cスタイルの弱点が現れてしまう。
1、比較演算子の選択を誤る(<と<=を間違えるとか)
2、反復変数の更新をしくじる
スカラーの反転にもreverse
スカラーにreverseを適用すると文字列の文字を並べかえることもできる。
ただし、スカラーであることを明記すること。
my $visible_email_address = scalar reverse $actual_email_address;
明記しないと、以下のようにサブルーチンの引数として使用した場合、
リストに作用して、ひとつしかないリスト要素を反転してしまう。
add_email_addr(reverse $email_address);#add_email_address(scalar reverse $email_address)とする。
固定幅のデータにはunpackを使用する
X123-S000001324700000199のような固定長のテキストデータの塊が使われていることがよくある。
ここから、特定文字列を取得することを考えると、
- substrはコードが汚くなるし、処理が遅い。
- 正規表現を使っても処理が遅い。
組み込み関数unpackはこのような時のために最適化された関数。'A'指定子を使用することで、文字列から文字を取り出すことができる。
Readonly my $RECORD_LAYOUT => 'A6 A10 A8'; #6文字 10文字 8文字の並びで指定 while (my $record = <$sales_data>) { my ($ident, $sales, $price)#文字列右から6文字,10文字,8文字を、リストに格納 = unpack $RECORD_LAYOUT, $record; #それぞれハッシュ登録してpush push @sales, { ident => translate_ID($ident), sales => $sales * 1000, price => $price, }; }
データがX123-S 0000013247 00000199のように、何かで区切られている場合は@指定子を使用して、
文字列の何文字目がそれぞれのフィールドの先頭なのかを教える。
Readonly my $RECORD_LAYOUT => '@0 A6 @8 A10 @20 A8' #@は昇順である必要はないので、好きな順序で文字列を取得することができる。
可変幅のデータにはsplitを使用
一般的に使用されていないが、splitの第3引数の使用を推奨する。
この第3引数はsplitから返されるフィールドの最大数を指定する。
予め返されるフィールドの数がわかっている場合は、数を指定することで、無駄な分割を防ぐことができる。
(例えば、splitの戻り値を受け取る変数が3つしかない場合、それ以上分割する必要はない)
ただし、注意するべきことは、
指定する第3引数は、取得したいフィールドの数+1を指定すること。
レコードから、「最初の3つ」のフィールドを取得するためには、レコードを4つに分割する必要がある。
複雑な可変幅のデータ
Text::CSV_XS、Text::CSV::Simple,Text::CSV
単純な文字列の並びならsplitで良いが、フォーマットルールは気まぐれに変化する。
これに対応するのは苦痛なので、モジュールを使用する。
Text::CSV_XSでは、フィールドセパレータ、エスケープ文字、フィールド引用符デリミタとして
使用される文字を指定し、フィールドから文字列を取得する。
Text::CSV::Simpleは、Text::CSV_XSのラッパーで、よりシンプルにモジュールを使用できる。
use Text::CSV::Simple; my $csv_format = Text::CSV::Simple->new({ sep_char => q{,}, #フィールドセパレータ escape_char => q{\\},#バックスラッシュのついた文字は常にデータ quote_char => q{"}, #フィールドは2重引用符で囲むことが可能 }); $csv_format->field_map( qw( ident sales price) ); #フィールドの順に名前を指定 #一度の呼び出しで、ファイル全体を読み込みハッシュリストに変換 #その後forでハッシュを使用してフィールド値を取得している。 for my $record_ref ($csv_format->read_file($sales_data)){ push @sales, { ident => translate_ID($record_ref->{ident}), sales => $record_ref->{sales} * 1000, price => $record_ref->{price}, }; }
Text::CSVはピュアPerlで実装されているので、コンパイル済みのモジュールを使用できない環境なら
こちらを使用する。
文字列の評価
eval
- 処理に時間がかかる
- コンパイル時に警告が生成されない
evalを使用したい状況の代表は、
「ユーザから渡された式に基づいて、新しいサブルーチンを作成する」こと。
匿名クロージャによるサブルーチンの作成
作成予定のサブルーチンの名前と、作成するサブルーチンの機能を選択するオプションとなる引数をサブルーチン作成用のサブルーチンに渡して、
そこで、オプションで指定した匿名サブルーチンを作成する。
そして、Sub::Installerを用いて、名前と匿名サブルーチンを呼び出し元の名前空間にインストールする。
これは構文に誤りがあればコンパイルエラーになる。
ソートの自動化
Sort::Makerによるソートルーチンの作成
いろいろなソート方法を実現できる。
引数を降順でソートするようなよく使う単純なソートも名前付き引数で指定できるので、
積極的に使うようにする。
部分文字列
以下の組み込み関数に値を代入するという奇妙な構文は、
substrで指定した部分文字列を右辺の文字列で置換する機能をもつ。
だた、これは理解しにくく、かつ処理が遅い。
substr($addr, $country_pos, $COUNTRY_LEN) = $country_name{$country_code};
perl5.6.1からはsubstrに第4引数が指定できるようになり、この引数で部分文字列の置換ができるようになった。
こちらの方が構文も自然で高速であるので、常にこちらを使用する。
ハッシュ値
ハッシュのvaluesはエイリアスなので、値を直接利用することができる。
ただし、perl5.6以前はできないので、インデックスキーを指定してアクセスする必要がある。
グロブ
入力演算(IOなど)ではない場合は、<>ではなく、グロブであることを明示するglob()を使用すること
my @files = <*.pl>;
はreadline処理と勘違いされやすいが、グロブである。
山括弧<>が入力演算の働きをするのは
- 空である場合<>
- 裸のワードだけ<DATA>
- 単純なスカラー変数<$input_file>
山括弧にそれ以外のものが含まれる場合は、ディレクトリ参照が実行される。
つまり、拡張子が*.plのファイル名のリストが返されることになる。
また、最悪なパターンとして、'*.pl'が定数として定義されてしまった場合、
スカラー変数を<>で囲むことになるので、グロブではなくなってしまう。
常に、
Readline my $FILE_PATTERN => '*.pl';
my @files = glob($FILE_PATTERN);
のようにする。
スリープ
組み込み関数sleepは整数の値しか対応していないが、selectの副作用を使ったスリープで1秒未満のスリープを実現しないこと。代わりにTime::HiRes::usleep関数を使用すること。
Time::HiRes::usleep関数を使用できない場合に、selectを使用する場合はサブルーチンでカプセル化すること。
ただし、サブルーチン呼び出しのオーバヘッドでわずかだか正確さに影響がでる。(気にしなくていい程度らしいが)
mapとgrep
mapとgrepの式は、常にブロック{}で囲むこと。そうしないと、後に続くリストと区別がつきにくくなる。
ユーティリティ 組み込みでない関数を使用する
次の3つのモジュールには便利な関数が複数存在する。
- Scalar::Util
- List::Util
- List::MoreUtils
詳しくは紹介しないけれど、first,reduce,apply,uniqなどがよく使いそう。
0 件のコメント:
コメントを投稿