Python 3: Deep Dive (Part 2 - Iterators, Generators): プロジェクト②(セクション5/14)
Pythonの`Polygon`クラスを改善し、計算プロパティを一度だけ計算して保存する遅延プロパティとして実装することで、パフォーマンスを向上させました。
`Polygons`クラスをイテラブルにリファクタリングし、ポリゴンを必要なときに生成する遅延評価を実装することで、メモリ効率を改善しました。
これらの最適化により、大規模なデータセットを扱う場合でも効率的に動作し、よりPythonicなコードとなりました。
このブログ記事では、Pythonの`Polygon`クラスと`Polygons`シーケンスタイプのリファクタリングプロジェクトについて詳しく説明します。このプロジェクトは、Python 3: Deep Dive (Part 2 - イテレータ、ジェネレータ)コースのセクション5、プロジェクト2の一部です。パフォーマンスと効率を向上させるために、遅延プロパティとイテラブルを実装する方法を探ります。
プロジェクト概要
私たちの出発点は、以前のプロジェクトで作成した`Polygon`クラスと`Polygons`シーケンスタイプです。このプロジェクトの主な目的は以下の通りです:
目標1:`Polygon`クラスをリファクタリングし、すべての計算されるプロパティを遅延プロパティにすること。これは、プロパティが一度計算され、将来の使用のためにキャッシュされ、冗長な計算を避けることを意味します。
目標2:`Polygons`シーケンスタイプをイテラブルにリファクタリングすること。さらに、ポリゴンが遅延計算されるようにすること。つまり、すべてのポリゴンをリストに事前計算して保存しないようにします。
それぞれの目標について詳しく見ていきましょう。
目標1:`Polygon`クラスにおける遅延プロパティの実装
元の`Polygon`クラス
まず、`Polygon`クラスの元の実装を見てみましょう:
import math
class Polygon:
def __init__(self, n, R):
if n < 3:
raise ValueError('Polygon must have at least 3 sides.')
self._n = n # 辺の数
self._R = R # 外接円の半径
def __repr__(self):
return f'Polygon(n={self._n}, R={self._R})'
@property
def count_vertices(self):
return self._n
@property
def count_edges(self):
return self._n
@property
def circumradius(self):
return self._R
@property
def interior_angle(self):
return (self._n - 2) * 180 / self._n
@property
def side_length(self):
return 2 * self._R * math.sin(math.pi / self._n)
@property
def apothem(self):
return self._R * math.cos(math.pi / self._n)
@property
def area(self):
return self._n / 2 * self.side_length * self.apothem
@property
def perimeter(self):
return self._n * self.side_length
def __eq__(self, other):
if isinstance(other, Polygon):
return (self.count_edges == other.count_edges and
self.circumradius == other.circumradius)
else:
return NotImplemented
def __gt__(self, other):
if isinstance(other, Polygon):
return self.count_vertices > other.count_vertices
else:
return NotImplemented
この実装では、`area`、`perimeter`、`interior_angle`などのプロパティにアクセスするたびに、入力が変更されていなくても値が再計算されます。`Polygon`インスタンスは不変(辺の数`n`と外接円の半径`R`は初期化後に変更されない)なので、これらの計算されたプロパティをキャッシュすることでクラスを最適化できます。
遅延プロパティへのリファクタリング
遅延プロパティを実装するために:
計算された値を保存するためのプライベート変数を導入します。
プロパティメソッドを修正し、値が既に計算されてキャッシュされているかどうかをチェックします。そうでない場合は、計算して保存します。
以下がリファクタリングされた`Polygon`クラスです:
import math
class Polygon:
def __init__(self, n, R):
if n < 3:
raise ValueError('Polygon must have at least 3 sides.')
self._n = n # 辺の数
self._R = R # 外接円の半径
# 遅延プロパティ用のプライベート変数
self._interior_angle = None
self._side_length = None
self._apothem = None
self._area = None
self._perimeter = None
def __repr__(self):
return f'Polygon(n={self._n}, R={self._R})'
@property
def count_vertices(self):
return self._n
@property
def count_edges(self):
return self.count_vertices # 頂点の数と同じ
@property
def circumradius(self):
return self._R
@property
def interior_angle(self):
if self._interior_angle is None:
self._interior_angle = (self._n - 2) * 180 / self._n
return self._interior_angle
@property
def side_length(self):
if self._side_length is None:
self._side_length = 2 * self._R * math.sin(math.pi / self._n)
return self._side_length
@property
def apothem(self):
if self._apothem is None:
self._apothem = self._R * math.cos(math.pi / self._n)
return self._apothem
@property
def area(self):
if self._area is None:
self._area = self._n / 2 * self.side_length * self.apothem
return self._area
@property
def perimeter(self):
if self._perimeter is None:
self._perimeter = self._n * self.side_length
return self._perimeter
def __eq__(self, other):
if isinstance(other, Polygon):
return (self.count_edges == other.count_edges and
self.circumradius == other.circumradius)
else:
return NotImplemented
def __gt__(self, other):
if isinstance(other, Polygon):
return self.count_vertices > other.count_vertices
else:
return NotImplemented
遅延プロパティの仕組み
リファクタリングされたクラスでは:
各計算プロパティは、対応するプライベート変数が`None`かどうかをチェックします。
`None`の場合、値を計算してプライベート変数に保存します。
その後のプロパティへのアクセスでは、冗長な計算を避けるためにキャッシュされた値を返します。
リファクタリングされた`Polygon`クラスのテスト
リファクタリングされたクラスが期待通りに動作することを確認しましょう。以下のテスト関数を使用します:
def test_polygon():
import math
rel_tol = 0.001
abs_tol = 0.001
try:
p = Polygon(2, 10)
assert False, '3辺未満のポリゴンを作成するとValueErrorが発生するはずです'
except ValueError:
pass
n = 3
R = 1
p = Polygon(n, R)
assert str(p) == 'Polygon(n=3, R=1)', f'実際: {str(p)}'
assert p.count_vertices == n
assert p.count_edges == n
assert p.circumradius == R
assert math.isclose(p.interior_angle, 60, rel_tol=rel_tol, abs_tol=abs_tol)
n = 4
p = Polygon(n, R)
assert math.isclose(p.interior_angle, 90, rel_tol=rel_tol, abs_tol=abs_tol)
assert math.isclose(p.area, 2.0, rel_tol=rel_tol, abs_tol=abs_tol)
assert math.isclose(p.side_length, math.sqrt(2), rel_tol=rel_tol, abs_tol=abs_tol)
assert math.isclose(p.perimeter, 4 * math.sqrt(2), rel_tol=rel_tol, abs_tol=abs_tol)
assert math.isclose(p.apothem, 0.7071, rel_tol=rel_tol, abs_tol=abs_tol)
print("すべてのテストに合格しました!")
test_polygon()
出力:
すべてのテストに合格しました!
目標2:遅延評価を伴う`Polygons`のイテラブルへのリファクタリング
元の`Polygons`シーケンスタイプ
以前は、`Polygons`クラスはシーケンスタイプとして実装され、すべてのポリゴンを事前計算してリストに保存していました:
class Polygons:
def __init__(self, m, R):
if m < 3:
raise ValueError('mは3より大きくなければなりません')
self._m = m # 最大辺数
self._R = R # 共通の外接円半径
self._polygons = [Polygon(i, R) for i in range(3, m+1)]
def __len__(self):
return self._m - 2
def __repr__(self):
return f'Polygons(m={self._m}, R={self._R})"
def __getitem__(self, s):
return self._polygons[s]
@property
def max_efficiency_polygon(self):
sorted_polygons = sorted(
self._polygons,
key=lambda p: p.area / p.perimeter,
reverse=True
)
return sorted_polygons[0]
この実装は、`m`が大きい値の場合、すべてのポリゴンインスタンスを事前に保存するためメモリ効率が良くありません。
遅延評価を伴うイテラブルへのリファクタリング
`Polygons`クラスをよりメモリ効率の良いものにするために:
`iter`メソッドを実装してイテラブルに変換します。
別個の`PolygonsIterator`クラスを作成します。
イテレーション中にポリゴンが遅延生成されるようにします。
`PolygonsIterator`クラスの作成
まず、イテレータクラスを定義します:
class PolygonsIterator:
def __init__(self, m, R):
self._m = m # 最大辺数
self._R = R # 共通の外接円半径
self._current = 3 # 開始辺数
def __iter__(self):
return self
def __next__(self):
if self._current > self._m:
raise StopIteration
else:
polygon = Polygon(self._current, self._R)
self._current += 1
return polygon
`Polygons`クラスの更新
次に、イテレータを使用するように`Polygons`クラスを更新します:
class Polygons:
def __init__(self, m, R):
if m < 3:
raise ValueError('mは3より大きくなければなりません')
self._m = m # 最大辺数
self._R = R # 共通の外接円半径
def __len__(self):
return self._m - 2
def __repr__(self):
return f'Polygons(m={self._m}, R={self._R})"
def __iter__(self):
return PolygonsIterator(self._m, self._R)
@property
def max_efficiency_polygon(self):
return max(
PolygonsIterator(self._m, self._R),
key=lambda p: p.area / p.perimeter
)
この文脈における遅延評価の仕組み
イテレーション:`PolygonsIterator`は、すべてを事前に保存するのではなく、イテレーション中に`Polygon`インスタンスをその場で生成します。
最大効率ポリゴン:`max_efficiency_polygon`プロパティは、すべてのポリゴンを事前に保存することなく、面積対周長比が最大のポリゴンを計算します。
リファクタリングされた`Polygons`クラスの使用
リファクタリングされた`Polygons`クラスの使用方法を見てみましょう:
polygons = Polygons(5, 1)
for polygon in polygons:
print(polygon)
出力:
Polygon(n=3, R=1)
Polygon(n=4, R=1)
Polygon(n=5, R=1)
複数回イテレーションしても、ポリゴンはその都度遅延生成されます:
for polygon in polygons:
print(polygon)
for polygon in polygons:
print(polygon)
最大効率ポリゴンの検索
max_eff_polygon = polygons.max_efficiency_polygon
print(f"面積/周長比が最も高いポリゴンは: {max_eff_polygon}")
出力:
面積/周長比が最も高いポリゴンは: Polygon(n=5, R=1)
結論
`Polygon`クラスと`Polygons`クラスをリファクタリングすることで、以下を達成しました:
最適化された計算:`Polygon`クラスで遅延プロパティを使用することで、冗長な計算を避け、パフォーマンスを向上させました。
メモリ効率:`Polygons`を遅延評価を伴うイテラブルに変換することで、特に多数のポリゴンを扱う場合のメモリ使用量を削減しました。
Pythonプロトコルの遵守:イテレータプロトコルを実装することで、私たちのクラスをよりPythonicで柔軟なものにしました。
これらの変更は、コードをより効率的にするだけでなく、スケーラビリティと保守性も向上させます。遅延プロパティとイテラブルの実装は、特に計算負荷の高い操作や大規模なデータセットを扱う場合に、Pythonプログラミングにおいて価値のある技術です。
ハッピーコーディング!