Python の第一級オブジェクトとメモリモデルの探索

PythonPythonIntermediate
今すぐ練習

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

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

この実験では、Python の第一級オブジェクト (first - class object) の概念について学び、そのメモリモデルを探索します。Python は関数、型、データを第一級オブジェクトとして扱い、強力で柔軟なプログラミングパターンを可能にします。

また、CSV データ処理用の再利用可能なユーティリティ関数を作成します。具体的には、reader.py ファイルに CSV データを読み取る汎用関数を作成し、これを異なるプロジェクト間で再利用できるようにします。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/FileHandlingGroup(["File Handling"]) python(("Python")) -.-> python/PythonStandardLibraryGroup(["Python Standard Library"]) python(("Python")) -.-> python/BasicConceptsGroup(["Basic Concepts"]) python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/ModulesandPackagesGroup(["Modules and Packages"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python/BasicConceptsGroup -.-> python/strings("Strings") python/BasicConceptsGroup -.-> python/type_conversion("Type Conversion") python/FunctionsGroup -.-> python/function_definition("Function Definition") python/FunctionsGroup -.-> python/scope("Scope") python/ModulesandPackagesGroup -.-> python/importing_modules("Importing Modules") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/FileHandlingGroup -.-> python/file_operations("File Operations") python/PythonStandardLibraryGroup -.-> python/data_collections("Data Collections") subgraph Lab Skills python/strings -.-> lab-132489{{"Python の第一級オブジェクトとメモリモデルの探索"}} python/type_conversion -.-> lab-132489{{"Python の第一級オブジェクトとメモリモデルの探索"}} python/function_definition -.-> lab-132489{{"Python の第一級オブジェクトとメモリモデルの探索"}} python/scope -.-> lab-132489{{"Python の第一級オブジェクトとメモリモデルの探索"}} python/importing_modules -.-> lab-132489{{"Python の第一級オブジェクトとメモリモデルの探索"}} python/classes_objects -.-> lab-132489{{"Python の第一級オブジェクトとメモリモデルの探索"}} python/file_operations -.-> lab-132489{{"Python の第一級オブジェクトとメモリモデルの探索"}} python/data_collections -.-> lab-132489{{"Python の第一級オブジェクトとメモリモデルの探索"}} end

Python における第一級オブジェクトの理解

Python では、すべてがオブジェクトとして扱われます。これには関数や型も含まれます。これはどういう意味でしょうか?つまり、関数や型をデータ構造に格納したり、他の関数に引数として渡したり、さらには他の関数から返すこともできるということです。これは非常に強力な概念であり、ここでは CSV データ処理を例にして探索していきます。

第一級型の探索

まず、Python インタープリタを起動しましょう。WebIDE で新しいターミナルを開き、以下のコマンドを入力します。このコマンドで Python インタープリタが起動し、ここで Python コードを実行します。

python3

Python で CSV ファイルを扱う際、これらのファイルから読み取った文字列を適切なデータ型に変換する必要があることがよくあります。たとえば、CSV ファイル内の数値は文字列として読み取られるかもしれませんが、Python コードでは整数または浮動小数点数として使用したいと思うでしょう。これを行うには、変換関数のリストを作成することができます。

coltypes = [str, int, float]

ここでは、実際の型関数を含むリストを作成していることに注意してください。文字列ではありません。Python では、型は第一級オブジェクトであり、他のオブジェクトと同じように扱うことができます。リストに入れたり、渡したり、コードで使用したりすることができます。

では、ポートフォリオの CSV ファイルからいくつかのデータを読み取り、これらの変換関数をどのように使用できるかを見てみましょう。

import csv
f = open('portfolio.csv')
rows = csv.reader(f)
headers = next(rows)
row = next(rows)
print(row)

このコードを実行すると、以下のような出力が表示されるはずです。これは CSV ファイルの最初のデータ行で、文字列のリストとして表されています。

['AA', '100', '32.20']

次に、zip関数を使用します。zip関数は、複数のイテラブル(リストやタプルなど)を受け取り、それらの要素をペアにします。これを使用して、行の各値を対応する型変換関数とペアにします。

r = list(zip(coltypes, row))
print(r)

これにより、以下のような出力が得られます。各ペアには、型関数と CSV ファイルからの文字列値が含まれています。

[(<class 'str'>, 'AA'), (<class 'int'>, '100'), (<class 'float'>, '32.20')]

これらのペアができたので、各関数を適用して値を適切な型に変換することができます。

record = [func(val) for func, val in zip(coltypes, row)]
print(record)

出力では、値が適切な型に変換されていることがわかります。文字列 'AA' は文字列のまま、'100' は整数 100 に、'32.20' は浮動小数点数 32.2 になります。

['AA', 100, 32.2]

また、これらの値を列名と組み合わせて辞書を作成することもできます。辞書は、キーと値のペアを格納できる便利なデータ構造です。

record_dict = dict(zip(headers, record))
print(record_dict)

出力は、キーが列名で値が変換されたデータである辞書になります。

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

これらの手順をすべて 1 つの内包表記で行うこともできます。内包表記は、Python でリスト、辞書、またはセットを簡潔に作成する方法です。

result = {name: func(val) for name, func, val in zip(headers, coltypes, row)}
print(result)

出力は前と同じ辞書になります。

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

Python インタープリタでの作業が終了したら、以下のコマンドを入力して終了できます。

exit()

このデモンストレーションは、Python が関数を第一級オブジェクトとして扱うことで、強力なデータ処理技術が可能になることを示しています。型や関数をオブジェクトとして扱えることで、より柔軟で簡潔なコードを書くことができます。

CSV 処理用のユーティリティ関数の作成

Python の第一級オブジェクトがデータ変換にどのように役立つかを理解したので、再利用可能なユーティリティ関数を作成します。この関数は CSV データを読み取り、辞書のリストに変換します。これは非常に有用な操作です。なぜなら、CSV ファイルは表形式のデータを格納するために一般的に使用されており、これを辞書のリストに変換することで、Python でデータを扱いやすくなるからです。

CSV リーダーユーティリティの作成

まず、WebIDE を開きます。開いたら、プロジェクトディレクトリに移動し、reader.py という名前の新しいファイルを作成します。このファイルでは、CSV データを読み取り、型変換を適用する関数を定義します。型変換は重要です。なぜなら、CSV ファイル内のデータは通常文字列として読み取られますが、さらなる処理のために整数や浮動小数点数などの異なるデータ型が必要になることがあるからです。

reader.py に以下のコードを追加します。

import csv

def read_csv_as_dicts(filename, types):
    """
    Read a CSV file into a list of dictionaries, converting each field according
    to the types provided.

    Parameters:
    filename (str): Name of the CSV file to read
    types (list): List of type conversion functions for each column

    Returns:
    list: List of dictionaries representing the CSV data
    """
    records = []
    with open(filename, 'r') as f:
        rows = csv.reader(f)
        headers = next(rows)  ## Get the column headers

        for row in rows:
            ## Apply type conversions to each value in the row
            converted_row = [func(val) for func, val in zip(types, row)]

            ## Create a dictionary mapping headers to converted values
            record = dict(zip(headers, converted_row))
            records.append(record)

    return records

この関数はまず、指定された CSV ファイルを開きます。次に、CSV ファイルのヘッダー(列名)を読み取ります。その後、ファイル内の各行を処理します。行内の各値に対して、types リストから対応する型変換関数を適用します。最後に、キーが列ヘッダーで値が変換されたデータである辞書を作成し、この辞書を records リストに追加します。すべての行が処理されると、records リストを返します。

ユーティリティ関数のテスト

ユーティリティ関数をテストしましょう。まず、ターミナルを開き、以下を入力して Python インタープリタを起動します。

python3

Python インタープリタに入ったら、関数を使用してポートフォリオデータを読み取ることができます。ポートフォリオデータは、株式の名前、株式数、価格などの株式に関する情報を含む CSV ファイルです。

import reader
portfolio = reader.read_csv_as_dicts('portfolio.csv', [str, int, float])
for record in portfolio[:3]:  ## Show the first 3 records
    print(record)

このコードを実行すると、以下のような出力が表示されるはずです。

{'name': 'AA', 'shares': 100, 'price': 32.2}
{'name': 'IBM', 'shares': 50, 'price': 91.1}
{'name': 'CAT', 'shares': 150, 'price': 83.44}

この出力は、ポートフォリオデータの最初の 3 つのレコードを示しており、データ型が正しく変換されています。

CTA バスデータでも関数を試してみましょう。CTA バスデータは、バス路線、日付、曜日タイプ、乗車人数に関する情報を含む別の CSV ファイルです。

rows = reader.read_csv_as_dicts('ctabus.csv', [str, str, str, int])
print(f"Total rows: {len(rows)}")
print("First row:", rows[0])

出力は以下のようになるはずです。

Total rows: 577563
First row: {'route': '3', 'date': '01/01/2001', 'daytype': 'U', 'rides': 7354}

これは、関数が異なる CSV ファイルを処理し、適切な型変換を適用できることを示しています。

Python インタープリタを終了するには、以下を入力します。

exit()

これで、任意の CSV ファイルを読み取り、適切な型変換を適用できる再利用可能なユーティリティ関数を作成しました。これは、Python の第一級オブジェクトの強力さと、柔軟で再利用可能なコードを作成するための使い方を示しています。

✨ 解答を確認して練習

Python のメモリモデルの探索

Python のメモリモデルは、オブジェクトがメモリにどのように格納され、どのように参照されるかを決定する上で重要な役割を果たします。特に大規模なデータセットを扱う際には、このモデルを理解することが不可欠です。なぜなら、それが Python プログラムのパフォーマンスとメモリ使用量に大きな影響を与える可能性があるからです。このステップでは、特に Python で文字列オブジェクトがどのように扱われるかに焦点を当て、大規模なデータセットのメモリ使用量を最適化する方法を探索します。

データセット内の文字列の繰り返し

CTA バスデータには、路線名などの多くの繰り返し値が含まれています。データセット内の繰り返し値は、適切に処理されない場合、メモリの非効率的な使用につながる可能性があります。この問題の程度を理解するために、まずデータセット内に何種類の一意の路線文字列があるかを調べてみましょう。

まず、Python インタープリタを開きます。ターミナルで以下のコマンドを実行することで、これを行うことができます。

python3

Python インタープリタが開いたら、CTA バスデータを読み込み、一意の路線を見つけます。これを実現するためのコードは以下の通りです。

import reader
rows = reader.read_csv_as_dicts('ctabus.csv', [str, str, str, int])

## Find unique route names
routes = {row['route'] for row in rows}
print(f"Number of unique route names: {len(routes)}")

このコードでは、まず reader モジュールをインポートします。このモジュールには、CSV ファイルを辞書として読み取る関数が含まれていると思われます。次に、read_csv_as_dicts 関数を使用して ctabus.csv ファイルからデータを読み込みます。2 番目の引数 [str, str, str, int] は、CSV ファイルの各列のデータ型を指定します。その後、セット内包表記を使用して、データセット内のすべての一意の路線名を見つけ、一意の路線名の数を出力します。

出力は以下のようになるはずです。

Number of unique route names: 181

では、これらの路線に対して何種類の異なる文字列オブジェクトが作成されているかを確認しましょう。たとえ一意の路線名が 181 種類しかなくても、Python はデータセット内の路線名の各出現に対して新しい文字列オブジェクトを作成する可能性があります。これを検証するために、id() 関数を使用して各文字列オブジェクトの一意の識別子を取得します。

## Count unique string object IDs
routeids = {id(row['route']) for row in rows}
print(f"Number of unique route string objects: {len(routeids)}")

出力は驚くかもしれません。

Number of unique route string objects: 542305

これは、一意の路線名は 181 種類しかないが、50 万を超える一意の文字列オブジェクトが存在することを示しています。これは、Python が値が同じであっても各行に対して新しい文字列オブジェクトを作成するために起こります。これは、特に大規模なデータセットを扱う際に、著しいメモリの浪費につながる可能性があります。

メモリ節約のための文字列インターニング

Python は、sys.intern() 関数を使用して文字列の「インターニング」(再利用)を行う方法を提供しています。データセットに多くの重複する文字列がある場合、文字列インターニングはメモリを節約することができます。文字列にインターニングを適用すると、Python はインターンプール内に同じ文字列が既に存在するかどうかを確認します。存在する場合、新しいオブジェクトを作成する代わりに、既存の文字列オブジェクトへの参照を返します。

文字列インターニングがどのように機能するかを簡単な例で示しましょう。

import sys

## Without interning
a = 'hello world'
b = 'hello world'
print(f"a is b (without interning): {a is b}")

## With interning
a = sys.intern(a)
b = sys.intern(b)
print(f"a is b (with interning): {a is b}")

このコードでは、まずインターニングを行わずに同じ値を持つ 2 つの文字列変数 ab を作成します。is 演算子は、2 つの変数が同じオブジェクトを参照しているかどうかをチェックします。インターニングを行わない場合、ab は異なるオブジェクトであるため、a is bFalse を返します。次に、sys.intern() を使用して両方の文字列にインターニングを適用します。インターニングの後、ab はインターンプール内の同じオブジェクトを参照するため、a is bTrue を返します。

出力は以下のようになるはずです。

a is b (without interning): False
a is b (with interning): True

では、CTA バスデータを読み取る際に文字列インターニングを使用して、メモリ使用量を削減しましょう。また、tracemalloc モジュールを使用して、インターニングの前後のメモリ使用量を追跡します。

import sys
import reader
import tracemalloc

## Start memory tracking
tracemalloc.start()

## Read data with interning for the route column
rows = reader.read_csv_as_dicts('ctabus.csv', [sys.intern, str, str, int])

## Check unique route objects again
routeids = {id(row['route']) for row in rows}
print(f"Number of unique route string objects (with interning): {len(routeids)}")

## 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")

このコードでは、まず tracemalloc.start() を使用してメモリ追跡を開始します。次に、最初の列のデータ型として sys.intern を渡すことで、路線列にインターニングを適用して CTA バスデータを読み取ります。その後、再度一意の路線文字列オブジェクトの数を確認し、現在のメモリ使用量とピークメモリ使用量を出力します。

出力は以下のようになるはずです。

Number of unique route string objects (with interning): 181
Current memory usage: 189.56 MB
Peak memory usage: 209.32 MB

インタープリタを再起動し、路線と日付の両方の文字列にインターニングを適用して、メモリ使用量をさらに削減できるかどうかを試してみましょう。

exit()

再度 Python を起動します。

python3
import sys
import reader
import tracemalloc

## Start memory tracking
tracemalloc.start()

## Read data with interning for both route and date columns
rows = reader.read_csv_as_dicts('ctabus.csv', [sys.intern, sys.intern, str, int])

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

出力は、メモリ使用量がさらに減少していることを示すはずです。

Current memory usage (interning route and date): 170.23 MB
Peak memory usage (interning route and date): 190.05 MB

これは、Python のメモリモデルを理解し、文字列インターニングなどの技術を使用することで、特に繰り返し値を含む大規模なデータセットを扱う際に、プログラムを最適化するのに役立つことを示しています。

最後に、Python インタープリタを終了します。

exit()

列指向のデータストレージ

これまで、CSV データを行辞書のリストとして格納してきました。これは、CSV ファイルの各行が辞書として表され、キーが列ヘッダーで値がその行の対応するデータであることを意味します。しかし、大規模なデータセットを扱う場合、この方法は非効率的です。列指向の形式でデータを格納する方が良い選択肢となります。列指向のアプローチでは、各列のデータが別々のリストに格納されます。これにより、同じデータ型がグループ化されるため、メモリ使用量を大幅に削減でき、列ごとのデータ集計などの特定の操作のパフォーマンスも向上させることができます。

列指向のデータリーダーの作成

ここでは、列指向の形式で CSV データを読み取るのに役立つ新しいファイルを作成します。プロジェクトディレクトリに colreader.py という名前の新しいファイルを作成し、以下のコードを記述します。

import csv

class DataCollection:
    def __init__(self, headers, columns):
        """
        Initialize a column-oriented data collection.

        Parameters:
        headers (list): Column header names
        columns (dict): Dictionary mapping header names to column data lists
        """
        self.headers = headers
        self.columns = columns
        self._length = len(columns[headers[0]]) if headers else 0

    def __len__(self):
        """Return the number of rows in the collection."""
        return self._length

    def __getitem__(self, index):
        """
        Get a row by index, presented as a dictionary.

        Parameters:
        index (int): Row index

        Returns:
        dict: Dictionary representing the row at the given index
        """
        if isinstance(index, int):
            if index < 0 or index >= self._length:
                raise IndexError("Index out of range")

            return {header: self.columns[header][index] for header in self.headers}
        else:
            raise TypeError("Index must be an integer")

def read_csv_as_columns(filename, types):
    """
    Read a CSV file into a column-oriented data structure, converting each field
    according to the types provided.

    Parameters:
    filename (str): Name of the CSV file to read
    types (list): List of type conversion functions for each column

    Returns:
    DataCollection: Column-oriented data collection representing the CSV data
    """
    with open(filename, 'r') as f:
        rows = csv.reader(f)
        headers = next(rows)  ## Get the column headers

        ## Initialize columns
        columns = {header: [] for header in headers}

        ## Read data into columns
        for row in rows:
            ## Convert values according to the specified types
            converted_values = [func(val) for func, val in zip(types, row)]

            ## Add each value to its corresponding column
            for header, value in zip(headers, converted_values):
                columns[header].append(value)

    return DataCollection(headers, columns)

このコードは 2 つの重要なことを行います。

  1. DataCollection クラスを定義します。このクラスはデータを列で格納しますが、データを行辞書のリストのようにアクセスできるようにします。これは、データを扱う際に馴染みのある方法を提供するために便利です。
  2. read_csv_as_columns 関数を定義します。この関数は、CSV ファイルからデータを読み取り、列指向の構造に格納します。また、提供された型に従って CSV ファイルの各フィールドを変換します。

列指向のリーダーのテスト

CTA バスデータを使用して、列指向のリーダーをテストしましょう。まず、Python インタープリタを開きます。ターミナルで以下のコマンドを実行することで、これを行うことができます。

python3

Python インタープリタが開いたら、以下のコードを実行します。

import colreader
import tracemalloc
from sys import intern

## Start memory tracking
tracemalloc.start()

## Read data into column-oriented structure with string interning
data = colreader.read_csv_as_columns('ctabus.csv', [intern, intern, intern, int])

## Check that we can access the data like a list of dictionaries
print(f"Number of rows: {len(data)}")
print("First 3 rows:")
for i in range(3):
    print(data[i])

## 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")

出力は以下のようになるはずです。

Number of rows: 577563
First 3 rows:
{'route': '3', 'date': '01/01/2001', 'daytype': 'U', 'rides': 7354}
{'route': '4', 'date': '01/01/2001', 'daytype': 'U', 'rides': 9288}
{'route': '6', 'date': '01/01/2001', 'daytype': 'U', 'rides': 6048}
Current memory usage: 38.67 MB
Peak memory usage: 103.42 MB

では、これを以前の行指向のアプローチと比較してみましょう。同じ Python インタープリタで以下のコードを実行します。

import reader
import tracemalloc
from sys import intern

## Reset memory tracking
tracemalloc.reset_peak()

## Read data into row-oriented structure with string interning
rows = reader.read_csv_as_dicts('ctabus.csv', [intern, intern, intern, int])

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

出力は以下のようになるはずです。

Current memory usage (row-oriented): 170.23 MB
Peak memory usage (row-oriented): 190.05 MB

ご覧の通り、列指向のアプローチは大幅に少ないメモリを使用します!

また、以前と同じようにデータを分析できることもテストしてみましょう。以下のコードを実行します。

## Find all unique routes in the column-oriented data
routes = {row['route'] for row in data}
print(f"Number of unique routes: {len(routes)}")

## Count rides per route (first 5)
from collections import defaultdict
route_rides = defaultdict(int)
for row in data:
    route_rides[row['route']] += row['rides']

## Show the top 5 routes by total rides
top_routes = sorted(route_rides.items(), key=lambda x: x[1], reverse=True)[:5]
print("Top 5 routes by total rides:")
for route, rides in top_routes:
    print(f"Route {route}: {rides:,} rides")

出力は以下のようになるはずです。

Number of unique routes: 181
Top 5 routes by total rides:
Route 9: 158,545,826 rides
Route 49: 129,872,910 rides
Route 77: 120,086,065 rides
Route 79: 109,348,708 rides
Route 4: 91,405,538 rides

最後に、以下のコマンドを実行して Python インタープリタを終了します。

exit()

列指向のアプローチはメモリを節約するだけでなく、以前と同じ分析を行うこともできることがわかります。これは、異なるデータストレージ戦略がパフォーマンスに大きな影響を与える一方で、データを扱うための同じインターフェースを提供することを示しています。

✨ 解答を確認して練習

まとめ

この実験では、いくつかの重要な Python の概念を学びました。まず、Python が関数、型、その他のエンティティを第一級オブジェクトとして扱い、通常のデータのように渡したり格納したりできることを理解しました。次に、自動型変換を伴う CSV データ処理のための再利用可能なユーティリティ関数を作成しました。

さらに、Python のメモリモデルを探索し、文字列インターニングを使用して繰り返しデータのメモリ使用量を削減しました。また、大規模なデータセットに対して、より効率的な列指向のストレージ方法を実装し、使い慣れたユーザーインターフェースを提供しました。これらの概念は、Python のデータ処理における柔軟性と強力さを示しており、これらの技術は実世界のデータ分析プロジェクトに適用できます。