基礎的なものも含めて、使いこなせるようになりたい便利な機能をまとめてみました。
- デコレーター (@)
- コンテキストマネージャ (with)
- 特殊メソッド (
__add__
,__str__
,__enter__
など) - 動的クラス定義とメタクラス (type,
__new__
)
1. デコレーター (@)
Python では関数を一つのオブジェクトとして扱うことができます。
def test_function():
print('function が呼ばれた')
>>> func = test_funcion # func 変数に test_function を渡す
>>> func()
function が呼ばれた
これを利用したのがデコレーターです。
デコレーターは関数を受け取り、受けっとった関数を加工して新しい関数を作成します。
↓のように定義、使用します。
def decotaor(func: Callable):
# func は↓で定義する main 関数になる
def wrapper(*args, **kwargs):
func(*args, **kwargs)
return wrapper
# @ で使用する
# decorator 関数に main が引数として渡される
@decorator
def main():
...
例えば、エラーが起きた時に指定した変数をログに書き込んでくれるデコレーター、を作成できます。
import logging
def log_vars_if_error(logger: logging.Logger, variables: list[str]):
"""
エラーが起きた場合、logger に variables の値を書き込む
"""
def decorator(func: Callable):
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except:
# func のローカル変数を取得
local_vars = inspect.trace()[-1][0].f_locals
logger.error(
{var: local_vars.get(var) for var in variables}
)
raise
return wrapper
return decorator
logger = logging.getLogger()
@log_vars_if_error(logger, variables=['test'])
def test_function():
test = 'エラーが起きた場合ログに書き込まれる'
raise Exception
デコレーターは関数意外を返すこともできますが、わかりづらくなるため注意が必要です。
2. コンテキストマネージャ (with)
特定のオブジェクトには、後処理となるメソッドが定義されている場合があります。例えば open
で作成したファイルオブジェクトには close
が用意されています。
この後処理のメソッドは、条件分岐やエラーによる中断によって呼び出し忘れがよく起こります。
コンテキストマネージャはその呼び出し忘れを防いでくれる便利な機能です。
f = open('test-file.txt', 'r')
...
if hogehoge:
raise Exception()
f.close() # エラーで中断されて呼び出されない場合がある
with open('test-file.txt', 'r') as f:
...
if hogehoge:
raise Exception()
# with のブロックを抜けると、内部で `close()` が呼び出される。
# エラーで中断しても呼び出されるので安心
このコンテキストマネージャを自作することで、使いやすいツールを作成できます。
上記のデコレーターの説明で作成したlog_vars_if_error
はメソッド全体を囲んでしまうため、スコープが大きく使い勝手が悪い場合もあります。そんな時にコンテキストマネージャが使えます。
class Log:
def __init__(self, logger: logging.Logger, variables: list[str]):
self.logger = logger
self.variables = variables
def __enter__(self) -> Self:
# with が始まる時に、 __init__ の後に呼び出される。
return self
def __exit__(self, exc_type, exc_val, exc_tb):
# with が終わる時に呼び出される。
# エラーで中断の際には 引数にエラーの情報が渡される
local_vars = inspect.stack()[1][0].f_locals
log_vars = {var: local_vars.get(var) for var in self.variables}
if exc_type is None:
# 正常に終了した場合
self.logger.info(log_vars)
else:
# エラーが起きた場合
self.logger.error(log_vars)
def test():
with Log(logger, variables=['test']):
test = 'エラーが起きても、起きなくてもログにこの値を書き込んでくれる'
raise Exception
他にも、非同期処理バージョンの async with
__aenter__
__aexit__
も用意されています。
3. 特殊メソッド (__add__
, __str__
, __enter__
など)
上の__enter__
などもそうですが、 Class
には__
で囲まれた特殊メソッドがいくつか用意されています。
これを設定していくことで、 python の算術演算子なども扱える便利なClass
を作成できます。
たとえば、簡単な並び替えの問題はビルトインのsorted
関数にやらせることができます。
class Student:
"""
背の順、身長が同じ場合は名前順に並べる
"""
def __init__(self, height: int, name: int):
self.height = height
self.name = name
def __eq__(self, other: Self) -> bool:
# == が True になる場合を定義する
return self.height == other.height and self.name == other.name
def __gt__(self, other: Self) -> bool:
# > が True になる場合を定義する
if self.height > other.height:
return True
elif self.height == other.height:
# 身長が同じ場合、アルファベットで遅い方が`大きい`とする
return self.name > other.name
else:
return False
def __lt__(self, other: Self) -> bool:
# < が True になる場合を定義する
if self == other:
return False
return not self.__gt__(other)
>>> yamada = Student(162, 'Yamada')
>>> sato = Student(172, 'Sato')
>>> yamada > sato
True
>>> tanaka = Student(168, 'Tanaka')
>>> sorted([yamada, sato, tanaka])
[yamada, tanaka, sato]
他にも足し算(__add__
)や論理積(__and__
)も定義できます。
ただし、直感的ではない定義は却って使いづらくなるため注意が必要です。
4. 動的クラス定義とメタクラス
クラスは静的に定義するのが一般的です。
class Test(Parent):
class_var = 'class_var'
type
関数を使うことでこのクラスの定義を動的にすることもできます。
# type('クラス名', ('継承するクラスのタプル',), {'クラス変数の辞書':})
Test = type('Test', (Parent,), {'class_var': 'class_var'})
DjangoのManager.from_queryset
はこの動的クラス定義が使用されています。
class Manager:
...
@classmethod
def from_queryset(cls, queryset_class, class_name=None):
if class_name is None:
class_name = "%sFrom%s" % (cls.__name__, queryset_class.__name__)
return type(
class_name,
(cls,),
{
"_queryset_class": queryset_class,
**cls._get_queryset_methods(queryset_class),
},
)
このtype
関数の仕組みを静的定義と組み合わせたものがメタクラスになります。
メタクラスを使うことで、クラスの定義時に値を操作することができます。
class TestMetaclass:
# __new__ の引数は type と同じ
def __new__(mcs, name: str, bases: tuple, attrs: dict):
attrs['class_var'] = 'class_var'
return super().__new__(name, bases, attrs)
class Test(metaclass=TestMetaclass):
pass
# 'class_var' が動的に定義されている
>>> Test.class_var
['class_var']
Django のModel
ではメタクラスが使われていて, クラス変数として定義したField
オブジェクトは直接アクセスすることはできません。
class Book(models.Model):
title = models.CharField(max_length=100)
# 直接はアクセスできない
>>> Book.title.max_length
Traceback (most recent call last):
...
AttributeError: 'DeferredAttribute' object has no attribute 'max_length'
# _meta からアクセスする
>>> Book._meta.get_field('title').max_length
100
メタクラスは非常に強力な機能ですが、エディタの予測が効かなくなったり、挙動が把握しづらいというデメリットも多いです。
乱用は控えた方がいいのかもしれません。