おもこん

おもこんは「思いつくままにコンピュターの話し」の省略形です

徒然Ruby(14)サブクラス

クラスの親子関係

Rubyのクラスには親子関係があります。 あるクラスの子をサブクラスといいます。 サブクラスは複数作ることができますが、親クラス(スーパークラス)を子から作ることはできません。 したがって、この親子関係はツリー状になります。

すべてのクラスの祖先はBasicObjectというクラスです。 あるクラスがどういう祖先を持っているかはancestorsメソッドで分かります。

p String.ancestors

これを実行すると

[String, Comparable, Object, Kernel, BasicObject]

と表示されます。 左から右へ「子から親」の順に並んでいます。 このうちComparableとKernelはクラスではなくモジュールです。 モジュールについては別の記事で説明します。 クラスだけの親子で言えば

String => Object => BasicObject

ということになります。 以下では子クラスを「サブクラス」、親クラスを「スーパークラス」と呼びます。

サブクラスはスーパークラスの性質、例えばメソッド、インスタンス変数を受け継ぎます。

親から子になるにつれ、より個別的、具体的なクラスになっていきます。

サブクラスの定義

サブクラスの定義は<をクラス名の後につけ、さらにスーパークラスを置きます。

class A < B
end

この例ではAがBのサブクラス、BがAのスーパークラスです。 BはRubyが既に定義しているクラスでも、ユーザが新たに定義したクラスでも構いません。

サンプルデータの作成

ここではサブクラスの例として統計のサンプルデータの集合を表すクラスを作ってみましょう。

今回は都道府県別人口のデータを処理するクラスを作ってみます。 データは独立行政法人統計センターのSSDSE(教育用標準データセット)から取ってきました。

2022/9/22の時点では「SSDSE-E-2022v2」(2022年第2版)が最新です。

データの中から都道府県名と人口をセットにして、ヒア・ドキュメントの文字列にしました。

北海道 5224614
青森県 1237984
... ...

都道府県と人口の間はタブで区切られています。 このデータをハッシュにして取り出すには

  • 改行を区切りとして各行を要素とする配列にする
  • 各要素をタブを区切りとして、都道府県名と人口の2要素の配列にする
  • その2要素の配列を文字列からシンボル、整数に変更する
  • 配列をハッシュに変換する

それぞれの項目はメソッドで実現できます。 全体として連続したメソッドになりますが、これを「メソッド・チェーン」ということもあります。 ここまでをプログラムにしましょう。 少し長くなりますが、全都道府県の人口データもプログラムリストに入れておきます。

d = <<EOS
北海道 5224614
青森県 1237984
岩手県 1210534
宮城県 2301996
秋田県 959502
山形県 1068027
福島県 1833152
茨城県 2867009
栃木県 1933146
群馬県 1939110
埼玉県 7344765
千葉県 6284480
東京都 14047594
神奈川県  9237337
新潟県 2201272
富山県 1034814
石川県 1132526
福井県 766863
山梨県 809974
長野県 2048011
岐阜県 1978742
静岡県 3633202
愛知県 7542415
三重県 1770254
滋賀県 1413610
京都府 2578087
大阪府 8837685
兵庫県 5465002
奈良県 1324473
和歌山県  922584
鳥取県 553407
島根県 671126
岡山県 1888432
広島県 2799702
山口県 1342059
徳島県 719559
香川県 950244
愛媛県 1334841
高知県 691527
福岡県 5135214
佐賀県 811442
長崎県 1312317
熊本県 1738301
大分県 1123852
宮崎県 1069576
鹿児島県  1588256
沖縄県 1467480
EOS

d = d.split(/\n/)\
     .map{|s| s.split(/\t/)}\
     .map{|a| [a[0].to_sym, a[1].to_i]}\
     .to_h

最後から2〜4行目の最後にバックスラッシュがあります。 これは改行を取り消すためのもので、次の行も同じ一行にくっつけるという意味です。 Rubyでは改行が空白よりも強い区切り(文の区切り)であるので、これをつけておきます。 ただし、これは一般論であり、上記のようなメソッドチェーンではバックスラッシュを取り去ってもRubyは同じように解釈してくれます。 好みの問題ですが、バックスラッシュをつけるほうが文のつながりを意識できて良いと思います。 to_hメソッドは配列をハッシュに変換します。 変換するには配列の内容がハッシュに対応するものでなければなりません。 詳細はRubyのドキュメントのArrayを参照してください。

結果としてdは

d = {:北海道=>5224614, :青森県=>1237984, ... ... ... , :沖縄県=>1467480}

となります。

Statクラス

ハッシュのサブクラスStatを統計処理のために作ります。 Statの中身はハッシュと同じですが、次のようなメソッドを作ります。

  • 値の検索(キーと値の組からなるハッシュを返す)
  • 最大の値をもつ組の検索
  • 最小の値をもつ組の検索
  • 平均値の算出
  • 昇順ソート
  • 並びを逆順にする

最初の3つは検索結果をStatオブジェクトで返します。 平均値は小数点以下第1位までに丸めた実数を返します。 最後の2つは並び替えられたStatオブジェクトを返します。

これらの機能はハッシュにはないものです。

class Stat < Hash
  def initialize h={}
    super()
    update(h)
  end
  def find_by_value(v)
    Stat.new(select{|key, value| value == v})
  end
  def max
    m = map{|k,v| v}.max
    find_by_value(m)
  end
  def min
    m = map{|k,v| v}.min
    find_by_value(m)
  end
  def average
    (map{|k,v| v}.sum.to_f/size).round(1)
  end
  def sort
    Stat.new(super{|a,b| a[1]<=>b[1]}.to_h)
  end
  def reverse
    Stat.new(to_a.reverse.to_h)
  end
end

このプログラムで注意真ければならないのはselfという特別な変数(疑似変数)です。 selfは「その」オブジェクト自身を表します。

def abc
  self.clear
  clear
end

メソッドabcの定義の中のselfとは、次のようなオブジェクトです。 例えばそのオブジェクトが変数xxに代入されていて、xx.abcが実行されたとき、selfは変数xxの指すオブジェクトになります。 同じクラスの別のオブジェクトが変数yyに代入されていて、yy.abcが実行されたときはselfyyの指すオブジェクトになります。

ですので、selfは固定されたオブジェクトではなく、メソッドabcが呼ばれたときのabcが属するオブジェクトになります。 このように、メソッドにはかならずそのメソッドとセットになったオブジェクトがあり、そのオブジェクトをメソッドのレシーバーとも言います。 ですから、「selfはメソッドabcのレシーバーを表す」とも言えます。

1行目はselfを空のハッシュにします。 2行目はレシーバーが書かれていません。 メソッド名だけです。 「メソッド名だけでメソッドが呼ばれたときのレシーバーは、selfとする」という約束があります。 ですから2行目と1行目は同じ結果をもたらします。

それでは、Statクラスの定義を説明しましょう。 クラス定義の中にはレシーバが省略されたメソッドが多数出てきますので注意してください。

  • StatはHashのサブクラス(class Stat < Hashの部分がそれを表す)
  • インスタンスを生成するStat.newが呼ばれたとき、initializeメソッドが初期化をする。 h={}というパラメータがあるので、newメソッドには引数をつけることができる。 引数が与えられなかったときのデフォルト値は{}、つまり空のハッシュ。 super()はこのメソッドのスーパークラスにおける同名のメソッドを呼び出す。 つまりHashのinitializeメソッドが呼び出される。 したがって、super()は空のハッシュ・インスタンスを生成、初期化する。 updateはレシーバが省略されたメソッドなので、selfをレシーバとして実行される。 updateはHashのメソッドで、引数を破壊的に付け加える。 以上から、Stat.newにハッシュが引数で与えられたときは、そのハッシュを中身とするStatオブジェクトが生成される。 引数がなければ空のStatオブジェクトが生成される
  • find_by_valueにはパラメータvがある。 vはハッシュの値(都道府県データでは人口を表す整数)のどれかであることが期待されている。 その値をもつキーと値の対からなるStatオブジェクトを返す。 selectメソッドはブロックの値が真になるような対だけからなるハッシュを返す。
  • maxは、まず値だけの配列をmapメソッドで作り、その最大値を(配列の)maxオブジェクトで取り出す。 最後にfind_by_valueでStatオブジェクトを返す。
  • minは同じことを最小値について行う
  • averageでは、Statの値だけからなる配列を作り、その合計を計算し、実数に変換、要素数で割って平均を求め、最後にroundで小数点以下一位に丸める
  • sortでは、Hashのsortメソッドをsuperで呼び出し、大小をブロックで評価してソートする。
  • reverseでは、ハッシュを配列に変換(各「キーと値」はその2つを要素とする配列に変換される)、reverseメソッドで逆順にし、ハッシュに戻す

説明は長くなってしまいましたが、これはメソッドチェーンでたくさんのことをコンパクトにプログラムしたからです。 逆にいえば、「Rubyでは短いプログラムで多くのことを実行できる」ということになります。

プログラムの実行

データの定義、Statクラスの定義に続けて、次のプログラムを書き加えます。

s = Stat.new(d)
p s.find_by_value(7344765)
p s.find_by_value(0)
p s.max
p s.min
p s.average
p s.sort
p s.sort.reverse

実行してみましょう。

{:埼玉県=>7344765}
{}
{:東京都=>14047594}
{:鳥取県=>553407}
2683959.6
{:鳥取県=>553407, :島根県=>671126, ... ... ... , :東京都=>14047594}
{:東京都=>14047594, :神奈川県=>9237337, ... ... ... , :鳥取県=>553407}
  • 都道府県別人口のデータをハッシュにしたdを用いてStatオブジェクトsを生成
  • 人口7344765の県を検索すると、埼玉県であった
  • 人口0の県を検索すると、そのような県はないので、空のStatオブジェクトが返される
  • 人口最大は東京都
  • 人口最小は鳥取県
  • 人口の平均値は2683959.6人
  • 人口の少ない順に並べると鳥取県・・・・東京都
  • それを逆順にし、人口の多い順に並べると東京都・・・鳥取県

クラスを作ったことにより、これらの統計処理がメソッドひとつで実行できるようになりました。 このクラスは都道府県人口以外の統計処理にも使えます。

また、標準偏差ヒストグラム(グラフィックの手段が必要)などもメソッドで用意すればもっと実用的になるでしょう。

今回はサブクラスを取り上げました。 StatクラスはHashのすべてのメソッドを使うことができるので、一から作るよりも少ない労力で作成することができました。 このようにサブクラスの仕組み(これを継承ともいう)はプログラムの資産を活かすことにつながるわけです。