Ruby: ブロックを使う
Rubyのブロックで一番使われているのは eachメソッド系だろうと思いますが以下のような使われ方をします。 (以下のコードはruby 2.0 で確認しています。)
(1..10).each{|x| p x}
このコードは (1..10)で1から10までの要素をもつ配列を、 each でその要素を1個づつブロックに渡して処理(画面に出力)をします。
{ } でかこまれだ部分がブロックです。|x| で受け取る引数名を指定しブロックの中で使います。 引数の数はメソッドがどう定義されているかに依存します。 ここでのeachメソッドはひとつだけ値を渡しますので引数をひとつだけ受け取ります。 例えば配列のeachは以下と同等に定義されています。
def each() i = 0 while i < self.size yield self[i] i += 1 end end
先頭から順に配列要素を取出してひとつづつ yield に渡しています。このyieldの部分に ブロックのコードが入る感じになります。先の例だとこんな感じです。
def each() i = 0 while i < self.size yield self[i] # → {|x| p x} xにself[i]を受け取る # → p self[i] i += 1 end end
このようにブロックを使うと共通に使える一連の前処理や後処理を隠してしまって 異なる部分だけに注目したブロック部分だけ書けば良いようにすることが出来ます。
言い換えると、同じパターンのコードを毎回書かなくても済みます。
引数の個数はyield で渡される引数の分だけ、ブロックの先頭で|p1,p2,..|と定義して受け取れば良いわけです。 yieldの場合、数が違ってもエラーと成りませんので注意が必要です。
Ruby: ブロックの使用例1, 文字列の処理
ブロックを使う処理を書いてみましょう。ここではカンマで区切られた文字列の要素を取出して 処理する例題をあげてみます。
#! ruby -EUTF-8 # -*- mode:ruby; coding:utf-8 -*- def split_and_count(str) dt = str.split(/,/).inject(Hash.new(0)){|hash, a| hash[a.strip] += 1; hash} if block_given? yield dt end dt end if $0 == __FILE__ split_and_count("abc,1,2,3,2,3,3,xyz,xyz"){|dt| dt.sort.each{|k,v| print "#{'%-5s'%k}:#{'%4d'%v} 回\n".encode("CP932")} } split_and_count("abc,1,2,3,2,3,3,xyz,xyz,なんとか,かんとか,なんとか"){|dt| print "2回以上の文字列 = ".encode("CP932") print (dt.sort.map{|x,v| x if v >= 2}.compact.join(',')+"\n").encode("CP932") } end
まず、ruby 2.0のデフォルト文字コードはUTF-8に成りました。 特に指定しなければUTF-8と成りますのでWindowsで利用する場合には表示等で SHIFT-JIS(CP932)でないと文字化けする場合がありますので注意して下さい。 文字コードを意識するためにも先頭でコードの指定をするようにした方が良いでしょう。
def split_and_count(str) では引数としてstrを取るとだけ指定していますが この様に定義したメソッドにもブロックを渡すことが出来ます。
dt = str.split(/,/).inject(Hash.new(0)){|hash, a| hash[a.strip] += 1; hash}
では渡された文字列をカンマで分解した文字列の配列を作り、
各要素の前後の空白を無くした文字列(a.strip)をキーとするハッシュを作り、
個数をカウントアップし、結果のハッシュを dt に入れています。
Hash.new(0)と初期値を0と明示的に指定することで、 新規のキーの時にnilへの加算となることを防いでいます。
if block_given?
はブロックが与えられたかを検証するコードです。ブロックが与えられた時だけ
yield dt を呼び出します。
最後のdt行でこのメソッドの戻り値として作ったハッシュを返しています。
if $0 == __FILE__
の行は、このプログラムファイルが直接実行されたかの判定です。
直接実行されたときだけtrueとなり以下の行を実行します。
require 等で組み込まれたときには以下の行を実行しません。
ファイル内に組み込みのテストコードを書く時の常套手段です。
split_and_count("abc,1,2,3,2,3,3,xyz,xyz"){|dt|
は文字列を渡して、ブロックにはカンマで分解した文字列の個数のハッシュを dt として受け取っています。
dt.sort.each{|k,v| print "#{'%-5s'%k}:#{'%4d'%v} 回\n".encode("CP932")}
はブロックの中のコードです。 dt の各要素を出力しています。
encode("CP932")で文字コードをWindows用に変更しています。 プログラムをCMD.exeのコンソールで実行する場合には UTF-8で出力しても自動的に変換されて正常に表示されますが、 RDE等で見ると文字化けします。
split_and_count("abc,1,2,3,2,3,3,xyz,xyz,なんとか,かんとか,なんとか"){|dt|
print "2回以上の文字列 = ".encode("CP932")
print (dt.sort.map{|x,v| x if v >= 2}.compact.join(',')+"\n").encode("CP932")
の部分は、同じsplit_and_countを使って出現頻度が2回以上の文字列を出力します。
この様にsplit_and_countで分解した文字列のハッシュに対して、異なる処理を簡単に 実行させることが出来ます。
Ruby: 関数オブジェクトの利用
ブロックとして渡せるのはひとつだけですが、 ブロックを関数オブジェクトとして受け渡すと、 複数の処理パートを受け渡すことが出来ます。
下記に指定されたディレクトリ以下のサブディレクトリを全部辿って、 ディレクトリ毎の処理(dir_proc)、ファイル毎の処理(file_proc)、サブディレクトリ単位での処理(end_proc)を 実行する例(visit_subdir)をあげます。
下記の呼び出し例は指定されたディレクトリ以下のファイルのサイズの合計を計算します。
#! ruby -EUTF-8 # -*- mode:ruby; coding:utf-8 -*- class VisitAllDir attr_accessor :max_level,:deep_dirs,:local_stack,:values,:level_max attr_accessor :show_error def initialize dir @max_level = 10 ## ネストが深い時、深さ制限する値 @local_stack = [] ## サブディレクトリ単位の処理で使用する作業用変数のスタック @values = nil ## サブディレクトリ単位の処理で使用する作業用変数の配列又はnil @too_deep = [] ## 深さ制限を越えたディレクトリパスの配列 @level_max = 0 ## 最も深い階層数 @show_error = true ## エラーが発生した時に表示するかしないか end def visit_subdir(dir, level, dir_proc, file_proc, end_proc) @level_max = level if level > @level_max if level >= @max_level @too_deep << dir else begin Dir.foreach(dir) do |f| full_path = '' if (f != '.' and f != '..') if dir == File::Separator full_path = dir + f else full_path = dir + File::Separator + f end if File.directory?( full_path) @local_stack.push(@values) @values = nil dir_proc.call(full_path) if dir_proc visit_subdir(full_path, level+1, dir_proc, file_proc, end_proc) @values = @local_stack.pop else file_proc.call(full_path) if file_proc end end end rescue =>err print "#{err}\n" if @show_error end end_proc.call(dir) if end_proc end end def visit_all(dir, level, dir_proc, file_proc, end_proc) @local_stack = [] @values = nil @too_deep = [] @values = [0,0] dir_proc.call(dir) if dir_proc visit_subdir(dir, level, dir_proc, file_proc, end_proc) end def dump_too_deep print "\n\n----- Too deep subdirs, nest over #{@max_level} level -------\n" n = 1 @too_deep.each do |dir| printf "%10d :%s\n",n,dir n += 1 end end end if $0 == __FILE__ STDOUT.sync=true open('work.txt','w') do |wf| dir = 'C:\Documents and Settings\All Users' # 適当なディレクトリ名に変更してください。 v = VisitAllDir.new(dir) dir_proc = proc do |dir| v.values = [0,0] if v.values == nil wf.print "-- #{dir}\n" end file_proc = proc do |file| v.values = [0,0] if v.values == nil v.values[0] += File.size(file) end end_proc = proc do |dir| if v.values printf "%10d,%10d : %s\n",v.values[0],v.values[1],dir v.local_stack.last[1] += (v.values[0] + v.values[1]) if v.local_stack.last end end print "ThisFolder,SubFolders : Path\n" v.visit_all(dir, 0,dir_proc,file_proc,end_proc) print "Total = #{v.values[0] + v.values[1]}\n" # v.dump_too_deep print "\n\n level max = #{v.level_max}\n" end end
今回は機能をクラスとして作っています。
def visit_subdir(dir, level, dir_proc, file_proc, end_proc)
が注目しているメソッドです。引数として明示的にdir_proc,file_proc,end_procと
言う名前で関数オブジェクトを受け取ります。
(注:引数に型の指定はされていません)
begin Dir.foreach(dir) do |f|
beginでエラーをハンドリングできるようにして、Dir.foreach でサブディレクトリや ファイルを順に取出して処理します。
if File.directory?( full_path) @local_stack.push(@values) @values = nil dir_proc.call(full_path) if dir_proc visit_subdir(full_path, level+1, dir_proc, file_proc, end_proc) @values = @local_stack.pop else file_proc.call(full_path) if file_proc end
ディレクトリなら現在使用している作業用変数をスタックに積み、 nilにすることでサブディレクトリ単位の作業用変数を管理します。
dir_proc.call(full_path) if dir_proc
でdir_procが渡されていればディレクトリ名を渡して呼び出します。
サブディレクトリの処理から戻ってきたら作業用変数をスタックから戻します。
file_proc.call(full_path) if file_proc
ファイルだったら file_procを呼び出します。
begin Dir.foreach(dir) do |f| : end rescue =>err print "#{err}\n" if @show_error end end_proc.call(dir) if end_proc
rescueでエラーが起こったときに必要ならエラーを表示させて、処理を中止させます。
Dir.foreachが終わったらend_procを呼び出しています。
STDOUT.sync=true
これは出力をバッファリングせずに直ぐに出力させるためのおまじないです。
open('work.txt','w') do |wf| : dir_proc = proc do |dir| v.values = [0,0] if v.values == nil wf.print "-- #{dir}\n" end
dir_proc = proc do |dir| で関数オブジェクトを作成していますが 関数オブジェクトは作成された時に利用可能な変数等の内容を利用可能です。 ここではopenで書込み用に作成した wf をブロックの中で出力用に使用しています。 実際に関数オブジェクトが呼び出される時に、このwfを使って出力されます。
file_proc = proc do |file| v.values = [0,0] if v.values == nil v.values[0] += File.size(file) end end_proc = proc do |dir| if v.values printf "%10d,%10d : %s\n",v.values[0],v.values[1],dir v.local_stack.last[1] += (v.values[0] + v.values[1]) if v.local_stack.last end end
file_procではファイルのサイズをvalues[0]に足しこみ、 end_procでスタック上のひとつ上のディレクトリ用のvalues[1]の所に足し込んでいます。
values[ ]の意味は実現したい機能に応じて自由に意味付けすることが可能です。
呼び出し部分を下記の様にするとディレクトリの深さが分ります。
if $0 == __FILE__ v = VisitAllDir.new(dir) v.max_level = 1000 v.show_error = false v.visit_all('c:', 0,nil,nil,nil) ## 適当なディレクトリ名に変更してください。 print "level max = #{v.level_max}\n" end
Excelマクロの操作 ← : ブロックを使う : → Seleniumの操作
お勧めのRuby開発環境
Trail4You 仮想マシンバザール : Ruby統合開発環境仮想マシン上にruby統合開発環境をインストールしてあります。 rvm, git もインストール済みで各種rubyを切替ながら試せます。