アクセス制御の抜け穴 (Python/Ruby編)

ある既存クラスの private なメソッドを外から使えるかどうかという話。

他人の触れてはならないプライベートな領域に踏み入りたい! なんていう不埒な欲望が首をもたげて現れた時点で9割9分どっか不健全なわけで、すぐに首を洗って出直すか首を括って死ぬべきですが、それでも往々にしてそんな時のための抜け穴を、僕たち私たちのプログラミング言語は用意してくれていたりするようです。*1

言語としてみれば、そういった裏技が使えることが必ずしも悪なのかどうか。全くないといざというときに困る…かどうかはともかく少なくとも困るかもしれないという漠然とした不安がつきまとうということはあるかもしれない。
それにまぁ、パズル的に使うのは全然アリだと思います。むしろそっちがメイン。そういうの好きな人多いですしね。

というわけで前置きが長くなりましたが私が知ってる各言語についてまとめてみます (その言語をふつうに使っている人にとっては常識といっていい内容だとは思います)。

まずはPythonRubyから。

Pythonの場合

Python はゆるいアクセス制御機構として、アンダースコア2つで始まり、アンダースコア1つ以下で終わるメンバ名 __hoge が _クラス名__hoge に置き変わるという仕組みを採っています。
名前が変わるだけなので、クラス名を知っていれば外からアクセスできます。楽勝です。

class Hoge:
    def func(self):
        print "Hoge func"
    def __priv(self):
        print "Hoge private"

x = Hoge()
x.func()
x.__priv()       # error
x._Hoge__priv()  # accessible

ほんとに名前が置換されるだけなので、こういったことも可能です。

class Hoge:
    def _Fuga__priv(self):
        print "Fuga private"

class Fuga(Hoge):
    def func(self):
        self.__priv()

x = Fuga()
x.func()

具体的に何かに使えるのかは知りませんが、何かメタなことを考える上でいろいろとやりやすそうではあります。

Rubyの場合

Ruby のアクセス制御は少し特殊で、private にするとレシーバ付きで呼ぶことができなくなります。x.hoge とか self.hoge でアクセスできないわけですね*2
なので基本的には、当該クラスまたはそのサブクラスのメソッド定義の中で直接呼ぶしかなくなります。逆にいえば、(protected ではなく) private であってもサブクラスからアクセスすることができるということです。

class Hoge
  private
  def priv
    puts "Hoge private"
  end
end

class Fuga < Hoge
  def priv; super end   # public
end

x = Hoge.new
x.priv        # error
y = Fuga.new
y.priv        # ok

ところが一方、自分のクラスのメソッドであっても、自分以外のオブジェクトに対してprivateメソッドを呼び出せません。

class Hoge
  def call_priv(obj)
    obj.priv
  end
end

x = Hoge.new
x.call_priv(Hoge.new)   # error

これはあんまり嬉しくないですね。こういう場合は protected にしておくのが普通です。


まぁ Ruby はわりといろんな抜け道の多い言語なので、例えばクラス定義を変更して、一時的に public にしてやるなんてことができてしまいます。

x = Hoge.new
class Hoge; public :priv end    # public に変更
x.priv
class Hoge; private :priv end   # 証拠隠滅☆

また instance_eval を使えば

x.instance_eval{ priv }

このように対象クラスの内部のコンテキストが自由に得られてしまいます。この方法ならアクセサメソッドが提供されていないフィールドにさえも外側から自由にアクセスできます。

*1:ところで「首を洗って出直す」って正しい日本語でしょうか? 「首を洗って待つ」なら分かりますが…。

*2:例外として名前が = で終わるメソッドは self 経由でアクセスできます

エラーモナド in Ruby

PHPの@演算子っぽいものをいじってたら Rubyエラーモナドっぽいものができました。

こんな感じで使えます。

result = try { parseint("hoge") }.
         catch(ArgumentError){ -1 }.
         catch{|e| puts e; -2 }
res_value = result.value if result.good?

begin/rescue 書くのと見た目ほとんど変わりません。
最後まで拾われなかった例外は、value を呼んだ時に投げられます。


エラーハンドラをたくさん用意しておいて、順番に試して最初に例外吐かなかったものを採用、とか。

maybe_error = try { ... }
result = handlers.inject(maybe_error) {|m,(e,h)| m.catch(e,&h)}.value


コードはこんな感じ。(ちょっと書き足した)

def try(exns=[])
  begin
    r = yield(*exns)
    Either.new(true, r)
  rescue => e
    exns.unshift(e)
    Either.new(false, exns)
  end
end

class Either
  def initialize(g, v)
    @good, @value = g, v
  end
  def catch(*exns, &b)
    exns = [Exception] if exns.empty?
    lastexn = @value.first unless @good
    if !@good && exns.any?{|e| lastexn.kind_of? e}
      try(@value, &b)
    else
      self
    end
  end
  def value
    if @good
      @value
    else
      raise @value.first
    end
  end
  def good?
    @good
  end
end

catch節での例外発生について

どういうときに普通のbegin/rescue式と違ってくるかというと

try { A }.catch(e1,e2){ B }.catch(e3){|x,y| C }

とあったときに、Aで例外が起こって、Bでそれを回復しようとしたところで別の例外が起こったとすると、C からはどの例外が見えるか。
普通のbegin/rescue式だと A の例外しか補足できない。Bの中で起こる例外に対処したい場合はBブロックの中で別のbegin/rescue式を書く必要がある。
それに対し上の実装では、Bの例外もCに流れるようになっている。
上のケースで C に処理が移るのは A で発生した例外 e1 or e2 が B で catch され、そこで例外 e3 が投げられた場合と、Aでe3が投げられて最初のcatchでe1,e2のどちらにもマッチせずBがスルーされた場合の2通りのパスがある。(前者の場合には x,y にそれぞれ Bの例外,Aの例外が入っているし、後者の場合には x に A の例外が入っており y は nil になる。)
両方の場合で C での対処方法が同じようなものになる場合やA,Bのどちらか一方でしかe3にマッチする例外が発生しない場合、線形に書けてすっきりするし、そうでない場合はややこしくなる。
begin/rescue式と比べてどちらの方がきれいに書けるかは場合によるが、多くの場合こっちで十分だと思える。

PHPの@演算子的なもの

http://d.hatena.ne.jp/gnarl/20080820/1219226223

例外かどうかを判断するのが誰であるべきかという話はおいといて、try-catch系の構文は、成功/失敗の二値が知りたいだけの場合でもあの長ったらしい構文を使わないといけないのがうざいなーとは常々思っていました。

Rubyだとこんな感じのブロックを取る演算があればいい?

class X
  @@exn = nil  # 直前に握り潰した例外

  # ブロックを評価し、例外が起きた場合は握り潰す
  def self.ignore_exception(recover_from=Exception)
    return nil if @@exn && !(@@exn.kind_of? recover_from)
    begin
      r = yield @@exn
      @@exn = nil
      r
    rescue => e
      @@exn = e
      nil
    end
  end
end

def _x(*a, &b)
  X.ignore_exception(*a, &b)
end

def parseint(s)
  if /^\d+$/ =~ s
    s.to_i
  else
    raise ArgumentError, "illegal format"
  end
end

parseint("123")       #=> 123
_x{parseint("123")}   #=> 123
parseint("hoge")      #=> 例外
_x{parseint("hoge")}  #=> nil が返る

_x{ ... } でくくると、中で例外が発生した場合は全て nil になります。(追記: これに近い働きをする rescue修飾子なんてものもありました。完全に忘れてました(^^;)

一般的には例外握り潰すとかマナー悪いもいいところですが、どんな例外でもひとつでも起きたら全部同じ処理っていうケースもよくあります。

if _x{ f(xxx) }
  // 成功したら...
else
  // 失敗したら...
end

特に長いメソッドチェーンでいちいちnilチェックしなくて済むのがうれしい。

result = _x{ tbl['xxx'][123][456] }
result = _x{ f(xxx).g(yyy).h(zzz) }

これはMaybeモナドっぽいと言えるかもしれない。

また ||演算子||-> 的に使えます(シングルスレッド限定)。

# 1番目で HogeError が起きた場合に限り, 2番目が実行される。
result = _x{...} ||_x(HogeError){|e|...} ||_x{...}

false や nil を途中で値として返したいときとか困りますがっ! (=> 追記:エラーモナド in Ruby)

たぶん計算をショートカットする二項演算というのが && や || の専売特許なのがあまり良くなくて、左辺のパターンマッチングでショートカットするかどうかを決められるような二項演算子定義機能があるとうれしい。

Two Envelopes

最近パズル系の本を読み漁っているのですが、以前からどうにも腑に落ちなかったものを一つ紹介します。
わりと有名なパズルですがググってもすっきりした解説が見あたらない、というかこれ系の解説が全然理解できないので。。
分かりやすい説明ができる方がいたら、ぜひぜひ教えてください!

    • -

二つの封筒A,Bがあって両方にお金が入っているのだが、
一方にはもう一方のちょうど二倍の金額が入っているとする。
あなたは好きなほうを選ぶことができ、選んだ封筒の中身を受け取ることができる。


いま、あなたはAの封筒に手をかけたとしよう。
Aの中にはn円入っているとする(まだ中身は見ていない)。
このときBには、2n円 または n/2円 のどちらかが入っている。


ここで気が変わってAではなくやっぱりBを選択することにしたとき、
50%の確率で 2n 円が手に入り、50%の確率で n/2 円が手に入るのだから、
受け取る金額の期待値は
2n/2 + (n/2)/2 = n + n/4 = 5n/4 (円) である。
Aの中身は n円なのだから、Bを選んだほうがよい(!)

逆の場合(最初Bを手に取り、やっぱりAを選ぶ場合)も同様に、途中で選択を変えた方が期待値が大きくなる。


さて、この議論はおかしいだろうか?
おかしいとしたら、どこがおかしいだろうか?


SRM414 Div1

久々のTopCoder。すっかり感覚を忘れててダメダメだった。

Easy

書類をn枚書かなきゃいけなくて、各書類の順番と書くのにかかる時間が与えられる。完成した書類を提出しないと次の書類がもらえない。しかも受付時間が制限されている。全部提出するのにどんだけ時間かかるか?

最初の書類に取り掛かる時刻を決めると最適なスケジュールがすぐ決まるので、各時刻で試してminを取ればよい。

Medium

n個の文字列からそれぞれ部分文字列を切りとって一つにつなげる。辞書順で一番小さいのは?

それぞれの文字列の先頭を見て一番小さいやつを採用、という手順の繰り返しでできる。ただし先頭が同じものが複数ある場合は、それ以降の文字列を比較して辞書順で前に来るものを選ばないといけない。また一方がもう一方の完全なプリフィクスになってる場合は長いほうを採用。

Hard

同一平面の同心円上を、n個の星がそれぞれ等速円運動している。与えられた時間の中で、「少なくともk個の星と中心が一直線上に並ぶ」という状況が何回発生するか?

むつかしー。