ひがきの日記

2013-04-13

Ruby初級者向けレッスン 45回 ─例外─

第57回 Ruby/Rails勉強会@関西での初級者向けレッスンの資料を公開します。*1

f:id:mas-higa:20130416224751p:image:w360

エラーメッセージ

require 'open-uri'
open 'http://github.com/rubykansai/workshops/wiki'
.../open-uri.rb:223:in `open_loop': redirection forbidden: http://github.com/rubykansai/workshops/wiki -> https://github.com/rubykansai/workshops/wiki (RuntimeError)
        from .../open-uri.rb:149:in `open_uri'
        from .../open-uri.rb:688:in `open'
        from .../open-uri.rb:34:in `open'
        from ex.rb:2:in `<main>'

f:id:mas-higa:20130416224727p:image:w360

  • 「...(RuntimeError)」までが 1行
  • 後は「from ...」から行末までが 1行
  • 最初の行がエラーの出た場所。
    • 「in `open_loop'」は open_loop メソッドでエラーが出たという意味。
    • 「.../open-uri.rb:223」は open-uri.rb の 223行目でエラーが出たという意味。
      • えっ、でも open_loop なんて見覚えないよ!?
  • 最後の行が呼び出し元。
  • 下から 2行目「in `open'」は <main> から open メソッドが呼ばれたという意味。
  • 結局、英語のメッセージを読む必要がある ><
    • 「redirection forbidden」理由は分からんが http から https へのリダイレクトが禁止されてるらしい。
  • 最初の行の行末「(RuntimeError)」は RuntimeError 例外が発生したという意味。

エラーメッセージと例外

f:id:mas-higa:20130416224703p:image:w360

  • 例外が起きたら、なんらかの対処をしないと、プログラムが異常終了する。

いろいろな例外と、そのエラーメッセージを見て、例外に慣れよう。

例外いろいろ TypeError

"1" + 1

# ~> ex.rb:1:in `+': can't convert Fixnum into String (TypeError)
# ~> 	from ex.rb:1:in `<main>'

f:id:mas-higa:20130416224639p:image:w360

例外いろいろ NoMethodError

1.to_sym

# ~> ex.rb:1:in `<main>': undefined method `to_sym' for 1:Fixnum (NoMethodError)

f:id:mas-higa:20130416224610p:image:w360

例外いろいろ NameError

n.times{puts 'Ruby!'}

# ~> ex.rb:1:in `<main>': undefined local variable or method `n' for main:Object (NameError)

f:id:mas-higa:20130416224539p:image:w360

例外いろいろ NoMethodError (再び)

n = ARGV.first.to_i unless ARGV.empty?
n.times{puts 'Ruby!'}

# ~> ex.rb:2:in `<main>': undefined method `times' for nil:NilClass (NoMethodError)

f:id:mas-higa:20130416224513p:image:w360

  • Rubyには、他の言語のように NullPointerException のようなものはない。

例外いろいろ Errno::ENOENT

open('nothing.txt')

# ~> ex.rb:1:in `initialize': No such file or directory - nothing.txt (Errno::ENOENT)
# ~> 	from ex.rb:1:in `open'
# ~> 	from ex.rb:1:in `<main>'

f:id:mas-higa:20130416224446p:image:w360

  • 存在しないファイルを open しようとした。
  • 「in `initialize'」は File.open メソッドが Fileクラスのインスタンスを生成しようとしている。
  • Errno::Exxx の例外は OS の errno に対応した例外。

例外いろいろ SyntaxError

1 def even?(n)
2   if n % 2 == 0
3     true
4   else
5     false
6 end
7 
8 puts even?(0)
9 puts even?(1)

# ~> -:9: syntax error, unexpected end-of-input, expecting keyword_end

f:id:mas-higa:20130416224423p:image:w360

  • if-else-end の end を忘れている。
  • Ruby のパーサがスクリプトを最後まで読んだが、end が見つからなかった。
  • end を忘れたのは 5行目と 6行目の間だが、例外は最後で出るので注意。

Intelligence と Wisdom

雨が降ってきて…
  • Intelligence
    • 雨だ!
  • Wisdom
    • 傘を差そう
    • 雨宿りしよう

f:id:mas-higa:20130416224357p:image:w360

雨が降ってきたときに、

  • それが雨だと認識できる ─── Intelligence (知識)
  • 濡れないためには、傘を差すか、雨宿りしないと ─── Wisdom (知恵)

例外も同じで、

  • エラーを検知したものが例外を起こす (Intelligence)
  • 対処方法を知っているものが例外を捕捉する (Wisdom)

例えば、アプリケーションがファイルを読もうとして、

  • open できない!
  • read できない!

File ライブラリはエラーを検知できる。(Intelligence)

  • 既定値を使うから、読めなくてもよい。
  • 別のファイルから読みなおしたい。
  • etc...

これはアプリケーションの仕様。(Wisdom)
ライブラリには分かりようがない。

例外を捕捉する (コード例1)

files = %w[file.txt file1.txt file2.txt]

files.each do |fn|
  begin
    open(fn, 'w'){|f| f.puts "Ruby!!"}
    break
  rescue => ex
    $stderr.puts "#{ex} (#{ex.class})"
  end
end

f:id:mas-higa:20130416224328p:image:w360

やりたいこと

  • ファイルに "Ruby!!" と書き込みたい。
  • ファイル名は file.txt
    • もし書けなければ、代わりに file1.txt に書こう。
    • それでも書けなければ、file2.txt に書こう。
    • それでダメなら諦める。

コード解説

  • ファイル名 "file.txt", "file1.txt", "file2.txt" という Array を用意。
  • 各ファイル名に対して処理を繰り返す。─── files.each ...
    • begin - end で例外を捕捉する準備。
    • ファイルに "Ruby!!" を書き込む。─── open( ...
      • 例外が起きると、rescue節に処理が移る。
      • 変数 ex が例外オブジェクトを指す。
      • ここでは、標準エラー出力に例外情報を出力するだけ。
      • 最初の繰り返しに戻る。─── files.each ...
    • 次のファイル名で open & puts に再挑戦。
      • 例外が起きなければ break で繰り返しを脱出する。

やってみる

$ ls -Fdl file*
drwxr-xr-x  2 mas  staff  68  4 12 23:37 file.txt/
-r--r--r--  1 mas  staff   0  4 12 23:37 file1.txt
# file.txt はディレクトリ
# file1.txt は書き込み権なし

$ ruby retry.rb
Is a directory - file.txt (Errno::EISDIR)
Permission denied - file1.txt (Errno::EACCES)

$ ls -Fdl file*
drwxr-xr-x  2 mas  staff  68  4 12 23:37 file.txt/
-r--r--r--  1 mas  staff   0  4 12 23:37 file1.txt
-rw-r--r--  1 mas  staff   7  4 12 23:39 file2.txt
# file2.txt ができた!

$ cat file2.txt 
Ruby!!

例外を捕捉する

begin
  式1…
[rescue [型1[, 型2]…][=> 変数][then]
  式2…]…
[else
  式3…]
[ensure
  式4…]
end

f:id:mas-higa:20130416224258p:image:w360

  • [ ... ] 内は省略可能。
  • begin - end で例外を捕捉する準備。
  • 式1 が例外を捕捉したい処理 (複数書ける)
  • rescue節で例外を捕捉。(rescue は複数書ける)
    • 「型」に例外クラスを指定すると、特定の例外だけを捕捉できる。(型は複数書ける)
    • 式2 が例外対処のための処理 (複数書ける)
    • 発生した例外に対応する rescue節がなければ、捕捉できない。
  • ensure節は begin - end のブロックを抜ける際の後始末。
    • 例外が起きても、起きなくても実行される。
    • 式4 が後始末の処理 (複数書ける)
  • else節は、上記 式1 で例外が起きなかった際に実行される。
    • rescue節で指定しなかった例外を捕捉する処理ではない。
    • 式3 で起きた例外は、このブロックでは捕捉しない。

上記のコードを省略なしで書くと、3通りの実行パターンがある。

  • 式1→式3→式4 (例外なし)
  • 式1→式2→式4 (例外を捕捉)
  • 式1→式4 (例外が発生するが、捕捉できない)

例外を捕捉する (コード例2)

require './factorial'

def fact(n)
  n.factorial
rescue ArgumentError
  "1以上の整数を指定してください"
rescue NoMethodError
  "整数を指定してください"
end

fact 3      # => 6
fact 4      # => 24
fact 5      # => 120
fact 0      # => "1以上の整数を指定してください"
fact 2.5    # => "整数を指定してください"
fact "2"    # => "整数を指定してください"

f:id:mas-higa:20130416224157p:image:w360

  • メソッド定義の def - end にも rescue, else, ensure が書ける。*4

例外の種類

puts NoMethodError.ancestors
# >> NoMethodError
# >> NameError
# >> StandardError
# >> Exception
# >> Object
# >> Kernel
# >> BasicObject

f:id:mas-higa:20130416224130p:image:w360

rescue修飾子

require './factorial'

3.factorial   rescue 0  # => 6
4.factorial   rescue 0  # => 24
5.factorial   rescue 0  # => 120
0.factorial   rescue 0  # => 0
2.5.factorial rescue 0  # => 0
"2".factorial rescue 0  # => 0

f:id:mas-higa:20130416224057p:image:w360

  • rescue の後置。
    • まず rescue の左辺を評価。
    • 例外が起きると rescue の右辺を評価。
  • 例外が起きたら、結果は 0 でいい。という大雑把なコード。

例外を起こす

raise "simple"

# ~> ex.rb:1:in `<main>': simple (RuntimeError)

f:id:mas-higa:20130416224018p:image:w360

  • 例外を起こすには Kernel#raise メソッドを使う。
  • RuntimeError が発生する。

例外を起こす (クラス指定)

raise ArgumentError, "bad argument"

# ~> ex.rb:1:in `<main>': bad argument (ArgumentError)

f:id:mas-higa:20130416223946p:image:w360

  • 例外クラスを指定して raise できる。

例外を起こす (オブジェクト)

raise TypeError.new("can't convert...")

# ~> ex.rb:1:in `<main>': can't convert... (TypeError)

f:id:mas-higa:20130416223843p:image:w360

例外を起こす (独自の例外クラス)

class MyError < StandardError; end

raise MyError, 'original'

# ~> ex.rb:3:in `<main>': original (MyError)

f:id:mas-higa:20130416223758p:image:w360

  • 独自の例外クラスを作れる。*6

サンプルコード

スライド、コードに間違いがあれば、ご指摘ください。

*1:スライドだけ欲しい人は直接どうぞ http://higaki-it.jp/ruby/57/print.pdf

*2スタックトレースとかバックトレースとか言う。

*3:あれ? 今実行したらメッセージ違う!?

*4:else節の評価結果はメソッドの評価結果となるが、ensure節の評価結果はメソッドの評価結果とはならない。

*5:例外でないオブジェクトを指定すると TypeError が発生する。えっ?

*6エンタープライズだと独自の例外じゃないと起こせないことあるよね。

*7:StandardError なら、クラス指定なしの rescue でも捕捉できるから。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/mas-higa/20130413/1366381564
リンク元
Connection: close