使いこなしたい Python の便利機能

基礎的なものも含めて、使いこなせるようになりたい便利な機能をまとめてみました。

1. デコレーター (@)
2. コンテキストマネージャ (__with__)
3. 特殊メソッド (`__add__`, `__str__`, `__enter__` など)
4. 動的クラス定義とメタクラス (__type__, `__new__`)

# 1. デコレーター (@)
Python では関数を一つのオブジェクトとして扱うことができます。
```python
def test_function():
    print('function が呼ばれた')

>>> func = test_funcion  # func 変数に　test_function を渡す
>>> func()
function が呼ばれた
```
これを利用したのがデコレーターです。
デコレーターは関数を受け取り、受けっとった関数を加工して新しい関数を作成します。
↓のように定義、使用します。
```python3
def decotaor(func: Callable):
    # func は↓で定義する main 関数になる
    def wrapper(*args, **kwargs):
        func(*args, **kwargs)
    return wrapper


# @ で使用する
# decorator 関数に main が引数として渡される
@decorator
def main():
    ...
```

例えば、エラーが起きた時に指定した変数をログに書き込んでくれるデコレーター、を作成できます。

```python3
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` が用意されています。
この後処理のメソッドは、条件分岐やエラーによる中断によって呼び出し忘れがよく起こります。
コンテキストマネージャはその呼び出し忘れを防いでくれる便利な機能です。
```python
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`はメソッド全体を囲んでしまうため、スコープが大きく使い勝手が悪い場合もあります。そんな時にコンテキストマネージャが使えます。
```python
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`関数にやらせることができます。
```python
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. 動的クラス定義とメタクラス
クラスは静的に定義するのが一般的です。
```python
class Test(Parent):
    class_var = 'class_var'
```

`type`関数を使うことでこのクラスの定義を動的にすることもできます。

```python
# type('クラス名', ('継承するクラスのタプル',), {'クラス変数の辞書':})
Test = type('Test', (Parent,), {'class_var': 'class_var'})
```
Djangoの`Manager.from_queryset`はこの動的クラス定義が使用されています。
```python
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`関数の仕組みを静的定義と組み合わせたものがメタクラスになります。
メタクラスを使うことで、クラスの定義時に値を操作することができます。
```python
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`オブジェクトは直接アクセスすることはできません。
```python
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
```
メタクラスは非常に強力な機能ですが、エディタの予測が効かなくなったり、挙動が把握しづらいというデメリットも多いです。
乱用は控えた方がいいのかもしれません。