LoginSignup
23
18

More than 3 years have passed since last update.

【Python】@classmethod及びデコレータとは?

Last updated at Posted at 2019-12-23

本記事で書くこと

Pythonのクラスで登場する@classmethodという表記。これが何を意味するのか、どうやって使うのか、何が便利なのかを整理します。

@classmethodとは?

  • クラスのメソッドをクラスメソッドに変換して、インスタンスを作成しなくても使えるメソッドにしてくれます。
  • なお、@classmethodはデコレータ式(シンタックスシュガー)です。

デコレータとは?

デコレータ式を理解するためには、デコレータについて知る必要があります。
デコレータとは、ある関数を受け取り、新たな関数を返す関数のことです。

関数の以下の性質を用いて作れらます。
 ① 関数は変数に割り当てられる
 ② 関数内で他の関数を定義できる
 ③ 関数は関数を返すことができる
 ④ 関数をある関数の引数に渡すことができる

上記①~④.が事実かどうか、試してみます↓

①関数は変数に割り当てられる

# 関数の定義
def big(word="hello world"):
    return word.capitalize()+"!"
print(big()) # <- Hello world!

# 関数はオブジェクトなので、変数に割り当てて使うことができる
big2 = big
print(big2()) # Hello world!
②関数内で他の関数を定義できる
# 関数のネスト
def tension():
    x = 'by neko like man'
    # low()tenstion()内で定義し、
    def low(word='Hello World'):
        return word + '...' + x
    # すぐに実行
    print(low())

# low()はtension()を呼び出すたびに毎度定義される
tension() # <- Hello World...by neko like man

# しかし、low()はtenstion()の外側からはアクセスできない
try:
    print(low())
except NameError:
    print('No low') # <- No low

tension関数の内部でlow関数が定義されています。
又、tension関数で定義されている変数xを、内部関数lowで使うことができます。
この時、low関数をクロージャ、tension関数をエンクロージャといいます。
又、変数xのように、あるコードブロックで使われているけど、そのコードブロックでは定義されていない変数を自由変数(free variable)と呼びます。
(内部関数から変数xの値を変更することは基本的にはできません(nonlocalを使えば可能))

③関数は関数を返すことができる
def dragonball():
    # 自由変数xとy
    x = 100
    y = 100
    # fusiton()では外側の関数(dragonball())で定義されたx, yを保持している
    # fusion()がクロージャ, dragonball()がエンクロージャ
    def fusion(left='goku', light='trunks'):
        # 自由変数の値をクロージャ内から更新したい場合はnonlocal()を使う
        nonlocal x
        x = 900
        return f'{left}「フュー...」 強さ:{x} \
        {light}「フュー....」 強さ:{y} \
        {left}, {light}「ジョン!!」 強さ:{x+y}'
    # dragonball関数内で定義して返す。()は付けず関数オブジェクトを返す。
    return fusion

x = dragonball()
print(x())
# ↑ goku「フュー...」 強さ:900  trunks「フュー....」 強さ:100  goku, trunks「ジョン!!」 強さ:1000
print(x('piccolo','kuririn'))
# ↑ piccolo「フュー...」 強さ:900  kuririn「フュー....」 強さ:100  piccolo, kuririn「ジョン!!」 強さ:1000

関数dragonball()は内部で定義された関数fusionを返しています。
()は付けず関数オブジェクトを返すことで、dragonball()の戻り値が渡された変数xfusion()を使うことができます。

④関数をある関数の引数に渡すことができる
def hand_over(func):
    print('hand over')
    print(func())

hand_over(dragonball())
# ↑ hand over
#   goku「フュー...」 強さ:900         trunks「フュー....」 強さ:100         goku, trunks「ジョン!!」 強さ:1000

関数dragonball関数hand_overの引数に渡すことができます。
関数dragonball内部関数fusionを含んでおり、関数hand_overには内部関数fusionが渡されます。
なお、関数を渡せる関数(今回で言えば、
hand_over()`)は、他の関数が存在することありきなので、単体では動きません。


上記のように関数は、
 ① 関数は変数に割り当てられる
 ② 関数内で他の関数を定義できる
 ③ 関数は関数を返すことができる
 ④ 関数をある関数の引数に渡すことができる
といった4つの性質を有しています。

デコレータは、上記②③④の性質使って定義された関数で、①の性質を用いて利用されます。

つまり、デコレータとは
関数Aを受け取り、機能を追加して、新たな関数A_ver2.0を返す関数です。
関数A_ver2.0は変数に割り当てて使います。

デコレータを作る際は、

  • デコレータの引数に機能を追加したい関数Aを渡す(④)
  • 内部関数で機能を追加(②)する(関数A自体も実行)
  • デコレータの戻り値を内部関数のオブジェクト(カッコは付けない)にする(③)
  • その戻り値を変数Xに割り当てる(①)

ことで、機能が追加された関数A_ver2.0誕生し、X()という形で使えるようになります。
(クラスのMixinと似ていると思いました。)

デコレータを使ってみる

デコレータの作り方

def デコレータ名(デコレートされる関数の置き場所):
    # デコレートされる関数の引数がどのようなものでも対応できるように可変長引数にする
    def デコレータ(*args, **kwargs): 
        # デコレートされる機能を呼び出す(元の機能をそのまま使うか、編集するかは自由)
        result = func(*args, **kwargs)
        # デコレートで新たに追加したい機能
    return デコレータ # <- デコレートされた機能を含む

def デコレートされる関数()
    機能

デコレータの使い方_1

デコレートされた新たな関数を格納する変数 = デコレータ(デコレートされる関数)
デコレートされた新たな関数を格納する変数()

実践

買う物をリストに入れて表示する関数があったとします。

関数get_fruits
def get_fruits(*args):
    basket = []
    for i in args:
        basket.append(i)
    print(basket)


get_fruits('apple','banana') # <- ['apple', 'banana']

これに、心の中のつぶやきをデコレートしてみます。

関数get_fruits_ver2.0
# デコレータ
def deco(func): #④
    def count(*args): #②
        print('何を買おうかな')
        func(*args)
        print('よし、これにしよう')
    return count #③

def get_fruits(*args):
    basket = []
    for i in args:
        basket.append(i)
    print(basket)

# get_fruits()をdeco()でデコレートする
# デコレータの引数にデコレートしたい関数を入れたものを変数に渡す
# その変数に引数を渡して実行すれば、deco + get_fruits な関数(get_fruits_ver2.0)が実行さる
deco_get_fruits = deco(get_fruits) #①
deco_get_fruits('apple','banana')
# ↑ 何を買おうかな
#   ['apple', 'banana']
#   よし、これにしよう

デコレータを使って作成した関数のオブジェクトを変数(deco_get_fruits)に入れることで、get_fruitsの機能追加版を使えるようにしています。又、デコレータを使って作成した関数のオブジェクトを、デコレートした関数と同じ名前の変数名に格納することで、デコレートされた関数で上書きできます。


get_fruits = deco(get_fruits)
get_fruits('apple','banana')
# ↑ 何を買おうかな
#   ['apple', 'banana']
#   よし、これにしよう

以上のように、デコレータの戻り値を変数に代入することで、関数ver2.0を作っていますが、デコレータ式を使えばいちいち変数に代入しなくても済み、コードがキレイになります。

デコレータの使い方_2 : デコレータ式を使う

@デコレータ名
def デコレートされる関数()
    機能

デコレートされた関数()

実践

@deco
def get_vegetable(*args):
    basket = []
    for i in args:
        basket.append(i)
    print(basket)

get_vegetable('tomato','carrot')
# ↑ 何を買おうかな
#   ['tomato', 'carrot']
#   よし、これにしよう

関数定義の前行に@デコレータの名前と記述することで直下に定義される関数がデコレートされ、その関数と同じ名前の変数名に割り当てられます。つまり、変数の中身が関数ver_2.0に上書きされます。以下のコードと同義です。

# デコレートされた関数名と同じ名前の変数 = デコレータ(デコレートされる関数)
get_vegitable = deco(get_vegetable)

使い方_1よりも、使い方_2のほうが見やすいです。このように、処理内容は同じだけど構文をシンプルにして、見やすくしたものシンタックスシュガーといいます。

以上のように、デコレータ式を使えば、変数への代入を省くことができ、可読性が上がるようです。
本記事の本題である@classmethodも、デコレータ式です。組み込み関数classmethod()を呼び出しています。
classmethod()には、クラス内に定義されたメソッドをクラスメソッドに変換する機能を有しています。

classmethod

classmethodは、クラスのインスタンスを作成しなくても使えるメソッドです。
あらかじめクラス内部で処理を行った上で、インスタンスを生成したい時に用いるようです。

具体例

例えば、detetimeモジュールでは、dateクラスのtodayメソッドがclassmethodとして定義されており、エポック(1970年1月1日午前0時0分0秒)からの経過時間を現地時間に変換し、必要な情報(年、月、日)を返すようになっています。

datetimeモジュールdateクラスのtodayメソッド
class date:
 #略
    @classmethod
    def fromtimestamp(cls, t):
        "Construct a date from a POSIX timestamp (like time.time())."
        y, m, d, hh, mm, ss, weekday, jday, dst = _time.localtime(t)
        return cls(y, m, d)

    @classmethod
    def today(cls):
        "Construct a date from time.time()."
        t = _time.time()
        return cls.fromtimestamp(t)
解説という名の個人的なお勉強

上記のコードを理解するために、改めて文章で整理します。

todayクラスメソッドでは、_timeとして呼び出した組み込みモジュールtimetime関数を使って、エポックからの経過時間(このブログを書いているときは1576760183.8697512という値が返された)を変数tに代入。それをfromtimestampクラスメソッドの引数に渡しています。

fromtimestampクラスメソッドでは、timeモジュールのlocaltime()関数に変数tを入れています。
localtime()を使うと、変数tに入っている数字を月や日、秒などに仕分けをし、タプルに格納してくれるため、わかりやすくて便利。
localtime()は引数なしで使うと、現地時間を取得してくれる。)

time.struct_time(tm_year=2019, tm_mon=12, tm_mday=19, tm_hour=12, tm_min=56, tm_sec=41, tm_wday=3, tm_yday=353, tm_isdst=0)

これらをそれぞれ、変数y, m, d, hh, mm, ss, weekday, jday, dstに格納して、最後にy,m,dだけを返しています。

そのため、todayクラスメソッドを実行すると、現在の年ymd を取得できます。

何が便利なのだろうか

まだ経験の浅い私には、classmethodのメリットをつかめていませんが、個人的には、「クラスの初期化をしたいけど、コードが煩雑になるため__init__とは別の場所で初期化をしたい」ような時に用いるのだろうと、整理しました。実用例を見ていると、外部から情報を取得してくる処理をクラスメソッドに定義しているような印象です。

@classmethodで遊んでみた。

外部から情報を取得してくるコードを考えていたところ、以前書いたブログで、地元の天気予報の情報を取得してくるコードを書いていたので、再利用してみました。

地元の天気予報を当てるゲーム
import requests, xml.etree.ElementTree as ET, os

# 今日の降水確率を当てるゲーム
class Game(object):
  ch = []

  def __init__(self, am1, am2, pm1, pm2):
    self.am1 = am1
    self.am2 = am2
    self.pm1 = pm1
    self.pm2 = pm2

  @classmethod
  # 6時間ごとの降水確率をwebから取得 茨城南部
  def rain_percent(cls):
    r = requests.get('https://www.drk7.jp/weather/xml/08.xml')
    r.encoding = r.apparent_encoding
    root = ET.fromstring(r.text)
    area = root.findall(".//area[@id]")  #北部と南部
    south = area[1] #南部エリアのノード
    info = south.findall('.//info[@date]') #南部の7日分
    today = info[0] #南部の今日の分のノード
    period = today.findall('.//period[@hour]') 
    cls.ch = []
    for percent in period:
      cls.ch.append(percent.text)
    return cls.ch

  def quiz(self):
    print(f'あなたの回答 -> [{self.am1}-{self.am2}-{self.pm1}-{self.pm2}] : 今日の降水確率 -> {Game.ch}')


play1 = Game(10,10,10,10)
play1.rain_percent()
play1.quiz() # あなたの回答 -> [10-10-10-10] : 今日の降水確率 -> ['0', '10', '40', '50']

クラス及びインスタンス両者からアクセス可能です↓

Game.rain_percent()  # ['0', '10', '40', '50']
play1.rain_percent() # ['0', '10', '40', '50']

クラスメソッドの第一引数はclsとすることが推奨され、クラス名.クラスメソッド()またはインスタンス名.クラスメソッド()として呼び出された時に、clsにはクラス名またはインスタンスされたクラスが渡されます。

まとめ

以上のように、@classmethodとは

  • クラスのメソッドをクラスメソッドに変換して、インスタンスを作成しなくても使えるメソッドにしてくれます。
  • 又、@classmethodはデコレータ式(シンタックスシュガー)です。
23
18
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
18