はじめに
本記事は認知科学に基づくアプローチを踏まえて、ロバストなPythonコードを書くための基本的なテクニックについてまとめています。
「ロバストPython―クリーンで保守しやすいコードを書く」を読み終えて、ロバストの重要性に感銘を受けるとともに「プログラマー脳 ~優れたプログラマーになるための認知科学に基づくアプローチ」との関連性を感じました。それぞれ今年に出版された書籍です。
認知科学に基づくアプローチとロバストを組み合わせることで、Pythonコードのロバストを向上させるための方法を探求します。
認知科学
認知科学の分野では、記憶に関する研究が現在も盛んに行われています。
コーディングなどの知的生産な活動は、脳のワーキングメモリの働きや記憶の仕組みが大きく関係しています。
従って脳のワーキングメモリを強化し、効率よく記憶することで、知的生産性を向上することができるのではないでしょうか。
プログラマー脳の本でも記憶について触れられていますが、少し深掘りしてみましょう。
記憶の仕組み
記憶は陳述記憶と、非陳述記憶の2つに大別できます。
陳述記憶は、意味記憶やエピソード記憶など言語化可能な記憶に分類されます。
非陳述記憶は、手続き記憶など言語化しにくい記憶として分類されます。例えば、手続き記憶は箸の持ち方や自転車の乗り方など体を使って覚える記憶になるため、陳述記憶と比べて忘れにくい性質があります。
知的生産な活動に関する陳述記憶の仕組みについて、以下の図に示します。
人間の脳は、目や耳など感覚器官を通して入ってきた情報を基に知覚することによって、外界の情報を認識します。
知覚によって認識した情報を記憶するためには、短期記憶から長期記憶へと海馬に保存されます。なお、エピソード記憶については、時間経過とともに大脳皮質に移動し、そこで遠隔記憶として固定されると考えられてきた1ようです。
短期記憶や長期記憶を一つの脳領域、特に海馬だけに結びつけるのは簡略化しすぎると言えるのではないでしょうか。記憶は複数の脳領域が関与する複雑なプロセスの結果として生じます。
人間が記憶をする際や記憶から情報を取り出す仕組みは、コンピュータがハードディスクにデータを保存したり、保存したデータを取り出す仕組みに似ています。
人間の場合は、短期記憶から長期記憶に転送するために、頭の中で何度も唱えるなどリハーサルを行うことで、長期記憶に定着しやすいと言われています。また、普段の睡眠によって記憶の整理が行われています。
効率よく長期記憶として保存するためには、短期記憶をエピソード記憶と関連付けさせて記憶するのが良いそうです。理由として、感情の動きによって扁桃体が活発化することで、海馬に影響すると言われています。
記憶のテクニック
試験勉強の暗記などで容量よく記憶するためには、いかに脳を騙すかがポイントです。
脳を騙すというのは、覚えたい情報に対してこれは重要な情報だと伝えることです。
現代人は、ポッドキャストを聴いたり動画を見ながらなどマルチタスク作業をすることが多いと思います。しかし、内容を十分に理解した上で、マルチタスク作業の質を上げることは難しいと思います。理由として人間の脳は、マルチタスクよりシングルタスクの方が向いているからです。従って複数のタスクを行っている状況では、脳は重要な情報と認識できないため、記憶にも影響します。
寝る直前までスマートフォンを見るのも良くありません。
寝る直前までに試験勉強として暗記行為を行なっても、寝る直前に見たスマートフォンの情報がノイズとなり、脳からするとどの情報が重要か判断に影響が出るようです。
男性と女性では、脳の視覚や聴覚に差異があると言われているようです。
このような脳の複雑な性質を理解することが、記憶力を保持したり向上することの秘訣だと思いました。
記憶の定着を図るためには、本を読むなどのインプットより、覚えたことを人に話すなどアウトプットよって、記憶から思いだす行為(想起)を行うのが良いそうです。
処理能力の不足
プログラマー脳の本では、認知科学の観点から脳がどのようにコードを処理するかについて書かれています。
プログラミングを行なうにあたり、コードの内容が分からないことは、誰でも経験することだと思います。プログラマー脳の本では、コードの内容が分からない状態について、日本語訳でコーディング中の混乱と表現されています。
プログラマーを混乱させる要因については、以下のようなものが存在します。
特にロバストに大きく関係するのは、「処理能力の不足」ではないでしょうか。
- 知識不足
- 長期記憶に関連する情報が存在しないため、プログラミングの考え方や言語特有の記法が理解できていない
- 情報不足
- 短期記憶に関する問題として、大量のコードを読む場合は、過去に読んだコードの内容を忘れてしまうため、プログラミングの考え方は理解しているものの、認知機能に影響が出る
- 処理能力の不足
- ワーキングメモリに対する負荷がかかるため、コードの処理について推測は理解できるが、変数や関数の処理内容について、正当性を評価できないなど、思考に影響を与える
短期記憶は、色々な書籍を見ても何十秒程度の容量しかないどの記述を見かけます。例えば、コードの変数でi
などが使われている場合、そのコードを理解するためには、コードを読んでいる間、頭の中でi
の意味する値を覚えておく必要があります。また、Pythonは動的型付け言語であるということを踏まえて、データ型を意識する必要があります。
従って変数に分かりやすい変数名を付けたり、必要に応じて型ヒントを付けることで、認知負荷を下げることができます。認知負荷を下げるということは、脳のワーキングメモリに対する負荷を下げると同等ではないでしょうか。
ロバストなコードを書くことは、ワーキングメモリに対する負荷を下げることだと考えています。
ロバスト
ロバストネスは、頑強性(がんきょうせい)、頑健性(がんけんせい)、堅牢性(けんろうせい)などを意味します。メリアム=ウェブスターのrobustからも同様の意味が確認できます。
ソフトウェアのロバストとは、強いシステムを作るためには柔軟性が必要であり、柔軟性とはコードのメンテナンス性を意味します。
従ってロバストネスを実現するためには、クリーンで保守の高いコードを書く必要があるということです。また、なぜクリーンで保守の高いコードを書くのかの問いに対する答えは、コミュニケーションにあると書かれています。
型アノテーション
Pythonの型アノテーションは、Python 3.52で導入されました。
型アノテーションは、期待するデータ型を明確にすることを目的に型ヒントして機能しますが、制約を与える訳ではありません。そのため、型アノテーションに反してもエラーは発生しません。
# 変数
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(整数型)を入れることができます。
>>> str1: str = "Hello, world!"
>>> str1: str = 123
ロバストPythonの本を読むまでは全て型アノテーションをした方が良いと思い込んでましたが、関数にはすべて型アノテーションをつけて、変数については特別な事情がない限り、型アノテーションは行わないと書かれていました。従ってなんでもかんでも型アノテーションを行うのではなく、冗長な書き方をしないようにすることがポイントです。
型チェッカ
型アノテーションは認知負荷を下げる効果がありますが、型チェッカと組み合わせることで、よりコードを安全にします。
Pythonは動的言語であるため、通常はコードを実行しようとしたときにのみコード内にエラーが表示されます。型チェッカなど静的チェッカを利用することで、プログラムを実行しなくてもプログラム内のバグを検出します。
mypyはPythonの静的型チェッカーです。mypyを使うためには、以下のコマンドを実行してインストールします。
$ pip3 install mypy
例として以下のテスト用ファイルを作成します。
- test.py
str1: str = "Hello, world!"
str1 = 123
以下のコマンドを実行すると、スクリプトはエラーを返します。このように型チェッカは、コード内で変数や関数が正しく使用されていることを確認するのに役立ちます。
$ mypy test.py
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)
ロバストPythonの本では型チェッカのカスタマイズについて、mypyの設定やその他の型チェッカについて紹介されています。また、型アノテーションは役に立つが、コストもかかるのでトレードオフであることや対象コードの戦略的な選択の考え方が重要であると謳っています。
型制約
高度な型アノテーションを使用してデータ型に制約を与えることで、プログラムの意図しない状態を明確にすることができます。
本記事では型制約のテクニックの内、Optionalについて記載しています。
null参照を防ぐために、防御的プログラミングを行ない、if key is None
のようなnullチェックを多用して実装する場合、コードは読み見にくくなります。従ってOptionalは、コード内のNone参照を置き換えるために使用されます。
Optionalの最大のメリットは、コードの意図をより明確に伝えることができます。例えば、関数の戻り値がNoneを返す可能性がある場合、Optional型を使うことで、Noneを返すことを明示的に示すことができます。
これにより、予期しないエラーの発生を防ぐことができます。また、関数が返す値が存在しない場合に備えて、適切なエラーハンドリングを行うことができます。
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
Python 3.10からOptionalと書かなくても、X | None
のように書けるようになりました。詳細は公式ドキュメントのtyping.Optionalをご参照ください。また、公式ドキュメントの特殊形式を参照すると、ロバストPythonの本には記載されていない他の型制約の種類が確認できます。
データクラス
データクラスは、Python 3.7で導入されました。
ユーザー定義のクラスに自動的に追加するデコレータや関数を提供します。データクラスのメリットは以下の通りです。
- コードの簡潔さと可読性
- データクラスを使用することで、従来のinitで初期化する書き方が不要になるため、シンプルに記述することができます。また、組み込みの関数を用いてデータクラスのインスタンス同士の比較などができます。
- イミュータブル
- デフォルトでイミュータブル(変更不可)なデータクラスを生成することができます。イミュータブルなデータクラスは、データの不変性とスレッドセーフ性を提供します。
- 型ヒント
- データクラスは型ヒントと組み合わせて使用することができます。
以下はデータクラスを用いてPoint(座標)クラスを定義する例です。
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
p1 = Point(2, 3)
p2 = Point(2, 3)
print(p1 == p2)
自動的に__eq__
や__ne__
メソッドが定義されるため、インスタンスの属性値の比較が簡単にできます。
True
eq(a, b) は a == b 、 ne(a, b) は a != b
等価比較のデフォルトの動作を変更したい場合は、特殊メソッドをオーバライドします。
クラス
オブジェクト思考の考え方として、クラスの設計を基にインスタンスを生成し、振る舞い(メソッド)と属性(データ)を定義することがクラスの性質です。
データクラスではなくクラスを使う場合のポイントは、不変式だということをロバストPythonの本を読んで学び直しました。
不変式は、プログラミングでは常に真である条件を示すなど数学的な概念で使われます。以下に不変式の例を記載します。
クラスでインスタンスを初期化する際は、初期化メソッド__init__()
を用いて、self
に必要な引数を渡して制御します。
以下の例では、車のクラスとして、Car
クラスを作成しています。
accelerate
メソッドによって最高速度を超える場合は、最高速度に制限されます。また、check_invariant
メソッドでは不変式をチェックし、速度が0以上かつ最高速度以下であることを確認しています。
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
メソッドで最高速度を越えようとしても制御されていることが確認できます。
Red 2022 Toyota Corolla
180
200
なお、インスタンスのプロパティは操作可能なため、最高速度を越える値を設定することができます。
my_car.speed = 300
my_car.check_invariant()
この場合、以下のようにAssertionError
が発生します。
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のするべきことではないこと」の記事を書きました。
禅の考え方を踏まえて「するべきことではない」という考え方を身に付けることで、コードの保守性を高めることができます。
まとめるとロバストなコードを書くためには、認知科学に基づくアプローチや禅など物事の本質となる考え方を養うことが重要だと思いました。