tak0kadaの何でもノート

発声練習、生存確認用。

医学関連は 医学ノート

pythonのデコレータ(setter, getter, property, staticmethod, classmethod)

nd2readerのソースコードを見ていたら@staticmethodが散見された。文法を知らなかったので調べた。

1. デコレータの定義について

関数fに対して、デコレータを使うとfという表記でg(f)を呼べるようになる。デコレータ用の便利関数を書いておけばユーザは自分の関数を修飾した関数を作ることが出来る。

def save(func):
    def decorated_func(x):
        print("calculated {} and result is {}".format(func.__name__, func(x)))
        #with open("log.txt", "w") as f:
            #f.write("calculated {} and result is {}\n".format(func.__name__, func(x)))
        return func(x)
    return decorated_func

def memoize(func):
    cache = {}
    def decorated_func(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return decorated_func

@save
def f(x):
    return x+1

@memoize
def g(x):
    print("g is called")
    return x

# calculated f and result is 4
a = f(3)
# decorated_func
print(f.__name__)

# g is calledが表示される
g(1)
# 2回目以降は何も表示されない
g(1)

とは言ったものの関数の名前(f.__name__)がdecorated_funcに変わってしまっている。そこで@functools.wrapsをつける。

def save(func):
    @functools.wrap(func)
    def decorated_func(x):
        ...
    return decorated_func

# 以下と同様
def save(func)
    def decorated_func(x):
        ...
    decorated_func.__name__ = func.__name__
    decorated_func.__doc__ = func.__doc__
    return decorated_func

memoizeと同様の機能はfunctools.lru_cacheにすでに実装されているので、

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

とすることが出来る。

2. setter、getter、property

propertyはAPIの内部実装を隠してメンバ変数のようにアクセスさせるのに便利そう。

class Hoge:
    def __init__(self, y):
        self._y = y

    @property
    def x(self):
        return self._y**2

hoge = Hoge(1)
hoge.x = 2 # エラー

from math import sqrt

class Hoge2:
    def __init__(self, y):
        self.__y = abs(y)

    @property
    def x(self):
        pass

    @x.getter
    def x(self):
        return self.__y**2

    @x.setter
    def x(self, x):
        if x < 0:
            raise ValueError("x must be larger than 0")
        self.__y = sqrt(x)

hoge = Hoge2(3)
print(hoge.x)
print(hoge._HOGE__y)

3. staticmethod、classmethod

staticmethod、classmethodどちらもクラス、インスタンスの両方から呼び出せる。classmethodは第1引数に暗黙的にクラスを受け取るが、staticmethodは明示的な引数のみ。使うタイミングがはっきりしないが、selfを取らないことを明確に出来ること、メンバ関数なので継承できることがありそう。

class Base:
  @staticmethod
  def say_anything(s: str):
      print(s)

  @classmethod
  def hello(cls):
      print("hello from {}.".format(cls.__name__))

class Derv(Base):
    pass

# 同じ
Base.say_anything("hello world!")
Derv.say_anything("hello world!")
# 継承先でクラス変数を参照
Base.hello() # hello from Base.
Derv.hello() # hello from Derv.
# インスタンスからも呼ぶことが出来る。(例は省略)

# もう少し具体的な例(classmethodをファクトリ関数に使う例)
class Date:
  day = 0
  month = 0
  year = 0

  def __init__(self, day, month, year):
    # 省略

  # y/m/dの形式かチェック
  @staticmethod
  def is_date_valid(s: str):
      pass

  # y/m/dの形式からDateオブジェクトを作る
  @classmethod
  def from_string(cls, s: str):
      return cls(map(int, str.split("/")))

class DateTime(Date):
  pass

datetime1 = DateTime(10, 10, 1990)
datetime2 = DateTime.millenium(10, 10) # mileniumはstaticmethod
isinstance(datetime1, DateTime) # True
isinstance(datetime2, DateTime) # False

# classmethodをシングルトン的な(?)クラスに使う場合
class Logger:
    DEBUG = 5
    INFO  = 4
    WARN  = 3
    ERROR = 2
    FATAL = 1
    _level = DEBUG

    def __init__(self):
        Logger._level = Logger.DEBUG

    @classmethod
    def isLevel(cls, level) :
        return cls._level >= level

    @classmethod
    def debug(cls, message) :
        if cls.isLevel(Logger.DEBUG) :
            print "DEBUG: " + message

追記

柔軟に色々やろうと言う文脈になるとPythonによる黒魔術入門が参考になりそう(メタプログラミングとは?)。