【Python3.7】dataclassがHashableを継承できない話

# 問題提起
Python 3.7 で新たに登場した`dataclass`デコレータは、クラスに様々なメソッドを自動的に付加してくれる。
例えば、次のようにすることで、`__init__()`と`__hash__()`と`__eq__()`を適切に実装した`namedtuple`のようなものを作ってくれる。

```python:dataclass_example.py
from dataclasses import dataclass

@dataclass(eq=True, frozen=True)
class Point3D:
    """Immutable struct for 3-D points"""
    x: float
    y: float
    z: float = 0.0  # default value

if __name__ == '__main__':
    p = Point3D(x=3.14, y=2.72)
    print(p)  # -> Point3D(x=3.14, y=2.72, z=0.0)
    q = Point3D(4.6, 9.3, 0.0)
    print(p == q)  # -> False
    another_p = Point3D(3.14, 2.72)
    print(p == another_p)  # -> True
    d = dict(p='Point3D can be', q='a key of a dictionary')
```

このインスタンス、`p`と`q`は、当然`hashable`である。

```python:make_sure_to_be_hashable.py
from dataclass_example import Point3D
from typing import Hashable

p = Point3D(x=3.14, y=2.72)
print(hash(p))  # -> -7688538011378258102
print(isinstance(p, Hashable))  # -> True
```

ところが、このクラスは`Hashable`を継承できない。
以下のコードはエラーになるのである。

```python:actually_its_not_hashable.py
from dataclasses import dataclass
from typing import Hashable

@dataclass(eq=True, frozen=True)
class HashablePoint3D(Hashable):  # Explicitly inherit from Hashable (actually it's a protocol)
    x: float
    y: float
    z: float = 0.0

if __name__ == '__main__':
    p = HashablePoint3D(x=3.14, y=2.72)
    # -> TypeError: Can't instantiate abstract class HashablePoint3D with abstract methods __hash__
```

`Point3D`はインスタンスが`__hash__()`メソッドを持つにもかかわらず、`Hashable`を明示的に継承することができない。
`Hashable`に限らず、`dataclass`によって付加されるメソッドを要求するほとんどの Abstract Base Class が、継承できずエラーになるはずだ。

# 理由
デコレータはただのラッパーで、`__new__()`を上書きするだけなのだが、その前に`ABCMeta`の監査が入ってしまうため、`__hash__()`が実装される前に`Hashable`かどうかの判定が行われてしまい、エラーが出てしまう。
`dataclass`はクラスの生成時ではなく、値の生成時に作用しているということだ。

以下では、このことをもう少し詳しく説明する。

## 復習: デコレータの中身
デコレータとは、メソッドやクラスに作用するラッパーである。

```python:minimal_decorator_example.py
from typing import Callable, Any

def sample_decorator(target_function: Callable[[int], str]) -> Callable[[int], str]:
    # decorated_function = ( lambda x: decoration(target_function(x)) )
    def decorated_function(x: int) -> str:
        original_return_value = target_function(x)
        return f'Decorated! Original return value is: { original_return_value }'
    
    return decorated_function

@sample_decorator
def sample(x: int) -> str:
    return f'x is {x}'

if __name__ == '__main__':
    print(sample(1))  # -> Decorated! Original return value is: x is 1
```

`sample_decorator`は、対象である`sample`メソッドを受け取って、それをラップした新たなメソッドを返す。
`@sample_decorator`を付けることで、元の`sample`メソッドにそのラッパーを作用させたメソッドを`sample`として改めて定義することができる。

## 復習: Abstract Base Class
Abstract Base Classとは、型チェックのないPythonにおいて、その型が特定のメソッドをサポートしていることを保証する構文である。

```python:minimal_abc_example.py
from abc import ABC, abstractmethod

class Button:
    def push(self) -> str:
        raise NotImplementedError()

class UnsafeButton(Button):
    """Ouch you unfortunately forget to implement push()"""
    pass

class ABCButton(ABC):  # Inheritance from ABC is the syntax sugar of metaclass=abc.ABCMeta
    @abstractmethod  # Declare that this method needs implementation
    def push(self) -> str:
        raise NotImplementedError()

class SafeButton(ABCButton)
    """Again you forget to implement push()"""
    pass

def do_stuff_and_push_the_button(button):
    do_your_complicated_stuff(button)
    button.push()

if __name__ == '__main__':
    unsafe_button = UnsafeButton()  # -> (No error)
    do_stuff_and_push_the_button(unsafe_button)  # -> NotImplementedError
    safe_button = SafeButton()  # -> TypeError: Can't instantiate abstract class SafeButton with abstract methods push
```

インスタンス生成時に怒ってくれるので、そのクラスのインスタンスが作れている段階で全ての`abstractmethod`が実装されていることを保証できる。

## 復習: Pythonの`__new__()`と`__init__()`
Pythonにおけるコンストラクタは2種類ある。`__new__(cls)`は`cls`のインスタンスを生成し、`__init__(self, ...)`は生成したクラスのインスタンス`self`に対して初期化処理を行う。

```python:minimal_constructor_example.py
class Test:
    def __new__(cls):
        instance = super().__new__(cls)
        print('new')
        return instance

    def __init__(self):
        super().__init__()
        print('init')

if __name__ == '__main__':
    t = Test()  # -> new, init
```

大事なのは、必ず`__new__()`を呼び出した後に`__init__()`が呼ばれること。
`__new__()`より先に、生成されるインスタンスに対して処理を加えることはできない。

## 本題
対象となるクラスを再掲する。

```python:actually_its_not_hashable.py
from dataclasses import dataclass
from typing import Hashable

@dataclass(eq=True, frozen=True)
class HashablePoint3D(Hashable):
    ...
```

この`@dataclass`はデコレータであるから、クラスを受け取ってクラスを返すメソッドである。
その中身は、たぶん「受け取ったクラスの`__new__()`を、以下のように書き換える」という内容なのだろう。

```python:the_newer_new_method.py
def __new__(new_cls):
    instance = old_cls.__new___()
    instance.__hash__ = ...
    instance.__eq__ = ...
    instance.__awesome_methods__ = ...
    return instance
```

すなわち、`__new__()`メソッドでインスタンスを生成する際に、種々のメソッドを付加するというものである。

ところが、このデコレータの作用対象である`old_cls`は、`Hashable`を継承している。

```python:Hashable_may_be_like_this.py
from abc import ABC, abstractmethod

class Hashable(ABC):
    @abstractmethod
    def __hash__(self) -> int:
        raise NotImplementedError()
```

したがって、

```
instance = old_cls.__new___()
```

の中身は、次のようになっている。

```python:old_class_new_may_be_like_this.py
def __new__(old_cls):
    instance = object.__new__(old_cls)
    ABC_abstract_implementaion_check(instance)  # -> Here we have NOT implemented the __hash__() yet!
```

この段階ではまだ`__hash__()`は実装されていないため、`abc.ABC`によって`TypeError`が送出されてしまう。

すなわち、
1. 対象となるクラスの`__new__()`、すなわちデコレータの`__new__()`が呼び出される。
2. デコレータの`__new__()`内で`super().__new__()`、すなわち`ABC`の`__new__()`が呼び出される。
3. `ABC`の`__new__()`内で`super().__new__()`、すなわち`object`の`__new__()`が呼び出される。
4. `object`の`__new__()`によってインスタンスが生成される。
5. `ABC`の`__new__()`による型検査が入り、この時点で全ての`abstractmethod`を実装していないと`TypeError`が送出される。
6. デコレータの`__new__()`によって種々のメソッドが付与される。

という順番で処理が行われ、5.の段階でエラーになってしまう。

これが事件の真相である。

# 対策
対策と言っても、`Hashable`を明示的に継承するシンプルな方法は、たぶん存在しない。
`__new__`の代わりに`dataclass`に直接メソッドを付与するようなメタクラス操作をするデコレータを自分で書くとか、`dataclass`でラップされたクラスを`Hashable`とは別に継承するとか、とにかくダサい方法しかないと思う。

だが、実際問題、`Hashable`を明示的に継承する必要はない。
我々には`Protocol`という強力な武器があるからである。

```python:protocol.py
from dataclasses import dataclass
from typing import Hashable
from abc import abstractmethod
from typing_extensions import Protocol

@dataclass(eq=True, frozen=True)
class Point3D:
    x: float
    y: float
    z: float = 0.0

class MyHashable(Protocol):
    @abstractmethod
    def __hash__(self) -> int:
        raise NotImplementedError()

def get_hashable_and_return_none(x: Hashable) -> None: pass
def get_myhashable_and_return_none(x: MyHashable) -> None: pass

if __name__ == '__main__':
    p = Point3D(4.6, 9.3)
    get_hashable_and_return_none(p)  # Mypy (static type checker) finds no error
    get_myhashable_and_return_none(p)  # Also no error
```

Mypyは賢いので、デコレータが付与するメソッドもきちんと理解してくれる。
明示的に継承できないのは若干読みづらいかもしれないが、静的型チェック時には`Protocol`を利用するようにすることで一応解決する。

（どうもmypyの`Hashable`の確認はかなり怪しい挙動をするようなのだが、そこには目を瞑るほかないようだ。）

この記事が皆さんのPython生活の一助となれば幸いである。