プロのように反復処理を行う

Beginner

This tutorial is from open-source community. Access the source code

はじめに

この実験では、Python プログラミングにおける反復処理の基本概念について学びます。反復処理を使うと、リスト、タプル、辞書などのシーケンス内の要素を効率的に処理することができます。反復処理技術を習得することで、Python のコーディングスキルを大幅に向上させることができます。

基本的な for ループによる反復処理、シーケンスのアンパッキング、enumerate()zip() などの組み込み関数の使用、メモリ効率を向上させるジェネレータ式の活用など、いくつかの強力な Python の反復処理技術を探索します。

これは Guided Lab です。学習と実践を支援するためのステップバイステップの指示を提供します。各ステップを完了し、実践的な経験を積むために、指示に注意深く従ってください。過去のデータによると、この 初級 レベルの実験の完了率は 96%です。学習者から 100% の好評価を得ています。

基本的な反復処理とシーケンスのアンパッキング

このステップでは、Python での for ループを使った基本的な反復処理とシーケンスのアンパッキングについて探索します。反復処理はプログラミングにおける基本的な概念で、シーケンス内の各要素を 1 つずつ処理することができます。一方、シーケンスのアンパッキングを使うと、シーケンスの個々の要素を便利に変数に割り当てることができます。

CSV ファイルからのデータの読み込み

まずは、CSV ファイルからデータを読み込みましょう。CSV (Comma-Separated Values、カンマ区切り値) は、表形式のデータを保存するためによく使われるファイル形式です。始めるには、WebIDE でターミナルを開き、Python インタープリターを起動する必要があります。これにより、Python コードを対話的に実行することができます。

cd ~/project
python3

これで Python インタープリターに入ったので、以下の Python コードを実行して portfolio.csv ファイルからデータを読み込みます。まず、CSV ファイルを扱う機能を提供する csv モジュールをインポートします。次に、ファイルを開き、csv.reader オブジェクトを作成してデータを読み込みます。next 関数を使って列のヘッダーを取得し、残りのデータをリストに変換します。最後に、pprint モジュールの pprint 関数を使って行をより読みやすい形式で表示します。

import csv

f = open('portfolio.csv')
f_csv = csv.reader(f)
headers = next(f_csv)    ## Get the column headers
rows = list(f_csv)       ## Convert the remaining data to a list
from pprint import pprint
pprint(rows)             ## Pretty print the rows

以下のような出力が表示されるはずです。

[['AA', '100', '32.20'],
 ['IBM', '50', '91.10'],
 ['CAT', '150', '83.44'],
 ['MSFT', '200', '51.23'],
 ['GE', '95', '40.37'],
 ['MSFT', '50', '65.10'],
 ['IBM', '100', '70.44']]

for ループによる基本的な反復処理

Python の for 文は、リスト、タプル、文字列など、あらゆるデータのシーケンスを反復処理するために使われます。この場合、CSV ファイルから読み込んだデータの行を反復処理するために使います。

for row in rows:
    print(row)

このコードは、rows リストの各行を順番に処理し、それを表示します。CSV ファイルの各行のデータが 1 つずつ表示されます。

['AA', '100', '32.20']
['IBM', '50', '91.10']
['CAT', '150', '83.44']
['MSFT', '200', '51.23']
['GE', '95', '40.37']
['MSFT', '50', '65.10']
['IBM', '100', '70.44']

ループ内でのシーケンスのアンパッキング

Python では、for ループ内で直接シーケンスをアンパッキングすることができます。シーケンス内の各要素の構造がわかっている場合、これは非常に便利です。この場合、rows リストの各行には 3 つの要素が含まれています。名前、株式数、価格です。これらの要素を for ループ内で直接アンパッキングすることができます。

for name, shares, price in rows:
    print(name, shares, price)

このコードは、各行を namesharesprice という変数にアンパッキングし、それらを表示します。データがより読みやすい形式で表示されます。

AA 100 32.20
IBM 50 91.10
CAT 150 83.44
MSFT 200 51.23
GE 95 40.37
MSFT 50 65.10
IBM 100 70.44

一部の値が必要ない場合は、_ をプレースホルダーとして使って、それらの値を無視することができます。たとえば、名前と価格だけを表示したい場合は、以下のコードを使うことができます。

for name, _, price in rows:
    print(name, price)

このコードは、各行の 2 番目の要素を無視し、名前と価格だけを表示します。

AA 32.20
IBM 91.10
CAT 83.44
MSFT 51.23
GE 40.37
MSFT 65.10
IBM 70.44

* 演算子による拡張アンパッキング

より高度なアンパッキングには、* 演算子をワイルドカードとして使うことができます。これにより、複数の要素をリストにまとめることができます。この技術を使って、データを名前でグループ化してみましょう。

from collections import defaultdict

byname = defaultdict(list)
for name, *data in rows:
    byname[name].append(data)

## Print the data for IBM
print(byname['IBM'])

## Iterate through IBM's data
for shares, price in byname['IBM']:
    print(shares, price)

このコードでは、まず collections モジュールから defaultdict クラスをインポートします。defaultdict は、キーが存在しない場合に自動的に新しい値 (この場合は空のリスト) を作成する辞書です。次に、* 演算子を使って、最初の要素以外のすべての要素を data というリストにまとめます。このリストを byname 辞書に名前でグループ化して保存します。最後に、IBM のデータを表示し、それを反復処理して株式数と価格を表示します。

出力:

[['50', '91.10'], ['100', '70.44']]
50 91.10
100 70.44

この例では、*data が最初の要素以外のすべての要素をリストにまとめ、それを名前でグループ化して辞書に保存します。これは、可変長シーケンスのデータを扱うための強力な技術です。

enumerate()zip() 関数の使用

このステップでは、Python の反復処理に欠かせない 2 つの非常に便利な組み込み関数、enumerate()zip() について探索します。これらの関数は、シーケンスを扱う際にコードを大幅に簡素化することができます。

enumerate() を使ったカウント

シーケンスを反復処理する際に、各要素のインデックスや位置を追跡する必要があることがよくあります。そのような場合に enumerate() 関数が便利です。enumerate() 関数はシーケンスを入力として受け取り、そのシーケンス内の各要素に対して (インデックス,値) のペアを返します。

前のステップから Python インタープリターを使い続けている場合は、そのまま続けることができます。そうでない場合は、新しいセッションを開始します。新しく始める場合のデータのセットアップ方法は次のとおりです。

## If you're starting a new session, reload the data first:
## import csv
## f = open('portfolio.csv')
## f_csv = csv.reader(f)
## headers = next(f_csv)
## rows = list(f_csv)

## Use enumerate to get row numbers
for rowno, row in enumerate(rows):
    print(rowno, row)

上記のコードを実行すると、enumerate(rows) 関数はインデックス (0 から始まる) と rows シーケンス内の対応する行のペアを生成します。for ループはこれらのペアを rownorow という変数にアンパッキングし、それらを表示します。

出力:

0 ['AA', '100', '32.20']
1 ['IBM', '50', '91.10']
2 ['CAT', '150', '83.44']
3 ['MSFT', '200', '51.23']
4 ['GE', '95', '40.37']
5 ['MSFT', '50', '65.10']
6 ['IBM', '100', '70.44']

enumerate() とアンパッキングを組み合わせることで、コードをさらに読みやすくすることができます。アンパッキングを使うと、シーケンスの要素を個々の変数に直接割り当てることができます。

for rowno, (name, shares, price) in enumerate(rows):
    print(rowno, name, shares, price)

このコードでは、(name, shares, price) の周りに追加の括弧を使って、行データを適切にアンパッキングしています。enumerate(rows) は依然としてインデックスと行を返しますが、今回は行を namesharesprice という変数にアンパッキングしています。

出力:

0 AA 100 32.20
1 IBM 50 91.10
2 CAT 150 83.44
3 MSFT 200 51.23
4 GE 95 40.37
5 MSFT 50 65.10
6 IBM 100 70.44

zip() を使ったデータのペアリング

zip() 関数は Python のもう 1 つの強力なツールです。複数のシーケンスから対応する要素を組み合わせるために使われます。複数のシーケンスを zip() に渡すと、各タプルが入力シーケンスの同じ位置の要素を含むタプルを生成するイテレータが作成されます。

これまで扱ってきた headersrow のデータで zip() を使う方法を見てみましょう。

## Recall the headers variable from earlier
print(headers)  ## Should show ['name', 'shares', 'price']

## Get the first row
row = rows[0]
print(row)      ## Should show ['AA', '100', '32.20']

## Use zip to pair column names with values
for col, val in zip(headers, row):
    print(col, val)

このコードでは、zip(headers, row)headers シーケンスと row シーケンスを取り、それらの対応する要素をペアにします。for ループはこれらのペアを col (headers からの列名) と val (row からの値) にアンパッキングし、それらを表示します。

出力:

['name', 'shares', 'price']
['AA', '100', '32.20']
name AA
shares 100
price 32.20

zip() の非常に一般的な使い方の 1 つは、キーと値のペアから辞書を作成することです。Python では、辞書はキーと値のペアのコレクションです。

## Create a dictionary from headers and row values
record = dict(zip(headers, row))
print(record)

ここでは、zip(headers, row) が列名と値のペアを作成し、dict() 関数がこれらのペアを辞書に変換します。

出力:

{'name': 'AA', 'shares': '100', 'price': '32.20'}

この考え方を拡張して、rows シーケンス内のすべての行を辞書に変換することができます。

## Convert all rows to dictionaries
for row in rows:
    record = dict(zip(headers, row))
    print(record)

このループでは、rows 内の各行に対して、zip(headers, row) を使ってキーと値のペアを作成し、その後 dict() を使ってそれらのペアを辞書に変換します。この技術は、データ処理アプリケーション、特に最初の行にヘッダーが含まれる CSV ファイルを扱う際に非常に一般的です。

出力:

{'name': 'AA', 'shares': '100', 'price': '32.20'}
{'name': 'IBM', 'shares': '50', 'price': '91.10'}
{'name': 'CAT', 'shares': '150', 'price': '83.44'}
{'name': 'MSFT', 'shares': '200', 'price': '51.23'}
{'name': 'GE', 'shares': '95', 'price': '40.37'}
{'name': 'MSFT', 'shares': '50', 'price': '65.10'}
{'name': 'IBM', 'shares': '100', 'price': '70.44'}

ジェネレータ式とメモリ効率

このステップでは、ジェネレータ式について探索します。Python で大規模なデータセットを扱う際に、これらは非常に便利です。ジェネレータ式を使うと、コードのメモリ効率を大幅に向上させることができ、大量のデータを扱う際には非常に重要です。

ジェネレータ式の理解

ジェネレータ式はリスト内包表記に似ていますが、重要な違いがあります。リスト内包表記を使うと、Python はすべての結果を一度に含むリストを作成します。これは、特に大規模なデータセットを扱う場合、大量のメモリを消費することがあります。一方、ジェネレータ式は必要になったときに結果を 1 つずつ生成します。つまり、すべての結果を一度にメモリに格納する必要がないため、かなりのメモリを節約することができます。

これがどのように機能するかを見るために、簡単な例を見てみましょう。

## Start a new Python session if needed
## python3

## List comprehension (creates a list in memory)
nums = [1, 2, 3, 4, 5]
squares_list = [x*x for x in nums]
print(squares_list)

## Generator expression (creates a generator object)
squares_gen = (x*x for x in nums)
print(squares_gen)  ## This doesn't print the values, just the generator object

## Iterate through the generator to get values
for n in squares_gen:
    print(n)

このコードを実行すると、以下の出力が表示されます。

[1, 4, 9, 16, 25]
<generator object <genexpr> at 0x7f...>
1
4
9
16
25

ジェネレータに関して重要なことは、一度しか反復処理できないということです。ジェネレータ内のすべての値を処理した後は、そのジェネレータは枯渇し、再度値を取得することはできません。

## Try to iterate again over the same generator
for n in squares_gen:
    print(n)  ## Nothing will be printed, as the generator is already exhausted

next() 関数を使って、ジェネレータから値を 1 つずつ手動で取得することもできます。

## Create a fresh generator
squares_gen = (x*x for x in nums)

## Get values one by one
print(next(squares_gen))  ## 1
print(next(squares_gen))  ## 4
print(next(squares_gen))  ## 9

ジェネレータに値が残っていない場合、next() を呼び出すと StopIteration 例外が発生します。

yield を使ったジェネレータ関数

より複雑なジェネレータロジックには、yield 文を使ってジェネレータ関数を書くことができます。ジェネレータ関数は、一度に単一の結果を返すのではなく、yield を使って値を 1 つずつ返す特殊な関数です。

def squares(nums):
    for x in nums:
        yield x*x

## Use the generator function
for n in squares(nums):
    print(n)

このコードを実行すると、以下の出力が表示されます。

1
4
9
16
25

ジェネレータ式によるメモリ使用量の削減

では、大規模なデータセットを扱う際に、ジェネレータ式がどのようにメモリを節約できるかを見てみましょう。非常に大きい CTA バスデータファイルを使用します。

まず、メモリを大量に消費するアプローチを試してみましょう。

import tracemalloc
tracemalloc.start()

import readrides
rows = readrides.read_rides_as_dicts('ctabus.csv')
rt22 = [row for row in rows if row['route'] == '22']
max_row = max(rt22, key=lambda row: int(row['rides']))
print(max_row)

## Check memory usage
current, peak = tracemalloc.get_traced_memory()
print(f"Current memory usage: {current / 1024 / 1024:.2f} MB")
print(f"Peak memory usage: {peak / 1024 / 1024:.2f} MB")

次に、Python を終了して再起動し、ジェネレータベースのアプローチと比較します。

exit() python3
import tracemalloc
tracemalloc.start()

import csv
f = open('ctabus.csv')
f_csv = csv.reader(f)
headers = next(f_csv)

## Use generator expressions for all operations
rows = (dict(zip(headers, row)) for row in f_csv)
rt22 = (row for row in rows if row['route'] == '22')
max_row = max(rt22, key=lambda row: int(row['rides']))
print(max_row)

## Check memory usage
current, peak = tracemalloc.get_traced_memory()
print(f"Current memory usage: {current / 1024 / 1024:.2f} MB")
print(f"Peak memory usage: {peak / 1024 / 1024:.2f} MB")

これら 2 つのアプローチのメモリ使用量には大きな違いがあることに気づくはずです。ジェネレータベースのアプローチは、すべてのデータを一度にメモリにロードすることなく、データを逐次処理するため、はるかにメモリ効率が良いです。

集約関数との組み合わせによるジェネレータ式の活用

ジェネレータ式は、sum()min()max()any()all() などの、シーケンス全体を処理して単一の結果を生成する関数と組み合わせると特に便利です。

いくつかの例を見てみましょう。

from readport import read_portfolio
portfolio = read_portfolio('portfolio.csv')

## Calculate the total value of the portfolio
total_value = sum(s['shares']*s['price'] for s in portfolio)
print(f"Total portfolio value: {total_value}")

## Find the minimum number of shares in any holding
min_shares = min(s['shares'] for s in portfolio)
print(f"Minimum shares in any holding: {min_shares}")

## Check if any stock is IBM
has_ibm = any(s['name'] == 'IBM' for s in portfolio)
print(f"Portfolio contains IBM: {has_ibm}")

## Check if all stocks are IBM
all_ibm = all(s['name'] == 'IBM' for s in portfolio)
print(f"All stocks are IBM: {all_ibm}")

## Count IBM shares
ibm_shares = sum(s['shares'] for s in portfolio if s['name'] == 'IBM')
print(f"Total IBM shares: {ibm_shares}")

ジェネレータ式は文字列操作にも便利です。タプル内の値を結合する方法を見てみましょう。

s = ('GOOG', 100, 490.10)
## This would fail: ','.join(s)
## Use a generator expression to convert all items to strings
joined = ','.join(str(x) for x in s)
print(joined)  ## Output: 'GOOG,100,490.1'

これらの例でジェネレータ式を使う主な利点は、中間リストが作成されないため、メモリ効率の高いコードになることです。

まとめ

この実験では、いくつかの強力な Python の反復処理技術を学びました。まず、基本的な反復処理とシーケンスのアンパッキングを習得し、for ループを使ってシーケンスを反復処理し、個々の変数にアンパッキングしました。次に、反復処理中にインデックスを追跡する enumerate() や、異なるシーケンスの要素をペアにする zip() などの組み込み関数を探索しました。

これらの技術は、効率的な Python プログラミングにおいて基本的なものです。これらを使うことで、より簡潔で読みやすく、メモリ効率の高いコードを書くことができます。これらの反復処理パターンを習得することで、Python プロジェクトでのデータ処理タスクをより効果的に処理することができます。