ひがきの日記

2016-06-04

「ねこふん」が終わったら「にゃらまち」

ねこふん

猫ふんじゃったギャラリーたち」と題して、主に大阪ギャラリー、カフェ、雑貨店が猫にまつわる企画を毎年 5月に開催している。通称「ねこふん」

2016年は 25の企画が行われた。 *1

私も 21の企画に足を運んだ。 *2


にゃらまち

「にゃらまち猫祭り」と題して、主にならまちの (ry

毎年 6月に開催している。

どうやって巡るか計画を立てるべく web サイトを見てみたが、ねこふん ほど情報が見やすくない。

営業時間と定休日が一覧で見たい。

とりあえずスクレイピングしてみたが……

require 'open-uri'
require 'nokogiri'

def detail xml
  h1 = xml.xpath('//h1').first
  return if h1.nil?

  eenc = Encoding::default_external
  opts = {replace: ''}

  shop = h1.text.encode(eenc, opts)

  keywd = '営業時間'
  srv = xml.xpath(%|//p[contains(text(), "#{keywd.encode('UTF-8', opts)}")]|)
  srv = srv.nil? ? '' : "%s:%s -- %s:%s" % (srv.text.scan(/\d+/) + %w[00 00 00 00]) # !> too many arguments for format string

  keywd = '定休日'
  hol = xml.xpath(%|//p[contains(text(), "#{keywd.encode('UTF-8', opts)}")]|)
  hol = hol.nil? ? '' : hol.text.encode(eenc, opts).strip.sub(/#{keywd}[ ::]*/, '')

  [shop, srv, hol]
end

def more(uri)
  opts = {invalid: :replace, undef: :replace, replace: ''}
  puts uri.encode(Encoding::default_external, opts)

  fn = uri.split(/\//).last + ".html"
  fn.encode!(Encoding::default_external, opts)
  open(fn, 'w'){|f| f.write(open(URI.encode(uri), &:read))} unless File.exist? fn
  xml = Nokogiri::XML(open(fn))

  lm = xml.xpath('//a[contains(text(), "more")]') || []
  lM = xml.xpath('//a[contains(text(), "More")]') || []
  links = lm + lM
  links.map{|link| more(link.attributes['href'].value)} << detail(xml)
end

require 'pp'
pp more("http://www.nyaramachi.com/shop-list/")
# >> http://www.nyaramachi.com/shop-list/
# >> http://www.nyaramachi.com/shop-list/雑貨-ファッション/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/acartlifestyle/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/花羅古ろん/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/みに工房キャンバス/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/cloverand喫茶通圓/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/啓林堂書店奈良店/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/ことあかり洋品店/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/がまくち雑貨工房janji/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/doco-ao/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/ならまちわおん/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/necoco/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/猫雑貨ベゼル/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/marypoppinspochotte/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/器と郷土玩具瑜伽/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/アート-クラフトショップロビン/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/ab-cats/
# >> http://www.nyaramachi.com/
# >> http://www.nyaramachi.com/
# >> http://www.nyaramachi.com/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/猫雑貨とハンドメイドの店-cat-time/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/太古堂-ギャラリー-ショップ/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/アジアン雑貨-パーンファン/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/奈良のすごいタオル屋さん-ときどき猫/
# >> http://www.nyaramachi.com/shop-list/体験-1/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/着物あそびにっこり/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/美容室照美/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/遊悠工房zooandかぎひろ/
# >> http://www.nyaramachi.com/shop-list/カフェ-グルメ-1/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/うとうと/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/curryandcafe香炉里/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/四季折々の和菓子とらや/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/猫カフェ寧估庵/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/プティマルシェandぷちまるカフェ/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/franzkafkaフランツカフカ/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/喫茶工房まほろば/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/素人珈琲と雑貨よつばカフェ/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/若草カレー本舗/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/和彩rest紅絲/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/サブロク珈琲/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/甘いもん処-咲良/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/中華粥-穀雨/
# >> http://www.nyaramachi.com/shop-list/ギャラリー-1/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/ギャラリー一風/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/ギャルリサンク/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/スペース-プラス&#178;/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/
# >> http://www.nyaramachi.com/shop-list/カフェ-ギャラリー-1/galleryandcafeならまち村/
# >> [[[["ACART LIFESTYLE", "11:00 -- 19:00", "月曜(祝日の場合は翌火曜)"]],
# >>   [nil],
# >>   [["ミニ工房きゃんばす", "10:30 -- 19:00", "火曜"]],
# >>   [["CLOVER&珈琲館通圓", "14:00 -- 23:00", "不定休"]],
# >>   [["啓林堂書店奈良店", "9:00 -- 21:00", "1月1日、2日"]],
# >>   [["ことあかり洋品店", "11:00 -- 17:00", "月曜、火曜"]],
# >>   [["がまくち雑貨工房janji", "11:17 -- 00:00", " 不定休"]],
# >>   [["doco*ao", "13:00 -- 18:00", "月曜、火曜"]],
# >>   [["ならまちわおん", "10:00 -- 18:00", "木曜"]],
# >>   [["necoco", "11:00 -- 18:00", "月曜、木曜&#160;"]],
# >>   [["猫雑貨べセル", "10:00 -- 19:00", "月曜"]],
# >>   [["MaryPoppinsPochette", "10:30 -- 18:30", "木曜"]],
# >>   [["器と郷土玩具 瑜伽", "12:00 -- 18:00", "月曜"]],
# >>   [["アート&クラフトショップ ロビン", "11:00 -- 19:00", "木曜"]],
# >>   [["AB-CATS", "11:00 -- 18:00", "月&火曜"]],
# >>   [["にゃらまち猫祭り2016", "00:00 -- 00:00", ""]],
# >>   [["にゃらまち猫祭り2016", "00:00 -- 00:00", ""]],
# >>   [["にゃらまち猫祭り2016", "00:00 -- 00:00", ""]],
# >>   [nil],
# >>   [["太古堂", "13:00 -- 18:00", "月〜木曜 "]],
# >>   [["アジアン雑貨 パーンファン", "10:00 -- 19:00", " 木曜"]],
# >>   [["奈良のすごいタオル屋さんときどき猫", "11:00 -- 19:00", "不定休"]],
# >>   nil],
# >>  [[["着物あそび にっこり", "10:00 -- 00:00", "水曜&#160;"]],
# >>   [["美容室照実", "9:00 -- 18:00", "火曜、第3水曜日"]],
# >>   [["遊悠工房 ZOO&かぎろひ", "11:00 -- 17:00", "火曜、水曜"]],
# >>   nil],
# >>  [[["うと○うと(にゃらまち美術館)", "11:00 -- 17:00", "不定休"]],
# >>   [["Curry&Cafe 香炉里", "11:00 -- 20:00", "火曜"]],
# >>   [["四季折々の和菓子 とらや", "9:00 -- 20:30", "火曜"]],
# >>   [["猫カフェ寧估庵", "00:00 -- 00:00", ""]],
# >>   [["プティ・マルシェ&ぷちまるカフェ〜", "00:00 -- 00:00", "月曜、火曜"]],
# >>   [["Franz KAFKA フランツ・カフカ", "11:00 -- 21:00", "水曜"]],
# >>   [["喫茶工房まほろば", "11:00 -- 17:00", "木曜"]],
# >>   [["素人珈琲と雑貨 よつばカフェ", "11:00 -- 19:00", "水曜日 営業時間:11:00~19:00"]],
# >>   [["若草カレー本舗", "11:30 -- 15:00", "水曜日夜"]],
# >>   [["和彩Rest紅絲", "11:00 -- 22:30", "火曜"]],
# >>   [["サブロク珈琲", "00:00 -- 00:00", ""]],
# >>   [["甘いもん処 咲良", "10:00 -- 18:00", "不定休"]],
# >>   [["中華粥 穀雨", "00:00 -- 00:00", ""]],
# >>   nil],
# >>  [[["ギャラリー一風", "00:00 -- 00:00", "水曜"]],
# >>   [["ギャルリ・サンク", "12:00 -- 19:00", "不定休(期間中)"]],
# >>   [["スペースプラス&#178;", "00:00 -- 00:00", ""]],
# >>   nil],
# >>  [[["Gallery&Cafeならまち村", "11:00 -- 19:00", "火曜、水曜"]], nil],
# >>  ["Shop List", "00:00 -- 00:00", ""]]

力尽きた。

ページごとに構造がバラバラで Nokogiri 使うより、正規表現でゴリゴリやった方がよさそう。

*1阪神百貨店梅田本店を含む

*2:散財した

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 でも捕捉できるから。

2013-02-02

Ruby初級者向けレッスン 44回 ― ブロック ―

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

f:id:mas-higa:20130205231822p:image

繰り返し

a = [0, 1, 2]

a.each do |i|
  puts i
end

a.each{|i| puts i}

# >> 0
# >> 1
# >> 2

f:id:mas-higa:20130204232616p:image

  • ブロックの代表的な使い方は繰り返し処理。
  • do と end で囲まれたもの、{ と } で囲まれたものがブロック。
    • 上のコードと下のコードは同じ処理をしている。
  • Array のように、たくさんのオブジェクトを持っていると、全てのオブジェクトに繰り返し同じ処理をしたいことがよくある。
  • | と | で囲まれた変数 i が Array の各要素を順に指す。

便利な例

a = [0, 1, 2, 3]        # => [0, 1, 2, 3]

a.map{|i| i * i}        # => [0, 1, 4, 9]
a.select{|i| i.even?}   # => [0, 2]
a.inject{|s, i| s + i}  # => 6
a.find{|i| i.odd?}      # => 1
a.all?{|i| i.even?}     # => false
a.any?{|i| i.even?}     # => true

f:id:mas-higa:20130204232617p:image

  • 初級者には、このスライドがおすすめ。

f:id:mas-higa:20130204232620p:image


ブロックを渡す

  • メソッドには、ブロックをひとつ渡せる。
  • ブロックをどう使うかは、メソッド次第。
    • 繰り返し
    • ハリウッドの原理
open('hello.txt')   # => #<File:hello.txt>
open('hello.txt'){|f| f.read}
                    # => "こんにちは\n"

f:id:mas-higa:20130204232618p:image

  • ブロックの役割りは、繰り返しだけではない。
  • ブロックの有無で動作を変えるメソッドがある。

ハリウッドの原理

# open('hello.txt'){|f| f.read}

begin
  f = open('hello.txt')
  f.read
ensure
  f.close unless f.nil?
end

f:id:mas-higa:20130204232619p:image

  • 処理のスケルトンをメソッドで用意しておき、処理の一部をブロックで切り替える。
  • ブロック付きの 1行で書いた open メソッドは、ブロックなしで書くと、だいたいこんな感じ。
  • close の部分は f.close unless f.closed? かも。

ブロックのない open

f:id:mas-higa:20130206000309p:image

  • ファイルを扱う場合は、以下の処理が必要。
    1. open
    2. read/write など
    3. close
  • コードを書く際の負担が多い。

ブロック付き open

f:id:mas-higa:20130206000308p:image

  • open メソッドが忘れずに close してくれる。
  • コードを書く際の負担が少ない。

値を受け取る

f:id:mas-higa:20130204232621p:image


値を受け取る (2)

  • 受け取るか受け取らないかは、ブロック次第。
2.times{puts 'こんにちは'}
# >> こんにちは
# >> こんにちは

2.times{|i| puts i}
# >> 0
# >> 1

f:id:mas-higa:20130204232622p:image

  • 上の例は値を受け取っていない。
  • しかし times メソッドは値を渡してくれる。(下の例)
  • ブロックは必ずしも値を受け取る必要はない。

Hash の例

people = {matz: 47, dhh: 32}
            # => {:matz=>47, :dhh=>32}

people.each{|person| p person}
# >> [:matz, 47]
# >> [:dhh, 32]

f:id:mas-higa:20130204232623p:image

  • key と value のペアを受け取れる。
  • ふたつの値が、ひとつの Array オブジェクトに。

Hash の例 (2)

people = {matz: 47, dhh: 32}

people.each do |name, age|
  p "#{name}(#{age})"
end

# >> "matz(47)"
# >> "dhh(32)"

f:id:mas-higa:20130204232624p:image

  • ふたつの値を、それぞれ別の変数で受け取れる。
  • | と | の間に , で区切って変数を列挙する。

each_cons の例

midosuji = ["梅田", "淀屋橋", "本町", "心斎橋", "なんば"]

midosuji.each_cons(2){|path| p path}

# >> ["梅田", "淀屋橋"]
# >> ["淀屋橋", "本町"]
# >> ["本町", "心斎橋"]
# >> ["心斎橋", "なんば"]

f:id:mas-higa:20130204232625p:image


each_cons の例 (2)

midosuji.each_cons(2) do |from, to|
  p "#{from} - #{to}"
end

# >> "梅田 - 淀屋橋"
# >> "淀屋橋 - 本町"
# >> "本町 - 心斎橋"
# >> "心斎橋 - なんば"

f:id:mas-higa:20130204232626p:image

  • ふたつの値を、ふたつの変数で受け取る。

each_cons の例 (3)

a = [*0..3]     # => [0, 1, 2, 3]

a.each_cons(3){|i| p i}
# >> [0, 1, 2]
# >> [1, 2, 3]

a.each_cons(3){|i, j| p [i, j]}
# >> [0, 1]
# >> [1, 2]

f:id:mas-higa:20130204232627p:image

  • 繰り返しの回数は、どちらも 2回。
  • みっつの値を、ひとつの変数で受け取ると Array オブジェクトになる。(上)
  • みっつの値を、ふたつの変数で受け取ると、みっつ目の値が受け取れない。(下)

おかしいな? と思ったら

p unknowns.first
            # >> [1, ["matz", 47]]

unknowns.each do |id, (name, age)|
  id        # => 1
  name      # => "matz"
  age       # => 47
end

f:id:mas-higa:20130204232628p:image

  • 要素を、ひとつ取り出して見る。
  • 例えば、こんなふうに表示されたら、値は幾つか?
    • 答えは、ふたつ! *2
      1. 数値 (1)
      2. Array
        • 中身は、文字列 ("matz') と数値 (47)
  • みっつの変数で受け取るには Array の中の Array を ( ) で表記する。

ブロックを受け取るメソッド

  • こんな感じで呼びたい
monta{puts 'block!'}

# >> block!
# >> block!
# >> 大切なことなので

f:id:mas-higa:20130204232629p:image

  • monta というメソッドを作る。
    • ブロックを 2回評価して、
    • 最後に '大切なことなので' と出力する。

ブロックを受け取る方法は、ふたつある。


ブロックを受け取る

def monta
  yield
  yield
  puts '大切なことなので'
end

f:id:mas-higa:20130204232630p:image

  • yield メソッドで受け取ったブロックを評価する。

ブロックを受け取る (2)

def monta &block
  block.call
  block.call
  puts '大切なことなので'
end

f:id:mas-higa:20130204232631p:image

  • 引数でブロックを受け取る。
  • call メソッドでブロックを評価する。

値を渡す

  • monta メソッドの仕様を変更。
  • ブロックに '大切なことなので' という文字列を渡す。
def monta
  yield '大切なことなので'
  yield '大切なことなので'
end

monta{|i| puts "#{i} block!"}

# >> 大切なことなので block!
# >> 大切なことなので block!

f:id:mas-higa:20130204232632p:image

  • yield メソッドに文字列を渡すだけの簡単なお仕事です。

値を渡す (2)

  • また monta メソッドの仕様を変更。
  • ブロックに '大切な', 'ことなので' という、ふたつの文字列を渡す。
def monta &block
  block.call '大切な', 'ことなので'
  block.call ['大切な', 'ことなので']
end

monta{|i| puts "#{i} block!"}

# >> 大切な block!
# >> ["大切な", "ことなので"] block!

f:id:mas-higa:20130204232633p:image

  • block.call に、ふたつの文字列を渡すだけだと……
    • ブロックがひとつの変数で待ち受けていると、ふたつ目の文字列が受け取ってもらえない。
  • ふたつ以上の値を渡すときは、Array のオブジェクトにして渡す。

ブロックは Proc

block = Proc.new do |i, j|
  puts "#{i}#{j} block!"
end

monta &block

# >> 大切なことなので block!
# >> 大切なことなので block!

f:id:mas-higa:20130204232634p:image

  • あらかじめブロックだけ生成しておける。
  • メソッドに渡す際には & を付ける。

Q&A

Q1
ブロックをふたつ渡せないの?
A1
& なしで普通の引数としてなら渡せます。
Q2
さっきから Emacs で Ruby のコード実行してるけど、それなに?
A2
るびきちさんが作られた rcodetools を使ってます。Ruby を起動して実行結果を # => の後に埋め込んでくれます。(コード補完もできます) コメントなので、結果の埋め込まれたコードは、そのまま保存・実行できます。
Q3
どんなキーバインドなの?
A3
えっ、それ重要?

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

*2:こんなデータ構造が Array で渡ってきたら、データ構造の設計が間違ってる。

2012-07-23

演習問題回答例

演習問題1

属性として身長と体重を追加しよう。体重は秘密にしよう。

仕様を決める。

  • initialize で height, weight を渡す
  • 省略時は nil
class Person
  @@variables = [:@name, :@born, :@height, :@weight]

  def initialize name, born = nil, height = nil, weight = nil
    @name, @born, @height, @weight = name, born, height, weight
  end

  attr_accessor :height
  attr_writer :weight

  def hash
    @@variables.map{|var| instance_variable_get(var)}.hash
  end

  def eql? o
    @@variables.all? do |var|
      instance_variable_get(var).eql? o.instance_variable_get(var)
    end
  end
end

属性が増えたので、それに合わせて hash, eql? も変更した。


演習問題2

BMI を計算するメソッドを追加しよう。
BMI = ¥frac{w}{t^2}
w = 体重[kg]
t = 身長[m]

仕様を決める。

  • height は Float で、単位は cm
  • weight は Float で、単位は kg
  • @height, @weight は Float でなくても、あるていど動くようにする
  • 計算できない場合は NaN を返す
class Person
  def bmi
    Float(@weight) / (Float(@height) / 100.0) ** 2
  rescue
    Float::NAN
  end
end

演習問題3

Person#<=> を書き直そう。
その妥当な仕様は?
p0 = Person.new('matz')
p1 = Person.new('Matz', Time.local(1965, 4, 14))

p0 <=> p1   # => ?

仕様を決める。

  • 単純に全ての属性に対して <=> を評価する
  • 比較対象が Person のインスタンスでなければ nil を返す
class Person
  def <=> o
    return nil unless o.kind_of? Person

    @@variables.each do |var|
      lhs = instance_variable_get(var)
      cmp = lhs <=> o.instance_variable_get(var)

      next if cmp == 0

      return cmp unless cmp.nil?
      return  -1 if lhs.nil?
      return   1
    end

    0
  end
end

演習問題4

クラス Person には、いくつかのバグがある。 それを見つけ出して修正しよう。

バグを探す。

  • @born に strftime メソッドがないと age で例外が発生する
    • 計算できなければ nil を返す
  • to_s が age を使用しているので、以下同文
    • age が nil なら、@name のみとする
class Person
  def age
    (Time.now.strftime('%Y%m%d').to_i -
     @born.strftime('%Y%m%d').to_i) / 1_00_00
  rescue
    nil
  end

  def to_s
    if a = age
      "#{name}(#{a})"
    else
      name
    end
  end
end

こればバグか?

matz = Person.new('matz')
matz.freeze
matz.name                       # => "matz"

matz.name.upcase!
matz.name                       # => "MATZ"

グループワークのときに質問してみたら、みんなこれは放置しているそうだ。

あえて対応するなら、

class Person
  def name
    @name.dup
  end
end

matz.name.upcase!       # => "MATZ"
matz.name               # => "matz"

間違いがあれば、指摘くださると助かります。

2012-07-21

第55回 Ruby/Rails 勉強会@関西で初級者向けレッスンやってきた

初級者向けレッスンを担当したので、以下スライドを解説。*1 *2

クラスとは

f:id:mas-higa:20120722174048p:image

クラスを作ってみよう

class Person; end

obj = Person.new
  # => #<Person:0x10102718>

obj.class           # => Person
Person.superclass   # => Object

f:id:mas-higa:20120722174047p:image

  • Person という名前のクラスを作る。
  • class から end までがクラス。
  • クラス名は大文字で始める (キャメルケース)

属性を持たせてみよう

class Person
  def initialize name
    @name = name
  end
end

matz = Person.new('matz')
  # => #<Person:0x10138598 @name="matz">

f:id:mas-higa:20120722174045p:image

属性にアクセスしてみよう

class Person
  attr_reader :name
end

matz.name       # => "matz"

f:id:mas-higa:20120722174044p:image

attr_writer
セッター
attr_reader
ゲッター
attr_accessor
セッター&ゲッター

変数・定数のおさらい

ローカル変数
person
インスタンス変数
@person
クラス変数
@@person
グローバル変数
$person
定数
Person

f:id:mas-higa:20120722174042p:image

  • 最初の数文字を見れば区別できる
  • クラス名は定数

属性を増やしてみよう

class Person
  def initialize name, born = nil
    @name, @born = name, born
  end
  attr_accessor :born
end

matz.methods.map(&:to_s).grep(/born/)
  # => ["born", "born="]

f:id:mas-higa:20120722173746p:image

属性を増やしてみよう (2)

  • アクセスしてみる
matz.born = Time.local(1965, 4, 14)
dhh = Person.new('dhh',
        Time.local(1979, 10, 15))

matz.born
  # => 1965-04-14 00:00:00 +0900
dhh.born
  # => 1979-10-15 00:00:00 +0900

f:id:mas-higa:20120722173744p:image

  • 代入のような表記で born= メソッドが呼ばれる

メソッドを作ってみよう

class Person
  def age
    (Time.now.strftime('%Y%m%d').to_i -
     @born.strftime('%Y%m%d').to_i) /
      10000
  end
end
matz.age    # => 47
dhh.age     # => 32

f:id:mas-higa:20120722173743p:image

  • メソッド名は小文字で始める (スネークケース)
  • return は省略可能
    • 最後に評価した式の値がメソッドの値になる

メソッドを上書きしてみよう

matz.to_s  # => "#<Person:0x10138598>"

class Person
  def to_s
    "#{@name}(#{age})"
  end
end
matz.to_s  # => "matz(47)"
dhh.to_s   # => "dhh(32)"

f:id:mas-higa:20120722173741p:image

インスタンスを比較してみると……

person = Marshal.load(Marshal.dump matz)

person == dhh   # => false
person == matz  # => false # おかしい

f:id:mas-higa:20120722173739p:image

  • 深いコピー
    • Marshal.dump して Marshal.load する
  • == はメソッド
    • Person#== を定義しても良いが……
    • ==, !=, >, <, >=, <= 別々に定義するの面倒

順序を決めよう

class Person
  include Comparable
  def <=> o
    @name <=> o.name
  end
end
person == matz  # => true
person == dhh   # => false
matz > dhh      # => true

f:id:mas-higa:20120722173622p:image

  • <=> 演算子で順序を決定
  • Comparable で ==, != , >, <, >=, <= を生成

Array#sort してみよう

  • 順序が決まれば sort できる
people = [matz, dhh]

people.sort  # => [dhh(32), matz(47)]

f:id:mas-higa:20120722173621p:image

Array ときたら、次は ……

Hash のキーにしてみると……

値に入れてもおもしろくないので

h = {matz => "Ruby", dhh => "Rails"}

h[matz]     # => "Ruby"
h[dhh]      # => "Rails"

key = Marshal.load(Marshal.dump matz)

key == matz # => true
h[key]      # => nil # おかしい

f:id:mas-higa:20120722173619p:image

  • 深いコピーをしたオブジェクトをキーにすると
    • 値が取り出せない!

hash 値を計算しよう

  • ハッシュアルゴリズム
class Person
  def hash
    code = 17
    code = 37 * code + @name.hash
    code = 37 * code + @born.hash
  end
end
matz.hash   # => -22068619118
dhh.hash    # => 14923733106

f:id:mas-higa:20120722173617p:image

  • 計算方法はハチドリ本に書いてあった*4
    • 出所は Effective Java らしい

eql? を上書きしよう

  • hash 値がぶつかっていないか調べる必要がある
  • Hash クラスは eql? でキーが正しいか判断する
class Person
  def eql? o
    return false unless @name.eql? o.name
    return false unless @born.eql? o.born
    true
  end
end
key.eql? matz   # => true
key.eql? dhh    # => false

f:id:mas-higa:20120722173616p:image

  • a.eql? b が真の場合 a.hash == b.hash であること
    • そのように hash, eql? を定義する

Hash にアクセスしてみよう

h = {matz => "Ruby", dhh => "Rails"}

h[matz]     # => "Ruby"
h[dhh]      # => "Rails"

h[key]      # => "Ruby"

f:id:mas-higa:20120722173429p:image

  • 最初に作った h (Hash) は Person#hash を定義する前に作成したもの
  • 格納位置がおかしいので作り直す*5

等値性のおさらい

==
内容が等しいか?
===
case 式で使用
eql?
Hash クラスが使用
equal?
同一オブジェクトか?

f:id:mas-higa:20120722173428p:image

アクセス制御してみよう

オマケ

  • public
  • protected
  • private
class Person
  protected :born
end
matz.born
# ~> protected method `born' called for matz(47):Person (NoMethodError)

f:id:mas-higa:20120722173426p:image

  • protected なメソッドを呼ぶと例外が発生する

今日 話さなかったこと

  • Range の始点と終点

f:id:mas-higa:20120722173425p:image

上記を調べて初級者を抜け出そう。

まとめ

f:id:mas-higa:20120722173423p:image

以上、クラスを作る際の決まり事を紹介した。

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

*2:コードが必要な人は gist からどうぞ。

*3Kernel モジュールで定義されている

*4:会場の @nayutaya さんによると [@name, @born].hash とするのが一般的らしい

*5:会場の @no6v さんによると h.rehash するといいらしい