Python のデコレーター


こんにちは、切口太郎です。

Python でよく利用されるデコレーター機能についてお話します。
デコレーターは便利な機能なのですが、少しだけ難しい概念です。

1 デコレーターとは


デコレーターは、関数の処理を「元の関数を変更しないで」処理の追加を行うために利用します。
デコレーターを宣言するときには、先頭に @をつけます。

Python にはいくつかのデコレーターが組み込まれています。
また、デコレーターは自分のプログラムで新しく作成することができます。

デコレーターは、少し解りにくいですが、読み方のコツがわかれば簡単です。

ソースは、Pythonのパスを返す簡単な関数です。
import sys

# Pythonのパスを返す関数
def get_python_path():
    return sys.base_exec_prefix


# Python のパスを表示する
print(get_python_path())

動かすと、Pythonのパスを表示します。


 C:\Users\kirikutitarou\Anaconda3

関数 get_Python_path() を「変更せずに」 Python が動いているOSの名前も追加して表示します。

import sys

def add_platform(f):
    def wrapper():
        return sys.platform + ' ' + f()
    return wrapper

# Pythonのパスを返す関数
@add_platform
def get_python_path():
    return sys.base_exec_prefix


# Python のパスを表示する
print(get_python_path())

9行目の関数を追加します。

まず、デコレーター用の関数を追加します。
関数 add_platform() が追加する関数です。
この関数には、内部関数を用意して、OSの名前と引数 f の戻り値をあわせた文字列を返します。

次にデコレーターを追加します。
元からある、get_python_path()関数の前に、@add_platform を追加します。
@ + 関数名がデコレーターです。

動かすと、OSとPythonパスを表示します。


 win32 C:\Users\kirikutitarou\Anaconda3

どのように動作しているのでしょう?

① print(get_python_path()) で、get_python_path() 関数を呼び出します。
② get_python_path() は、@add_platform でデコレートされているので、デコレーター関数の add_platform を呼び出します。
  引数の f は、get_python_path() 関数の仮引数です。
③ 内部関数の wrapper が実行されます。
④ wrapper では、sys のプラットフォーム名(OSの簡略名)を取得して、仮引数 f 関数( get_python_path) を実行して、リターン値と結合して返します。
⑤ デコレーター関数( add_platform )は、wrapper ラッパーを返します。
  これは、仕様でデコレーター関数は、何も返さないか、関数を返すようになっています。
⑥ print(get_python_path()) に戻り、戻り値の wrapper 関数の結果を表示します。

これが、デコレーターの処理です。

Python では、同じ処理をクラスを使用しても記述できます。
クラスには、継承があるので、メソッドをオーバーライドすることで、メソッドの動作を変更できます。

import sys

# ベースのクラス
class BaseCls:
    def get_python_path(self):
        #Pythonのパスを得る
        return sys.base_exec_prefix        
    
# ベースクラスを継承する拡張クラス
class ChildCls(BaseCls):
    def get_python_path(self):
         # プラットフォーム(OSの簡略名)とスーパークラスのメソッドの戻り値を結合する
         return sys.platform + ' ' + super().get_python_path()

# テストでベースクラスのメソッドを表示する。
base = BaseCls()
print(base.get_python_path())

# 拡張クラスのメソッドを表示する。
child = ChildCls()
print(child.get_python_path())
このプログラムでも同じ結果になります。

拡張クラスの get_python_path() メソッドを呼び出すと、現在のプラットフォーム(OSの簡略名)を取得してスーパークラス(BaseCls) の get_python_path()メソッドを呼び出します。

どっちも同じように作れるのですが、class メソッドでもデコレーターは利用できます。
いろいろ拡張できて便利なのですが、ソースを読むのは慣れが必要(大変)です。

デコレーターは python ではよく利用されるので、まず動作を理解して読み方のコツを覚えましょう!
読めれば、自然と書けるようになります。


2 Pythonの組み込みデコレーター


Python には、組み込みのデコレータがいくつもありますが、クラス内でよく利用する3つのデコレーターを紹介します!

@classmethod


このデコレーターを使用するとインスタンス化しないでクラスのメソッドを呼び出すことができます。
また、クラス変数を使用することもできます。

class BaseCls:
    # classmethod で参照する変数
    _val = 0
    def __init__(self, value):
       self._value = value
       _initValue = value

    def getValue(self):
        return self._value
    
    @classmethod
    def getClassValue(self):
        # self._val = 1024
        return self._val

    @classmethod
    def setClassValue(self , value):
        self._val = value
        return self._val

# インスタンス化して呼び出す
base = BaseCls(100)
print(base.getValue())
print(base.getClassValue())

# インスタンス化しないで呼び出す

#classmehod デコレーター付き
BaseCls.setClassValue(200)
print(BaseCls.getClassValue())              # 呼び出される
# クラスのメソッド
print(BaseCls.getValue())                   # エラーになる
3行目の変数は、@classmethod でデコレートしたメソッドが参照します。
@classmethod で呼び出すと、__init__() のメソッドが通らないので _value は参照できません。(初期化はインスタンス化の時に呼び出されます)

11行目と 16行目が @classmethod でデコレートしたメソッドです。
16行目のメソッドが、_val に値を設定するメソッド、11行目が _val の値を取得するメソッドです。

21行目からクラスメソッドの呼び出しです。
21行目から24行目までが、クラスをインスタンス化して呼び出しています。

@classmethod でデコレートした場合でも、普通にインスタンスから呼び出せます。

実行すると、
23行目が、__init__() で初期化した値 の 200を表示します。
24行目は、3行目で初期化した値の0が取り出せます。

29,30行目は @classmethod でデコレートしたメソッドを呼び出します。
@classmethodでデコレートしたメソッドをインスタンス化しないで呼び出す場合、「クラス名 + "." + メソッド名」で呼び出します。

29行目で、_val に200を設定します。
30行目で、_val の値を取得して表示します。
200が表示されるはずです。

32行目は、実験です。
@classmethod でデコレートしていない getValue() メソッドを 「クラス名 + "." + メソッド名」の形式で呼び出します。

エラーメッセージが表示されるはずです。
TypeError: getValue() missing 1 required positional argument: 'self'

@classmethod をつけないメソッドは、23行目のようにインスタンス化してから呼び出します。


@staticmethod


このデコレーターと @classmethod は似ているのですが、「クラスメソッドを関数のように利用する」目的で利用します。
どこが違うかというと下表に整理しました。

デコレーター
呼び出し方
クラス変数の利用
インスタンス化
インスタンス化したクラスからの呼び出し
@classmethod
 「クラス名 + "." + メソッド名」
不要
@staticmethod
 「クラス名 + "." + メソッド名」
不要

@staticmethod は、①クラス変数が利用できない、②インスタンス化したクラスから利用できない という違いがあります。

class BaseCls:
    def __init__(self, value):
       self._value = value
       _initValue = value

    def getValue(self):
        return self._value
    
    @staticmethod
    def getClassValue():
        return 1024;



#staticmethod デコレーター付き
print(BaseCls.getClassValue())
9行目が @staticmethod でデコレータしたメソッドです。
クラス変数が利用できないので、そのまま定数を返しています。

16行目が呼び出し方法です。
実行すると、1024 が表示されます。

@property


読み出し専用のプロパティの作成
Python クラスのプロパティは、読み出し、変更ができますが、読み出し専用のプロパティを作成する時に利用します。
このデコレーターは、クラス内部に定数を持ちたい場合や、クラスのインスタンス時に設定した値を定数として変更させたくない場合に利用します。

# クラス定義 インスタンス化の引数で渡した value の値を定数として利用する。
class BaseCls:
    def __init__(self, value):
       self._value = value
    
    # プロパティデコレーターで、value という疑似プロパティを定義する
    @property
    def value(self):
        return self._value

# クラスのインスタンス化
base = BaseCls(100)
BaseCls の疑似プロパティ value から値を取得する
print(base.value)        # ①

# 疑似プロパティ value に値を設定する
base.value = 200        # ②
@property デコレーターを利用して、value という存在しない疑似プロパティを作成します。
この疑似プロパティは、self._value へアクセスしますが、読み取りのみで、値の設定はできません。

実行するとこうなります。

① print(base.value)
---> 100

② base.value = 200
---> AttributeError: can't set attribute エラーとなって値は設定できません。


もし、この value 疑似プロパティに値をせってしたい場合には、デコレーターを追加して setter メソッドを作成します。

# クラス定義 インスタンス化の引数で渡した value の値を定数として利用する。
class BaseCls:
    def __init__(self, value):
       self._value = value
    
    # プロパティデコレーターで、value という疑似プロパティを定義する
    @property
    def value(self):
        return self._value

    @value.setter
    def value(self , setValue):
        self._value = setValue

# クラスのインスタンス化
base = BaseCls(100)
BaseCls の疑似プロパティ value から値を取得する
print(base.value)        # ①

# 疑似プロパティ value に値を設定する
base.value = 200        # ②
print(base.value)        # ③

11行目のメソッドを追加します。
@value.setter は、@property で設定した疑似プロパティに値を設定します。


実行するとこうなります。

① print(base.value)
---> 100

② base.value = 200

③ print(base.value)
---> 200

これで、疑似プロパティ value へのアクセスができるようになります。

「そのまま _value にアクセスすればいいやん」と思いませんか?


>>>base._value = 300
>>>print(base.value)
300

確かに。


Pythonでは、@property だけと考えていたほうが良いですね。

この疑似プロパティアクセスは、オブジェクト指向の隠蔽化(カプセリング)を Python のクラスで実現させるために使用します。

コメント

このブログの人気の投稿

Hyper-V で Docker Desktop for Windows を使う(その2)

Python の命名規約 - ネーミングルール

VS Code で Hyper-V + Docker Desktop for Windows