Python/三種の内包表記とジェネレーター式

出典: フリー教科書『ウィキブックス(Wikibooks)』

三種の内包表記とジェネレーター式[編集]

Pythonは豊富なデータ構造をサポートしており、そのデータを効果的に生成する手段として「3種の内包表記」と「ジェネレーター式」があります。この章では、「3種の内包表記」と「ジェネレーター式」に焦点を当て、リスト、セット、辞書それぞれの内包表記の基本構文や使い方、そしてジェネレーター式の利点について解説します。

内包表記とは?[編集]

プログラマーにとって、データの変換やフィルタリングは日常茶飯事です。しかし、Pythonではこれをより簡潔に行うための手段として「内包表記」が提供されています。リスト、セット、辞書内包表記それぞれの基本構文について学び、コードの可読性と効率性の向上を目指します。

リスト内包表記[編集]

リスト内包表記は、Pythonにおいてリストを生成するための簡潔で強力な構文です。一般的な構文は以下の通りです。

構文
[ expression for expr1 in sequence1
             for expr2 in sequence2
             ...
             for exprN in sequenceN
             if condition ]
リスト内包表記のサンプル
# 1から10までの奇数を含むリスト
odd_numbers = [x for x in range(1, 11) if x % 2 != 0]
print(odd_numbers) # 実行結果 [1, 3, 5, 7, 9]

# 文字列の各文字を大文字に変換するリスト
original_str = "hello world"
upper_chars = [char.upper() for char in original_str]
print(upper_chars) # 実行結果 ['H', 'E', 'L', 'L', 'O', ' ', 'W', 'O', 'R', 'L', 'D'] 

# ネストしたリスト内包表記:2次元行列をフラットなリストに変換
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flattened_matrix = [num for row in matrix for num in row]
print(flattened_matrix) # 実行結果 [1, 2, 3, 4, 5, 6, 7, 8, 9]
リスト内包表記を使わない場合の実装
# 1から10までの奇数を含むリスト
odd_numbers = list(filter(lambda x: x % 2 != 0, range(1, 11)))
print(odd_numbers)

# 文字列の各文字を大文字に変換するリスト
original_str = "hello world"
upper_chars = list(map(str.upper, original_str))
print(upper_chars)

# ネストしたリスト内包表記:2次元行列をフラットなリストに変換
def flatten(lst):
    flat_list = []
    for item in lst:
        if isinstance(item, list):
            flat_list.extend(flatten(item))
        else:
            flat_list.append(item)
    return flat_list

matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flattened_matrix = flatten(matrix)
print(flattened_matrix) # 実行結果 [1, 2, 3, 4, 5, 6, 7, 8, 9]
比較
  1. リスト内包表記を使った場合:
    • コードが簡潔であり、一行で表現されています。
    • Pythonらしいコードスタイルであり、可読性が高い。
  2. リスト内包表記を使わない場合:
    • filtermap を使用しているため、関数型プログラミングの要素があります。
    • フラット化のための再帰関数が明示的に定義されており、より柔軟性があります。
どちらのアプローチも優れたものであり、使用する状況により選択すると良いでしょう。簡潔さや可読性、再利用性を考慮して、適切な方法を選択してください。

セット内包表記[編集]

セット内包表記もリスト内包表記と似た構文を持っていますが、生成されるのはセットです。セット内包表記の基本的な構文は次の通りです。

構文
{ expression for expr1 in sequence1
             for expr2 in sequence2
             ...
             for exprN in sequenceN
             if condition }
セット内包表記のサンプル
# 1から10までの偶数を含むセット
even_numbers_set = {x for x in range(1, 11) if x % 2 == 0}
print(even_numbers_set)

# 文字列の中のユニークな文字を含むセット
unique_chars_set = {char for char in "programming is fun"}
print(unique_chars_set)

# 辞書からキーだけを含むセット
sample_dict = {'apple': 1, 'orange': 2, 'banana': 3}
keys_set = {key for key in sample_dict}
print(keys_set)
実行結果
{2, 4, 6, 8, 10}
{'m', 'u', 'i', 'o', ' ', 'f', 'r', 's', 'p', 'n', 'g', 'a'} 
{'banana', 'apple', 'orange'}

辞書内包表記[編集]

辞書内包表記は、イテレーションを行いつつ新しい辞書を生成するための構文です。基本的な構文は以下の通りで

構文
{ key_expression: value_expression for expr1 in sequence1
                                   for expr2 in sequence2
                                   ...
                                   for exprN in sequenceN
                                   if conditio n}
辞書内包表記のサンプル
# 1から5までの数字をキーにし、その2倍の値を値に持つ辞書
double_values_dict = {num: num * 2 for num in range(1, 6)}
print(double_values_dict)

# 文字列の各文字をキーとし、その文字のASCIIコードを値に持つ辞書
ascii_dict = {char: ord(char) for char in "abcde"}
print(ascii_dict)

# 辞書内包表記と条件式:奇数のみを含む辞書
numbers_dict = {x: x**2 for x in range(1, 11)}
odd_numbers_dict = {key: value for key, value in numbers_dict.items() if value % 2 != 0}
print(odd_numbers_dict)
実行結果
{1: 2, 2: 4, 3: 6, 4: 8, 5: 10}
{'a': 97, 'b': 98, 'c': 99, 'd': 100, 'e': 101} 
{1: 1, 3: 9, 5: 25, 7: 49, 9: 81}

ジェネレーター式とは?[編集]

リストやセット、辞書内包表記は結果をシーケンスとして一度生成しますが、時には大量のデータを保持せずに逐次生成したい場合があります。そこで登場するのが「ジェネレーター式」です。メモリの効率的な利用や遅延評価の観点から、ジェネレーター式の基本的な概念に迫ります。

ジェネレーター式のサンプル
# イテレータとしての利用
squared_numbers = (x**2 for x in range(1, 6))
print(list(squared_numbers))  # [1, 4, 9, 16, 25]

# フィルタリング
even_numbers = (x for x in range(1, 11) if x % 2 == 0)
print(list(even_numbers))  # [2, 4, 6, 8, 10]
実行結果
[1, 4, 9, 16, 25] 
[2, 4, 6, 8, 10]

上記のジェネレーター式をジェネレーター関数で置き換えると次のようになります。

ジェネレータ関数のサンプル
# squared_numbers のジェネレータ関数による実装
def squared_numbers_generator():
    for x in range(1, 6):
        yield x**2

# 使用例
result = list(squared_numbers_generator())
print(result)  # [1, 4, 9, 16, 25]

# even_numbers のジェネレータ関数による実装
def even_numbers_generator():
    for x in range(1, 11):
        if x % 2 == 0:
            yield x

# 使用例
result = list(even_numbers_generator())
print(result)  # [2, 4, 6, 8, 10]
ジェネレーター式とジェネレータ関数の比較
ジェネレーター式とジェネレータ関数は、共にイテレータを生成する手段ですが、それぞれ特徴や利点が異なります。以下に、ジェネレーター式とジェネレータ関数の比較を示します。
ジェネレーター式
  1. 簡潔さ: ジェネレーター式は非常に簡潔で、1行のコードでイテレータを生成できます。単純なイテレーションやフィルタリングに向いています。
  2. 即座の評価: ジェネレーター式は即座に評価され、すぐにイテレータが得られます。メモリ効率が高く、すぐに結果が利用可能です。
  3. 制約: 複雑な条件分岐や再帰的な処理が難しい。単純な式のみを使えます。
squared_numbers = (x**2 for x in range(1, 6))
ジェネレータ関数
  1. 柔軟性: ジェネレータ関数は条件分岐や再帰的な処理が可能で、複雑なイテレーションを実装できます。柔軟性が高いです。
  2. 遅延評価: ジェネレータ関数は yield を使って逐次値を生成するため、遅延評価が可能です。必要な時にのみ評価され、メモリを節約できます。
  3. 複雑な処理: 複雑な処理や再利用性が高いイテレータを構築するのに向いています。
def squared_numbers_generator():
    for x in range(1, 6):
        yield x**2
選択基準
  • シンプルなイテレーションやフィルタリングにはジェネレーター式が適しています。
  • 複雑な条件や再帰的な処理が必要な場合、または遅延評価が求められる場合は、ジェネレータ関数が適しています。

状況やニーズによって使い分けると良いでしょう。


ネストした形でリスト内包表記とジェネレーター式[編集]

ネストした三種の内包表記とジェネレーター式は、Pythonでデータを生成したり変換したりするための強力な手段です。以下に、それぞれの内包表記とジェネレーター式をネストさせた例を示します。

# ネストしたリスト内包表記:
# 1. 2次元リストの生成:
matrix = [[j for j in range(1, 4)] for i in range(1, 4)]
# 結果: [[1, 2, 3], [1, 2, 3], [1, 2, 3]]

# 2. フィルタリングと変換:
numbers = [1, 2, 3, 4]
filtered_squares = [[x**2 for x in numbers if x % 2 == 0] for _ in range(3)]
# 結果: [[4, 16], [4, 16], [4, 16]]

# ネストしたセット内包表記:
# 1. 2次元セットの生成:
matrix_set = {(i, j) for i in range(1, 4) for j in range(1, 4)}
# 結果: {(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3), (3, 1), (3, 2), (3, 3)}

# 2. フィルタリングと変換:
even_squared_set = {(x**2 for x in numbers if x % 2 == 0) for _ in range(3)}
# 結果: 集合内の各要素が条件に基づくフィルタリングされた集合が入れ子になった集合

# ネストした辞書内包表記:
# 1. 辞書の生成:
matrix_dict = {(i, j): i*j for i in range(1, 4) for j in range(1, 4)}
# 結果: {(1, 1): 1, (1, 2): 2, ..., (3, 3): 9}

#2. フィルタリングと変換:
even_squared_dict = {f'key_{i}': {x**2 for x in numbers if x % 2 == 0} for i in range(1, 4)}
# 結果: 辞書内の各値が条件に基づくフィルタリングされた集合が入れ子になった辞書

#ネストしたジェネレーター式:
# 1. ジェネレーター式のジェネレーター式:
gen_of_gens = ((i * j for j in range(1, 4)) for i in range(1, 4))
# 結果: ジェネレーター式が入れ子になったジェネレーター

# 2. フィルタリングと変換:
even_squared_gen = (x**2 for x in numbers if x % 2 == 0)
filtered_gen_of_gens = ((val for val in even_squared_gen) for _ in range(3))
# 結果: 各要素が条件に基づくフィルタリングされたジェネレーターが入れ子になったジェネレーター

これらの例は、ネストした内包表記やジェネレーター式がどのように構築されるかを示しています。ネストは可読性を損なわないように心がけ、必要な場面で利用すると良いでしょう。

実践的な例[編集]

内包表記とジェネレーターを用いた実際の問題解決の例 理解を深めるために、内包表記とジェネレーターを用いた実際の問題解決の例をいくつか挙げてみましょう。

# 1. 問題: 与えられたリストの各要素を2倍にした新しいリストを生成する。
## 内包表記:
original_list = [1, 2, 3, 4, 5]
doubled_list = [x * 2 for x in original_list]
print(doubled_list)
# 結果: [2, 4, 6, 8, 10]

## ジェネレーター:
original_list = [1, 2, 3, 4, 5]
doubled_gen = (x * 2 for x in original_list)
doubled_list = list(doubled_gen)
print(doubled_list)
# 結果: [2, 4, 6, 8, 10]

# 2. 問題: 1から10までの自然数のうち偶数のみを含むリストを生成する。
## 内包表記:
even_numbers_list = [x for x in range(1, 11) if x % 2 == 0]
print(even_numbers_list)
# 結果: [2, 4, 6, 8, 10]

## ジェネレーター:
even_numbers_gen = (x for x in range(1, 11) if x % 2 == 0)
even_numbers_list = list(even_numbers_gen)
print(even_numbers_list)
# 結果: [2, 4, 6, 8, 10]

# 3. 問題: 与えられた文章の単語の長さを格納したリストを生成する。
## 内包表記:
sentence = "This is a sample sentence."
word_lengths = [len(word) for word in sentence.split()]
print(word_lengths)
# 結果: [4, 2, 1, 6, 8]

## ジェネレーター:
sentence = "This is a sample sentence."
word_length_gen = (len(word) for word in sentence.split())
word_lengths = list(word_length_gen)
print(word_lengths)
# 結果: [4, 2, 1, 6, 8]

これらの例は、内包表記やジェネレーターを使って、シンプルで簡潔な方法で問題を解決する方法を示しています。リスト内包表記は新しいリストを生成し、ジェネレーターは遅延評価やメモリの節約に役立ちます。どちらもコードを簡素化し、可読性を向上させるのに寄与します。

他のプログラミング言語の内包表記[編集]

Pythonの内包表記のアイデアは、Haskellなどの関数型プログラミング言語から来ています。Greg Ewing氏が、Haskellのリスト内包表記に触発されて、Pythonに導入することを提案しました。この提案は、Python 2.0で採用され、以後、内包表記はPythonの一部として成長しました。

Haskellのリスト内包表記の構文やアプローチがPythonに影響を与えたと言えます。Pythonでは、シンプルで直感的な表現力を持つ内包表記が、リストや他のシーケンス型の生成や変換に広く利用されています。

Pythonのリスト内包表記
# 例: 1から10までの偶数を含むリスト
even_numbers = [x for x in range(1, 11) if x % 2 == 0]
# 結果: [2, 4, 6, 8, 10]
Haskellのリスト内包表記
-- 例: 1から10までの偶数を含むリスト
evenNumbers = [x | x <- [1..10], even x]
-- 結果: [2, 4, 6, 8, 10]
主な違い
  1. 構文の違い: Pythonでは[expression for item in iterable if condition]のように構文が記述されますが、Haskellでは[expression | element <- list, condition]のように表現されます。Haskellでは<-を使用してリストの要素にアクセスします。
  2. 範囲指定: Pythonではrange()関数を使用して範囲を指定しますが、Haskellでは[1..10]のように直接範囲を指定します。
  3. 条件式: 条件式の表現も若干異なります。Pythonではif conditionを使用しますが、Haskellでは,を使って条件を表現します。
  4. even関数の利用: Haskellの例ではeven関数を使用して偶数を判定しています。Haskellのリスト内包表記は関数型プログラミングの影響を受けており、関数が豊富に利用されます。

要するに、両者は同じような目的を果たすための簡潔で表現力豊かな機能を提供していますが、文法や詳細な使い方にはプログラミング言語の特性による違いがあります。

Julia[編集]

Juliaも内包表記(Comprehensions)をサポートしています。Juliaの内包表記はPythonやHaskellといくつかの点で類似していますが、Juliaの言語特性に合わせて独自の構文を持っています。以下に、Juliaの内包表記の例を示します。

# 例: 1から10までの偶数を含む配列
even_numbers = [x for x in 1:10 if x % 2 == 0]
# 結果: [2, 4, 6, 8, 10]

Juliaでは、[expression for item in iterable if condition]のような形式で内包表記が行われます。上記の例では、1:10は範囲を表し、条件 if x % 2 == 0 によって偶数が選択されています。

Juliaの内包表記は、PythonやHaskellと同様に、簡潔で表現力豊かなコードを書くための強力なツールです。それぞれのプログラミング言語の特性や文法に合わせて内包表記が実装されています。

Fortran[編集]

Fortranは伝統的には内包表記のような機能がない言語でしたが、最新のFortran規格であるFortran 2003以降では、内包表記に似た機能が導入されています。これは、配列操作において簡潔で効果的なコードを書くために提供されています。

以下は、Fortranの内包表記風のコード例です。Fortran 2008以降では、array constructorを使用して配列を生成できます。

program array_example
  implicit none

  integer :: i
  integer, dimension(5) :: even_numbers

  ! 内包表記風の構文
  even_numbers = [(i * 2, i=1,5,1)]

  ! 結果の表示
  print *, even_numbers  ! [2, 4, 6, 8, 10]

end program array_example

この例では、[(i * 2, i=1,5,1)]が内包表記風の構文です。これは1から5までの整数を2倍して生成した配列を表しています。

Fortranの新しい機能や規格によって、より現代的で表現力豊かなコードを書くための手段が増えています。ただし、内包表記の機能がPythonや他の一部の言語に比べてFortranにおいてまだ一般的ではないことに注意してください。

F#[編集]

F#は関数型プログラミング言語であり、Haskellのような機能が一部取り入れられています。そのため、内包表記も直感的で柔軟な記法を提供しています。

// 例: 1から10までの偶数を含むリスト
let evenNumbers = [for x in 1..10 do if x % 2 = 0 then yield x * 2]
// 結果: [4; 8; 12; 16; 20]

この例では、[for x in 1..10 do if x % 2 = 0 then yield x * 2]のような構文を使用して、1から10までの偶数を2倍にしてリストに格納しています。if x % 2 = 0 then yield x * 2は条件を表し、xが偶数である場合にのみリストに追加されます。

F#は関数型プログラミング言語であり、Haskellのような機能が一部取り入れられています。そのため、内包表記も直感的で柔軟な記法を提供しています。

Chapel[編集]

Chapelも内包表記(comprehension)をサポートしています。Chapelの内包表記は、集合を構築するためのコンパクトで表現力豊かな構文を提供します。

以下は、Chapelでの内包表記の例です:

// 例: 1から10までの偶数を含む配列
var evenNumbers: [1..10] = { x*2 | x in 1..10 && x mod 2 == 0 };
// 結果: [4, 8, 12, 16, 20]


この例では、{ x*2 | x in 1..10 && x mod 2 == 0 }のような構文を使用して、1から10までの偶数を2倍にして配列に格納しています。x in 1..10 && x mod 2 == 0は条件を表し、xが偶数である場合にのみ内包表記に含まれます。

Chapelは並列処理を強力にサポートする言語であり、内包表記もその特徴を反映しています。

まとめ[編集]

三種の内包表記(リスト内包表記、セット内包表記、辞書内包表記)とジェネレーターの特徴と使いどころをまとめます。

リスト内包表記:[編集]

  • 特徴:
    1. リストを生成するための簡潔で直感的な構文。
    2. リスト内包表記で生成されるオブジェクトはミュータブル(要素の変更や追加が可能)。
  • 使いどころ:
    1. データ変換やフィルタリング。
    2. 新しいリストを生成する場合。

セット内包表記:[編集]

  • 特徴:
    1. 集合(セット)を生成するための構文。
    2. 重複を許さない。
  • 使いどころ:
    1. データの一意な要素を取り出す。
    2. 集合演算を行う。

辞書内包表記:[編集]

  • 特徴:
    1. 辞書を生成するための構文。
    2. キーと値のペアを指定して生成。
  • 使いどころ:
    1. キーと値のペアを指定して新しい辞書を生成する場合。
    2. データのグループ化や変換。

ジェネレーター:[編集]

  • 特徴:
    1. 遅延評価を行う。
    2. 要素が必要な時点で生成され、メモリを節約する。
    3. yield 文を使ったジェネレーター関数またはジェネレーター式で実現できる。
  • 使いどころ:
    1. 大規模なデータセットや無限シーケンスを扱う場合。
    2. 遅延評価が必要な場合。
    3. イテレータとして動作する関数を実装する場合。

まとめ:[編集]

  • 内包表記はデータ変換やフィルタリング、新しいコレクションの生成に利用される。
  • ジェネレーターは遅延評価とメモリ効率があり、大規模なデータセットや無限シーケンスを扱う際に適している。
  • ジェネレーターは yield 文を使ってジェネレーター関数やジェネレーター式として実装できる。
  • 使いどころはコードの要件や処理の特性により異なるため、適切なものを選択すると良い。

参考文献[編集]

脚註[編集]