gallu’s blog

エンジニアでゲーマーで講師で占い師なおいちゃんのブログです。

いわゆる__callと__getマジックメソッド

PHPで言うところのマジックメソッド、Pythonの世界では「特殊メソッド」って呼称するようですねぇ、とかいう細かいナレッジをぶっこみつつ。
ちなみにPythonは3でございます。「2とかありえない」と、複数名のPythonist(Pythonista?)に言われたので、割と素直に従っておりまする。


ちょいと扱いたいので調べてみたのですが……案外に厄介だったので、いくつか試行錯誤。
まずそもそもとして「存在しないメソッド/プロパティをcallした時に呼ばれる特殊メソッド」が、__getattr__一つなので、まずその辺で「メソッドcallなのかプロパティcallなのか」を把握しなきゃいけないという以下略。
いやまぁそもそもとして「publicなプロパティとかありえないからプロパティにアクセスしようとした愚か者に百年の呪いあれ」とか言いきってもよさそうな気がわりとビシバシとしてはいるのですがいやマジで。


ただまぁ一応技術者として「できない」よりは「使わないけどできるけど使ったものに災いあれって事で使わない」ほうがよろしいかなぁ、と思うので「どうしても無理ならあきらめる」くらいの緩い温度感で、さくり、と。


とりあえずどっかググって書いた初手。
幾分「いろいろやってる感がある」のが、若干もにょりつつ、まず「なんとかはなるんだよなぁ」というあたりを確保。

#
import inspect

#
class hoge(object):

  def dummy(self, *args):
    print('__call: {}({})'.format(self.name, args))
    return "hogehoge"

  def __getattr__(self, name):
    #print("{}".format(name))

    # ここがもうちょっと綺麗だとうれしいんだろうなぁ……
    frame = inspect.getframeinfo(inspect.currentframe().f_back)
    code = ''.join([line for line in frame.code_context])
    code = ''.join(code.split()) + ' '  # remove all spaces and append sentinel
    s = code[code.index(name) + len(name)]
    #print("::{}".format(s))
    #
    if s == '(':
      self.name = name
      return self.dummy
    else:
      print("error")

h = hoge()

#
s = h.test(1,2,3)
print("return is {}".format(s))
s = h.test2()
print("return is {}".format(s))

s = h.foo
print("return is {}".format(s))


少しだけ試行錯誤してみて、コード量を落ち着けてみた……つもりのコード

import inspect

class X(object):
  def __getattr__(self, key):
    class MethodHook:
      def __init__(self, key):
        self.key = key
      def __call__(self, *args, **kwargs):
        print('__call: "{}", {}, {}'.format(self.key, args, kwargs))

    frame = inspect.getframeinfo(inspect.currentframe().f_back)
    code = ''.join([line for line in frame.code_context])
    code = ''.join(code.split()) + ' '  # remove all spaces and append sentinel
    s = code[code.index(key) + len(key)]
    if s == '(':
      return MethodHook(key)
    else:
      print('__get: "{}"'.format(key))
      return "placeholder"

#
x = X()
x.undef_method(1, 2, 3, a1="Hello", a2="World")
x.undef_method2()

#
y = x.hoge
print("y={}".format(y))


色々調べてみるともうちょっとすっきり……なんだけど、プロパティが取れない………

class A:
    def __init__(self):
        self.testProperty = 'text of testProperty'

    def __getattr__(self, name):
        def function(*args):
            #print("You tried to call a method named: %s" % name)
            print('__call: {}({})'.format(name, args))
            print(self.testProperty)
        return function

a = A()
a.test()
a.test2(1,2,3)
a.test2([1,2,3])
val = a.hoge
print ("hoge is {}".format(val))


もうちょい試行錯誤して、概ね一段落……なんだけど、戻り値の型が固定になるなぁ……

class Magic(object):
    def __getattr__(self, name):
        print("__getattr__: {}".format(name))
        class Callable(int):
            def __call__(self, *arguments):
                print("call {}: {}".format(name, arguments))
        return Callable(100)

m = Magic()
m.test()
m.test(20)
m.test2(1,2,3,4,5)
print(m.x)


まぁ「プロパティだしなぁ」ってんで、雑な解決策を考えてみたら通ったので、まぁこの辺かなぁ一旦、的な。

class Magic(object):
    def __getattr__(self, name):
        print("__getattr__: {}".format(name))
        class CallableI(int):
            def __call__(self, *arguments):
                print("call {}: {}".format(name, arguments))
        class CallableS(str):
            pass
        if (name == 'test_s'):
            return CallableS('test')
        # else
        return CallableI(100)

m = Magic()
m.test()
m.test(20)
m.test2(1,2,3,4,5)
print(m.x)
print(m.test_s)

# ところで、存在しないプロパティでも受け取るんだねぇ……
m.hoge = 999
print(m.hoge)


とりあえず初手で欲しかった「__call」はまぁまぁな感じで実装ができたので。

__get、ぶっちゃけると一番よくやるのは「攻性防御用 http://d.hatena.ne.jp/gallu/20140119/p1 」なのでw
適当に(第一種)ホワイトリストで「許可されているメソッド名」以外は「例外ぶん投げる」とかで処理が終わりそうでもあるので、あんまり問題なさそうな気も、わりとむんむんwww
(ゲーム作成初期で、時々「実用で」__get使いますが)


まぁとりあえず当初の目的は達成したので、備忘録的に、めも。

「こうやったらもっとエレガントぢゃ!!」とかいう突っ込みがあったら歓迎いたしますので、是非。

PHPエンジニアがPythonに触れてみる:まずはインストール

さておいちゃんが果たして「PHPエンジニアなのか?」というのは、微妙に疑問もあるのですが(PHPばっかりではないし、さりとて最近はPHPが圧倒的に多いのも事実だし)。
いわゆる「初めての**」的な感じのPython版は、書籍なりWebなり色々あるかと思うので。
ちと切り口を変えて「PHPはある程度がっちょりやってるエンジニアがPythonをやってみる」って切り口での学習ルートを残しておこうかなぁ、と思ったのがきっかけでございます。


Pythonをやろうかなぁと思う理由は、建前を取り繕えば多々あれど、本音ぶっちゃけると「綺麗だし切り口が気に入っているから」(笑
まぁ、人間が何かに興味を持つ時なんて、そんな程度のものでございますw


で、先日、信頼おけるPythonエンジニアのむーちゃんに質問をしたのですが、ものっそざっくりと要約すると「これから学習するんなら3でもいいんぢゃね?」って話だったので。
Pythonの2系についてはバッサリと一端切り落としをして、3系を学習したいと思います。


プログラムを学習する以上当然ながら「実行環境」が必須のmustでないと死んじゃうので。
…ただ、手持ちのサーバが(ある意味都合よい事に)Pythonの2系が入ってたので、まずは「3系のinstall」をいたしませう、というあたりから。
鯖はLinuxなので、それ以外の環境は適宜自主的にどうにかしてくださいw


総本山っぽい
https://www.python.org/
に伺うと「Download」なる所があるので、そこの「Latest:」にあるリンク先を選択。
今なら
https://www.python.org/downloads/release/python-360/


tarballでのインストールが基本のおいちゃんなので、Gzipped source tarball( https://www.python.org/ftp/python/3.6.0/Python-3.6.0.tgz )を把握。
Linux鯖なので

wget https://www.python.org/ftp/python/3.6.0/Python-3.6.0.tgz

で一撃のもとに取得。

tar zvxf Python-3.6.0.tgz
cd Python-3.6.0
./configure --prefix=/opt/lang/python-3.6.0
make -j 3
sudo make install


で、一端「バージョン番号のついたディレクトリ」にインストール。
対して深い意味もないんだけど細かい話をすると「複数バージョンを気軽にごにょごにょしたり出来る」ので、一応。
シンボリックリンクを使うんだけど、そのあたりは http://d.hatena.ne.jp/gallu/20120724/p1 を参照。

なんかいくつかのサイトを見ていると「Pythonコンパイルの前に入れておかないといけないライブラリ」がいくつかあるぽいんだけど、多分おいちゃんの現在環境だと「すでにtarballで入れてる」と思われるので、なんの引っかかりもなくインストール完了。
この辺は、後でコメントとかあったら直すかも。


で、この状態で

/opt/lang/python-3.6.0/bin/python3.6

叩くと動くので、最低限これでもよし。
ただまぁ「pathが通ってるところに入ってるほうが色々と好都合だなぁ」ってのはあるので、以下のコマンドを追加でぶっこみ。

sudo ln -s /opt/lang/python-3.6.0/bin/python3.6 /usr/bin/python3

これで、よっぽど奇異な設定かましてる鯖でなければ

python3

だけで動くようになったと思う。


いや「/usr/bin/pythonを上書き」とかでもよかったんだけど、installした時のbinのファイル名が「python3.6」だったので、なんとな〜く。
「万が一、2系との挙動差分を楽しみたい」なんて事が、なきにしもあらずんば、かもしれない風な可能性が予見されないでもないので。

恐らく九分九厘、近々のタイミングで「pip3」とか必要になるんだろうけど「必要なものは必要になってから」のほうがよいと思っているので、あえてここでは一端無視。


ここまでで、とりあえず「学習環境」が最低限整いました。
なんかこの時点での突っ込みどころなどありましたら、ご連絡くださいませ。