見出し画像

Python 3: Deep Dive (Part 2 - Iterators, Generators): 呼び出し可能とセンチネル (セクション4-4/14)

  • Python の組み込みイテラブルとイテレータの違いを解説し、遅延評価の利点を説明している。

  • `iter()` 関数の深い使い方、特に呼び出し可能オブジェクトとセンチネル値を用いたイテレータ作成を紹介している。

  • 例外処理のアプローチ(EAFP vs LBYL)を比較し、より Pythonic なコーディングスタイルについて議論している。

Python のイテラブルとイテレータについての継続的な探求において、Python の組み込みイテラブルとイテレータ、`iter()` 関数、および呼び出し可能オブジェクトに対する反復処理に焦点を当てる重要な部分に到達しました。このブログ記事は、Udemy コース Python 3: Deep Dive (Part 2 - Iterators, Generators) のセクション4のレッスン44から50までの内容をまとめたものです。

Python の組み込みイテラブルとイテレータの理解

イテラブルとイテレータの違い

Python の組み込みイテラブルとイテレータに深入りする前に、両者の違いを再確認しましょう:

  • イテラブル:メンバーを一度に1つずつ返すことができるオブジェクト。リスト、タプル、文字列などが例として挙げられます。イテラブルは `iter()` メソッドを実装しています。

  • イテレータ:データのストリームを表すオブジェクト。イテラブルに対して `iter()` を呼び出すことで返されます。イテレータは `iter()` と `next()` の両方のメソッドを実装しています。

なぜこの区別が重要か

イテラブルとイテレータのどちらを扱っているかを知ることは重要です:

  • イテラブルは複数回反復処理できます。なぜなら、`iter()` を呼び出すたびに新しいイテレータを返すからです。

  • イテレータは一度だけ反復処理できます。一度使い尽くすと、リセットや再利用はできません。

遅延評価

Python の多くの組み込み関数は遅延評価を使用しています。これは、必要になった時点で値を計算することを意味します。このアプローチは、特に大規模なデータセットや潜在的に無限のシーケンスを扱う際にメモリを節約し、パフォーマンスを向上させます。

組み込みイテラブルとイテレータの検証

`range` 関数

`range` 関数は、遅延評価を使用するイテラブルの典型的な例です:

r = range(10)
print('__iter__' in dir(r))   # True
print('__next__' in dir(r))   # False
  • `range` は `iter()` を実装しているためイテラブルです。

  • `next()` を実装していないため、イテレータではありません。

`range` オブジェクトからイテレータを取得するには `iter()` を使用します:

r_iter = iter(r)
print('__next__' in dir(r_iter))  # True

`zip` 関数

`zip` 関数はイテレータを返します:

z = zip([1, 2, 3], 'abc')
print('__iter__' in dir(z))    # True
print('__next__' in dir(z))    # True
  • `zip` は `iter()` と `next()` の両方を実装しているため、イテレータです。

`enumerate` 関数

`zip` と同様に、`enumerate` 関数もイテレータを返します:

e = enumerate('Python')
print('__iter__' in dir(e))    # True
print('__next__' in dir(e))    # True

`open` 関数

`open` 関数を使ってファイルを開くと、ファイルの各行に対するイテレータが返されます:

with open('example.txt') as f:
    print('__iter__' in dir(f))    # True
    print('__next__' in dir(f))    # True

辞書のビュー

`keys()`、`values()`、`items()` などの辞書メソッドはイテラブルを返します:

d = {'a': 1, 'b': 2}
keys = d.keys()
print('__iter__' in dir(keys))     # True
print('__next__' in dir(keys))     # False
  • これらはイテラブルであり、イテレータではありません。

  • 複数回反復処理できます。

実践的な遅延評価

メモリ効率

遅延評価は大規模なデータセットを扱う際に有益です:

  • イテレータは要素をその場で計算し、シーケンス全体をメモリに保存しません。

  • イテレータを使用することで、不要なデータをメモリにロードすることを防ぎ、効率を向上させます。

例:大規模ファイルの読み込み

大規模なファイルを処理する際は、1行ずつ読み込むのがより効率的です:

origins = set()
with open('cars.csv') as f:
    next(f), next(f)  # ヘッダーをスキップ
    for row in f:
        origin = row.strip('\n').split(';')[-1]
        origins.add(origin)
print(origins)
  • このアプローチでは1行ずつ読み込み、メモリを節約します。

  • `readlines()` でファイル全体をリストに読み込むのは非効率的です。

`iter()` 関数の深掘り

Python における反復処理の仕組み

`for item in iterable` のようなループを使用すると、Python は内部で `iter()` 関数を呼び出してイテラブルからイテレータを取得します:

  1. `iter(iterable)` を呼び出します。

  2. イテレータを取得します。

  3. `StopIteration` が発生するまで `next()` を使用して各アイテムを取得します。

`iter()` 関数の2つの形式

  1. `iter(iterable)`:イテラブルオブジェクトからイテレータを返します。

  2. `iter(callable, sentinel)`:提供された呼び出し可能オブジェクトを呼び出し、センチネル値が返されるまでイテレータを作成します。

`__iter__()` を実装していないオブジェクトに対する反復処理

オブジェクトが `iter()` を実装していなくても `getitem()` を実装している場合、Python はそれに対して反復処理を行うことができます:

class Squares:
    def __init__(self, n):
        self.n = n

    def __getitem__(self, i):
        if i >= self.n:
            raise IndexError
        return i  2

squares = Squares(5)
for num in squares:
    print(num)
  • Python はインデックス0から始まる `getitem()` メソッドを使用します。

  • 反復処理の終了を示すために `IndexError` を発生させます。

オブジェクトがイテラブルかどうかをテストする

イテレータを取得しようとすることで、オブジェクトがイテラブルかどうかをチェックできます:

def is_iterable(obj):
    try:
        iter(obj)
        return True
    except TypeError:
        return False

print(is_iterable([1, 2, 3]))  # True
print(is_iterable(42))         # False
  • `iter(obj)` が `TypeError` を発生させなければ、そのオブジェクトはイテラブルです。

`iter()` を使用した呼び出し可能オブジェクトに対する反復処理

呼び出し可能イテレータパターン

データを返す呼び出し可能オブジェクトがあり、センチネル値が返されるまでその結果に対して反復処理を行いたい場合を考えます。

カスタムイテレータの例

class CallableIterator:
    def __init__(self, callable, sentinel):
        self.callable = callable
        self.sentinel = sentinel
        self.is_consumed = False

    def __iter__(self):
        return self

    def __next__(self):
        if self.is_consumed:
            raise StopIteration
        result = self.callable()
        if result == self.sentinel:
            self.is_consumed = True
            raise StopIteration
        return result
  • `next()` が呼び出されるたびに提供された呼び出し可能オブジェクトを呼び出します。

  • センチネル値が検出されると反復処理を停止します。

カスタムイテレータの使用

def counter():
    i = 0
    def inc():
        nonlocal i
        i += 1
        return i
    return inc

cnt = counter()
cnt_iter = CallableIterator(cnt, 5)
for num in cnt_iter:
    print(num)

出力:

1
2
3
4

`iter(callable, sentinel)` の使用

Python の組み込み `iter()` 関数は、この機能を標準で提供しています:

cnt = counter()
cnt_iter = iter(cnt, 5)
for num in cnt_iter:
    print(num)
  • 呼び出し可能オブジェクトとセンチネルからイテレータを作成するのを簡略化します。

実践的な例:乱数生成器

import random

random.seed(0)
rand_iter = iter(lambda: random.randint(0, 10), 8)
for num in rand_iter:
    print(num)
  • 8が返されるまで乱数を生成します。

  • 特定の条件が満たされるまでデータを読み込む際に有用です。

カウントダウンの例

def countdown(start):
    def run():
        nonlocal start
        start -= 1
        return start
    return run

takeoff = countdown(10)
for num in iter(takeoff, -1):
    print(num)

出力:

9
8
7
6
5
4
3
2
1
0
  • 9から0までカウントダウンします。

  • `-1` が返されると停止します。

例外処理:EAFP vs. LBYL

EAFP(許可を求めるより許しを請う方が簡単)

  • アプローチ:直接何かを試み、例外が発生した場合に処理します。

LBYL(飛び込む前に確認する)

  • アプローチ:アクションを実行する前に条件をチェックします。

どちらを使用するか?

  • EAFPはより Pythonic であり、しばしば好まれます。

  • 競合状態を回避し、より簡潔なコードにつながる可能性があります。

結論

Python のイテラブルとイテレータ、特に組み込みのものの微妙な違いを理解することで、より効率的で Pythonic なコードを書くことができます。遅延評価と `iter()` 関数の機能を活用することで、メモリ使用量とパフォーマンスを最適化できます。さらに、EAFP のような例外処理パターンを LBYL よりも使用すべき時を認識することで、より堅牢で保守性の高いコードにつながります。

イテラブルとイテレータについてさらに深く掘り下げていくにつれて、ジェネレータとその応用について探求し、最小限のコードでカスタムイテレータを作成する方法を学びます。今後のレッスンでさらなる洞察をお楽しみに。


注:すべてのコード例は教育目的であり、このポストで議論された概念を示すためのものです。


「超本当にドラゴン」へ

この記事が気に入ったらサポートをしてみませんか?