本記事で書くこと
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()
の戻り値が渡された変数x
はfusion()
を使うことができます。
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
デコレートされた新たな関数を格納する変数 = デコレータ(デコレートされる関数)
デコレートされた新たな関数を格納する変数()
実践
買う物をリストに入れて表示する関数があったとします。
def get_fruits(*args):
basket = []
for i in args:
basket.append(i)
print(basket)
get_fruits('apple','banana') # <- ['apple', 'banana']
これに、心の中のつぶやきをデコレートしてみます。
# デコレータ
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秒)からの経過時間を現地時間に変換し、必要な情報(年、月、日)を返すようになっています。
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として呼び出した組み込みモジュールtime
のtime関数
を使って、エポックからの経過時間(このブログを書いているときは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クラスメソッドを実行すると、現在の年y
月m
日d
を取得できます。
何が便利なのだろうか
まだ経験の浅い私には、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はデコレータ式(シンタックスシュガー)です。