ロバストなPythonコードを書くための基本的なテクニック

# はじめに
本記事は認知科学に基づくアプローチを踏まえて、ロバストなPythonコードを書くための基本的なテクニックについてまとめています。

[「ロバストPython―クリーンで保守しやすいコードを書く」](https://www.oreilly.co.jp/books/9784814400171/)を読み終えて、ロバストの重要性に感銘を受けるとともに[「プログラマー脳 ～優れたプログラマーになるための認知科学に基づくアプローチ」](https://www.shuwasystem.co.jp/book/9784798068534.html)との関連性を感じました。それぞれ今年に出版された書籍です。

認知科学に基づくアプローチとロバストを組み合わせることで、Pythonコードのロバストを向上させるための方法を探求します。

## 認知科学
[**認知科学**](https://ja.wikipedia.org/wiki/%E8%AA%8D%E7%9F%A5%E7%A7%91%E5%AD%A6)の分野では、記憶に関する研究が現在も盛んに行われています。

コーディングなどの知的生産な活動は、脳の[**ワーキングメモリ**](https://ja.wikipedia.org/wiki/%E3%83%AF%E3%83%BC%E3%82%AD%E3%83%B3%E3%82%B0%E3%83%A1%E3%83%A2%E3%83%AA)の働きや記憶の仕組みが大きく関係しています。

従って脳のワーキングメモリを強化し、効率よく記憶することで、知的生産性を向上することができるのではないでしょうか。

プログラマー脳の本でも記憶について触れられていますが、少し深掘りしてみましょう。

### 記憶の仕組み
記憶は陳述記憶と、非陳述記憶の2つに大別できます。

**陳述記憶**は、意味記憶やエピソード記憶など言語化可能な記憶に分類されます。
**非陳述記憶**は、手続き記憶など言語化しにくい記憶として分類されます。例えば、手続き記憶は箸の持ち方や自転車の乗り方など体を使って覚える記憶になるため、陳述記憶と比べて忘れにくい性質があります。

知的生産な活動に関する陳述記憶の仕組みについて、以下の図に示します。

![kiokunosikumi.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221759/83cd780a-0d36-a717-0b7a-81ab31439285.png)

人間の脳は、目や耳など感覚器官を通して入ってきた情報を基に[**知覚**](https://ja.wikipedia.org/wiki/%E7%9F%A5%E8%A6%9A)することによって、外界の情報を認識します。

知覚によって認識した情報を記憶するためには、短期記憶から長期記憶へと海馬に保存されます。なお、エピソード記憶については、時間経過とともに大脳皮質に移動し、そこで遠隔記憶として固定されると考えられてきた[^1]ようです。

::: note
短期記憶や長期記憶を一つの脳領域、特に海馬だけに結びつけるのは簡略化しすぎると言えるのではないでしょうか。記憶は複数の脳領域が関与する複雑なプロセスの結果として生じます。
:::

人間が記憶をする際や記憶から情報を取り出す仕組みは、コンピュータがハードディスクにデータを保存したり、保存したデータを取り出す仕組みに似ています。

人間の場合は、短期記憶から長期記憶に転送するために、頭の中で何度も唱えるなど**リハーサル**を行うことで、長期記憶に定着しやすいと言われています。また、普段の睡眠によって記憶の整理が行われています。

::: note
効率よく長期記憶として保存するためには、短期記憶をエピソード記憶と関連付けさせて記憶するのが良いそうです。理由として、感情の動きによって扁桃体が活発化することで、海馬に影響すると言われています。
:::

[^1]: [古い記憶を呼び起こす脳部位間コミュニケーション](https://www.riken.jp/press/2019/20191218_1/index.html)

### 記憶のテクニック
試験勉強の暗記などで容量よく記憶するためには、いかに**脳を騙す**かがポイントです。

脳を騙すというのは、覚えたい情報に対してこれは重要な情報だと伝えることです。

現代人は、ポッドキャストを聴いたり動画を見ながらなどマルチタスク作業をすることが多いと思います。しかし、内容を十分に理解した上で、マルチタスク作業の質を上げることは難しいと思います。理由として人間の脳は、マルチタスクよりシングルタスクの方が向いているからです。従って複数のタスクを行っている状況では、脳は重要な情報と認識できないため、記憶にも影響します。

寝る直前までスマートフォンを見るのも良くありません。
寝る直前までに試験勉強として暗記行為を行なっても、寝る直前に見たスマートフォンの情報がノイズとなり、脳からするとどの情報が重要か判断に影響が出るようです。

男性と女性では、脳の視覚や聴覚に差異があると言われているようです。
このような脳の複雑な性質を理解することが、記憶力を保持したり向上することの秘訣だと思いました。

https://www.lifehacker.jp/article/2307-keep-forgetting-things-neuroscience-says-these-4-simple-routines-significantly-boost-recall-retention-long-term-memory/

https://www.lifehacker.jp/article/130929memory_works/

::: note
記憶の定着を図るためには、本を読むなどのインプットより、覚えたことを人に話すなどアウトプットよって、記憶から思いだす行為（想起）を行うのが良いそうです。
:::

### 処理能力の不足
プログラマー脳の本では、認知科学の観点から脳がどのようにコードを処理するかについて書かれています。

プログラミングを行なうにあたり、コードの内容が分からないことは、誰でも経験することだと思います。プログラマー脳の本では、コードの内容が分からない状態について、日本語訳で**コーディング中の混乱**と表現されています。

プログラマーを混乱させる要因については、以下のようなものが存在します。
特にロバストに大きく関係するのは、「**処理能力の不足**」ではないでしょうか。

- 知識不足
    - 長期記憶に関連する情報が存在しないため、プログラミングの考え方や言語特有の記法が理解できていない
- 情報不足
    - 短期記憶に関する問題として、大量のコードを読む場合は、過去に読んだコードの内容を忘れてしまうため、プログラミングの考え方は理解しているものの、認知機能に影響が出る
- 処理能力の不足
    - ワーキングメモリに対する負荷がかかるため、コードの処理について推測は理解できるが、変数や関数の処理内容について、正当性を評価できないなど、思考に影響を与える

短期記憶は、色々な書籍を見ても何十秒程度の容量しかないどの記述を見かけます。例えば、コードの変数で`i`などが使われている場合、そのコードを理解するためには、コードを読んでいる間、頭の中で`i`の意味する値を覚えておく必要があります。また、Pythonは動的型付け言語であるということを踏まえて、データ型を意識する必要があります。

従って変数に分かりやすい変数名を付けたり、必要に応じて型ヒントを付けることで、認知負荷を下げることができます。認知負荷を下げるということは、脳のワーキングメモリに対する負荷を下げると同等ではないでしょうか。

::: note
ロバストなコードを書くことは、ワーキングメモリに対する負荷を下げることだと考えています。
:::

## ロバスト
[**ロバストネス**](https://ja.wikipedia.org/wiki/%E3%83%AD%E3%83%90%E3%82%B9%E3%83%88%E3%83%8D%E3%82%B9)は、頑強性（がんきょうせい）、頑健性（がんけんせい）、堅牢性（けんろうせい）などを意味します。[メリアム＝ウェブスター](https://ja.wikipedia.org/wiki/%E3%83%A1%E3%83%AA%E3%82%A2%E3%83%A0%EF%BC%9D%E3%82%A6%E3%82%A7%E3%83%96%E3%82%B9%E3%82%BF%E3%83%BC)の[robust](https://www.merriam-webster.com/dictionary/robust)からも同様の意味が確認できます。

ソフトウェアのロバストとは、強いシステムを作るためには柔軟性が必要であり、柔軟性とはコードのメンテナンス性を意味します。
従ってロバストネスを実現するためには、**クリーンで保守の高い**コードを書く必要があるということです。また、なぜクリーンで保守の高いコードを書くのかの問いに対する答えは、コミュニケーションにあると書かれています。

### 型アノテーション
Pythonの**型アノテーション**は、Python 3.5[^2]で導入されました。

型アノテーションは、期待するデータ型を明確にすることを目的に型ヒントして機能しますが、制約を与える訳ではありません。そのため、型アノテーションに反してもエラーは発生しません。

```python
# 変数
str1: str = "Hello, world!"

# 関数
def add(x: int, y: int) -> int:
  return x + y

def foo(x: List[int]) -> List[str]:
  return [str(i) for i in x]

# クラス
class Person(object):
  name: str
  age: int
```

インタプリンタを起動して上記コードを試してみましょう。
このようにstr（文字型）を無視して、int（整数型）を入れることができます。

```python
>>> str1: str = "Hello, world!"
>>> str1: str = 123
```

::: note
ロバストPythonの本を読むまでは全て型アノテーションをした方が良いと思い込んでましたが、関数にはすべて型アノテーションをつけて、変数については特別な事情がない限り、型アノテーションは行わないと書かれていました。従ってなんでもかんでも型アノテーションを行うのではなく、冗長な書き方をしないようにすることがポイントです。
:::

[^2]: [PEP 484 – Type Hints](https://peps.python.org/pep-0484/)

#### 型チェッカ
型アノテーションは認知負荷を下げる効果がありますが、**型チェッカ**と組み合わせることで、よりコードを安全にします。

Pythonは動的言語であるため、通常はコードを実行しようとしたときにのみコード内にエラーが表示されます。型チェッカなど静的チェッカを利用することで、プログラムを実行しなくてもプログラム内のバグを検出します。

**mypy**はPythonの静的型チェッカーです。mypyを使うためには、以下のコマンドを実行してインストールします。

`$ pip3 install mypy`

例として以下のテスト用ファイルを作成します。

- test.py

```python
str1: str = "Hello, world!"
str1 = 123
```

以下のコマンドを実行すると、スクリプトはエラーを返します。このように型チェッカは、コード内で変数や関数が正しく使用されていることを確認するのに役立ちます。

`$ mypy test.py`

```console
test.py:2: error: Incompatible types in assignment (expression has type "int", variable has type "str")  [assignment]
Found 1 error in 1 file (checked 1 source file)
```

:::note
ロバストPythonの本では型チェッカのカスタマイズについて、mypyの設定やその他の型チェッカについて紹介されています。また、型アノテーションは役に立つが、コストもかかるのでトレードオフであることや対象コードの戦略的な選択の考え方が重要であると謳っています。
:::

### 型制約
高度な型アノテーションを使用してデータ型に制約を与えることで、プログラムの意図しない状態を明確にすることができます。

本記事では**型制約**のテクニックの内、Optionalについて記載しています。

null参照を防ぐために、防御的プログラミングを行ない、`if key is None`のようなnullチェックを多用して実装する場合、コードは読み見にくくなります。従ってOptionalは、コード内のNone参照を置き換えるために使用されます。

Optionalの最大のメリットは、コードの意図をより明確に伝えることができます。例えば、関数の戻り値がNoneを返す可能性がある場合、Optional型を使うことで、Noneを返すことを明示的に示すことができます。

これにより、予期しないエラーの発生を防ぐことができます。また、関数が返す値が存在しない場合に備えて、適切なエラーハンドリングを行うことができます。

```python
from typing import Optional

def get_user_name(user_id: int) -> Optional[str]:
    user = db.get_user(user_id)
    if user:
        return user.name
    else:
        return None
```

::: note
Python 3.10からOptionalと書かなくても、`X | None`のように書けるようになりました。詳細は公式ドキュメントの[typing.Optional](https://docs.python.org/ja/3/library/typing.html#typing.Optional)をご参照ください。また、公式ドキュメントの[特殊形式](https://docs.python.org/ja/3/library/typing.html?highlight=optional#special-forms)を参照すると、ロバストPythonの本には記載されていない他の型制約の種類が確認できます。
:::

### データクラス
[**データクラス**](https://docs.python.org/ja/3/library/dataclasses.html?highlight=class#module-dataclasses)は、Python 3.7で導入されました。

ユーザー定義のクラスに自動的に追加するデコレータや関数を提供します。データクラスのメリットは以下の通りです。

- コードの簡潔さと可読性
    - データクラスを使用することで、従来のinitで初期化する書き方が不要になるため、シンプルに記述することができます。また、組み込みの関数を用いてデータクラスのインスタンス同士の比較などができます。
- イミュータブル
    - デフォルトでイミュータブル（変更不可）なデータクラスを生成することができます。イミュータブルなデータクラスは、データの不変性とスレッドセーフ性を提供します。
- 型ヒント
    - データクラスは型ヒントと組み合わせて使用することができます。


以下はデータクラスを用いてPoint（座標）クラスを定義する例です。

```python
from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

p1 = Point(2, 3)
p2 = Point(2, 3)

print(p1 == p2)
```

自動的に`__eq__`や`__ne__`メソッドが定義されるため、インスタンスの属性値の比較が簡単にできます。

```console
True
``` 

::: note
eq(a, b) は a == b 、 ne(a, b) は a != b
等価比較のデフォルトの動作を変更したい場合は、特殊メソッドをオーバライドします。
:::

### クラス
オブジェクト思考の考え方として、[**クラス**](https://docs.python.org/ja/3/tutorial/classes.html)の設計を基にインスタンスを生成し、振る舞い（メソッド）と属性（データ）を定義することがクラスの性質です。

データクラスではなくクラスを使う場合のポイントは、**不変式**だということをロバストPythonの本を読んで学び直しました。

不変式は、プログラミングでは常に真である条件を示すなど数学的な概念で使われます。以下に不変式の例を記載します。

クラスでインスタンスを初期化する際は、初期化メソッド`__init__()`を用いて、`self`に必要な引数を渡して制御します。

以下の例では、車のクラスとして、`Car`クラスを作成しています。
`accelerate`メソッドによって最高速度を超える場合は、最高速度に制限されます。また、`check_invariant`メソッドでは不変式をチェックし、速度が0以上かつ最高速度以下であることを確認しています。

```python
class Car:
    def __init__(self, make, model, year, color, max_speed):
        self.make = make
        self.model = model
        self.year = year
        self.color = color
        self.speed = 0
        self.max_speed = max_speed

    def accelerate(self, amount):
        if self.speed + amount <= self.max_speed:
            self.speed += amount
        else:
            self.speed = self.max_speed

    def brake(self, amount):
        self.speed -= amount
        if self.speed < 0:
            self.speed = 0

    def get_speed(self):
        return self.speed

    def __str__(self):
        return f"{self.color} {self.year} {self.make} {self.model}"

    def check_invariant(self):
        assert self.speed >= 0, "Speed cannot be negative."
        assert self.speed <= self.max_speed, "Speed cannot exceed the max speed."

my_car = Car("Toyota", "Corolla", 2022, "Red", max_speed=200)

# 不変式をチェックする
my_car.check_invariant()

print(my_car)  # Output: Red 2022 Toyota Corolla

my_car.accelerate(180)
print(my_car.get_speed())  # Output: 180

my_car.accelerate(50)  # 最高速度を超える加速
print(my_car.get_speed())  # Output: 200 (最高速度に制限)

# 不変式を再度チェックする
my_car.check_invariant()
```

`accelerate`メソッドで最高速度を越えようとしても制御されていることが確認できます。

```console
Red 2022 Toyota Corolla
180
200
```

なお、インスタンスのプロパティは操作可能なため、最高速度を越える値を設定することができます。

```python
my_car.speed = 300
my_car.check_invariant()
```

この場合、以下のように`AssertionError`が発生します。

```console
Traceback (most recent call last):
  File "/Users/python/Desktop/car.py", line 49, in <module>
    my_car.check_invariant()
  File "/Users/python/Desktop/car.py", line 29, in check_invariant
    assert self.speed <= self.max_speed, "Speed cannot exceed the max speed."
AssertionError: Speed cannot exceed the max speed.
```

# おわりに
今年のはじめに[「PythonZen & PEP 8 検定試験で学ぶ　Pythonのするべきことではないこと」](https://qiita.com/Brutus/items/94fe23153a922f76c646)の記事を書きました。

禅の考え方を踏まえて「するべきことではない」という考え方を身に付けることで、コードの保守性を高めることができます。

まとめるとロバストなコードを書くためには、認知科学に基づくアプローチや禅など物事の本質となる考え方を養うことが重要だと思いました。