シンボルの制御とサブモジュールの結合

Intermediate

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

はじめに

この実験では、Python パッケージの組織化に関連する重要な概念を学びます。まず、Python モジュールで __all__ を使用してエクスポートされるシンボルを制御する方法を学びます。このスキルは、モジュールから公開される内容を管理するために重要です。

次に、サブモジュールを組み合わせてインポートを簡素化する方法を理解し、コードの組織化を向上させるためのモジュール分割のテクニックを習得します。これらの実践により、Python コードの可読性と保守性が向上します。

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

パッケージインポートの複雑さを理解する

Python パッケージを使い始めると、すぐにモジュールのインポートが非常に複雑で冗長になることに気づくでしょう。この複雑さは、コードの読み書きを困難にする可能性があります。この実験では、この問題を詳しく調べ、インポートプロセスを簡素化する方法を学びます。

現在のインポート構造

まず、ターミナルを開きましょう。ターミナルは、コンピュータのオペレーティングシステムと対話するための強力なツールです。ターミナルを開いたら、プロジェクトディレクトリに移動する必要があります。プロジェクトディレクトリは、Python プロジェクトに関連するすべてのファイルが保存されている場所です。これを行うには、「change directory」(ディレクトリを変更する)を意味する cd コマンドを使用します。

cd ~/project

これでプロジェクトディレクトリにいるので、structly パッケージの現在の構造を調べてみましょう。Python のパッケージは、関連するモジュールを組織する方法です。ls -la コマンドを使用して、structly パッケージ内のすべてのファイルとディレクトリ(隠しファイルを含む)をリスト表示できます。

ls -la structly

structly パッケージ内にいくつかの Python モジュールがあることに気づくでしょう。これらのモジュールには、コードで使用できる関数やクラスが含まれています。ただし、これらのモジュールの機能を使用したい場合、現在は長いインポート文を使用する必要があります。例えば:

from structly.structure import Structure
from structly.reader import read_csv_as_instances
from structly.tableformat import create_formatter, print_table

これらの長いインポートパスは、書くのが面倒です。特に、コード内で何度も使用する場合には顕著です。また、コードの可読性を低下させるため、コードを理解したりデバッグしたりする際に問題になる可能性があります。この実験では、これらのインポートを簡素化する方法でパッケージを組織する方法を学びます。

パッケージの __init__.py ファイルの内容を見てみましょう。__init__.py ファイルは、Python パッケージにおける特別なファイルです。パッケージがインポートされると実行され、パッケージを初期化したり、必要なインポートを設定したりするために使用できます。

cat structly/__init__.py

おそらく、__init__.py ファイルは空であるか、ほとんどコードが含まれていないことがわかるでしょう。次のステップでは、このファイルを変更してインポート文を簡素化します。

目標

この実験の終わりまでに、はるかに簡単なインポート文を使用できるようにすることが目標です。先ほど見た長いインポートパスの代わりに、次のような文を使用できるようになります。

from structly import Structure, read_csv_as_instances, create_formatter, print_table

または、さらに:

from structly import *

これらの簡単なインポート文を使用することで、コードがきれいになり、作業が容易になります。また、コードの作成と保守にかかる時間と労力を節約することができます。

__all__ を使用したエクスポートシンボルの制御

Python で from module import * 文を使用する場合、モジュールからインポートするシンボル(関数、クラス、変数)を制御したいことがあります。このような場合に __all__ 変数が便利です。from module import * 文は、モジュール内のすべてのシンボルを現在の名前空間にインポートする方法です。ただし、場合によってはすべてのシンボルをインポートしたくないことがあります。特に、シンボルが多数ある場合や、一部のシンボルがモジュール内部で使用するものである場合です。__all__ 変数を使用すると、この文を使用したときに具体的にどのシンボルをインポートするかを指定できます。

__all__ とは何か?

__all__ 変数は文字列のリストです。このリスト内の各文字列は、from module import * 文を使用したときにモジュールがエクスポートするシンボル(関数、クラス、または変数)を表します。モジュール内で __all__ 変数が定義されていない場合、import * 文はアンダースコアで始まらないすべてのシンボルをインポートします。アンダースコアで始まるシンボルは、通常、モジュールのプライベートまたは内部用と見なされ、直接インポートすることを意図していません。

各サブモジュールの修正

では、structly パッケージ内の各サブモジュールに __all__ 変数を追加しましょう。これにより、from module import * 文を使用したときに各サブモジュールからエクスポートされるシンボルを制御できます。

  1. まず、structure.py を修正しましょう。
touch ~/project/structly/structure.py

このコマンドは、プロジェクトの structly ディレクトリに structure.py という名前の新しいファイルを作成します。ファイルを作成した後、__all__ 変数を追加する必要があります。インポート文の直後、ファイルの上部近くに次の行を追加します。

__all__ = ['Structure']

この行は、from structure import * を使用した場合、Structure シンボルのみがインポートされることを Python に伝えます。ファイルを保存し、エディタを終了します。

  1. 次に、reader.py を修正しましょう。
touch ~/project/structly/reader.py

このコマンドは、structly ディレクトリに reader.py という名前の新しいファイルを作成します。今度は、ファイルを調べて read_csv_as_ で始まるすべての関数を見つけます。これらの関数がエクスポートしたい関数です。そして、これらの関数名をすべて含む __all__ リストを追加します。次のようになります。

__all__ = ['read_csv_as_instances', 'read_csv_as_dicts', 'read_csv_as_columns']

実際の関数名は、ファイル内にあるものによって異なる場合があります。見つけたすべての read_csv_as_* 関数を含めるようにしてください。ファイルを保存し、エディタを終了します。

  1. では、tableformat.py を修正しましょう。
touch ~/project/structly/tableformat.py

このコマンドは、structly ディレクトリに tableformat.py という名前の新しいファイルを作成します。ファイルの上部近くに次の行を追加します。

__all__ = ['create_formatter', 'print_table']

この行は、from tableformat import * を使用した場合、create_formatterprint_table シンボルのみがインポートされることを指定します。ファイルを保存し、エディタを終了します。

__init__.py での統一的なインポート

各モジュールがエクスポートするものを定義したので、__init__.py ファイルを更新してこれらのすべてのシンボルをインポートできます。__init__.py ファイルは Python パッケージにおける特別なファイルです。パッケージがインポートされると実行され、パッケージを初期化したり、サブモジュールからシンボルをインポートしたりするために使用できます。

touch ~/project/structly/__init__.py

このコマンドは、structly ディレクトリに新しい __init__.py ファイルを作成します。ファイルの内容を次のように変更します。

## structly/__init__.py

from .structure import *
from .reader import *
from .tableformat import *

これらの行は、structurereader、および tableformat サブモジュールからすべてのエクスポートされたシンボルをインポートします。モジュール名の前のドット (.) は、これらが相対インポートであることを示しており、同じパッケージ内からのインポートであることを意味します。ファイルを保存し、エディタを終了します。

変更のテスト

変更が機能することを確認するために、簡単なテストファイルを作成しましょう。このテストファイルは、__all__ 変数で指定したシンボルをインポートしようとし、インポートが成功した場合は成功メッセージを表示します。

touch ~/project/test_structly.py

このコマンドは、プロジェクトディレクトリに test_structly.py という名前の新しいファイルを作成します。ファイルに次の内容を追加します。

## A simple test to verify our imports work correctly

from structly import Structure
from structly import read_csv_as_instances
from structly import create_formatter, print_table

print("Successfully imported all required symbols!")

これらの行は、structly パッケージから Structure クラス、read_csv_as_instances 関数、および create_formatterprint_table 関数をインポートしようとします。インポートが成功した場合、プログラムは「Successfully imported all required symbols!」というメッセージを表示します。ファイルを保存し、エディタを終了します。では、このテストを実行しましょう。

cd ~/project
python test_structly.py

cd ~/project コマンドは、現在の作業ディレクトリをプロジェクトディレクトリに変更します。python test_structly.py コマンドは、test_structly.py スクリプトを実行します。すべてが正しく動作している場合、画面に「Successfully imported all required symbols!」というメッセージが表示されるはずです。

パッケージからの全シンボルエクスポート

Python では、コードを効果的に管理するためにパッケージの組織化が重要です。今回は、パッケージの組織化をさらに進めます。パッケージレベルでどのシンボルをエクスポートするかを定義します。シンボルをエクスポートするとは、特定の関数、クラス、または変数をコードの他の部分や、あなたのパッケージを使用する可能性のある他の開発者に利用可能にすることを意味します。

パッケージに __all__ を追加する

Python パッケージを扱う際に、from structly import * 文を使用したときにどのシンボルにアクセスできるかを制御したいことがあります。このような場合に __all__ リストが便利です。パッケージの __init__.py ファイルに __all__ リストを追加することで、from structly import * 文を使用したときにどのシンボルが利用可能かを正確に制御できます。

まず、__init__.py ファイルを作成または更新しましょう。ファイルが存在しない場合は touch コマンドを使用して作成します。

touch ~/project/structly/__init__.py

次に、__init__.py ファイルを開き、__all__ リストを追加します。このリストには、エクスポートしたいすべてのシンボルを含める必要があります。シンボルは、それらが由来する場所(例えば structurereadertableformat モジュール)に基づいてグループ化されます。

## structly/__init__.py

from .structure import *
from .reader import *
from .tableformat import *

## Define what symbols are exported when using "from structly import *"
__all__ = ['Structure',  ## from structure
           'read_csv_as_instances', 'read_csv_as_dicts', 'read_csv_as_columns',  ## from reader
           'create_formatter', 'print_table']  ## from tableformat

コードを追加したら、ファイルを保存し、エディタを終了します。

import * の理解

from module import * パターンは、ほとんどの Python コードでは一般的に推奨されていません。これにはいくつかの理由があります。

  1. 予期しないシンボルで名前空間を汚染する可能性があります。これは、現在の名前空間に予期しない変数や関数が含まれることを意味し、名前の衝突につながる可能性があります。
  2. 特定のシンボルがどこから来たかが不明確になります。import * を使用すると、シンボルがどのモジュールから来ているかを判断するのが難しく、コードの理解と保守が困難になります。
  3. シャドウイング(shadowing)の問題を引き起こす可能性があります。シャドウイングは、ローカル変数または関数が他のモジュールの変数または関数と同じ名前を持っている場合に発生し、予期しない動作を引き起こす可能性があります。

ただし、import * を使用するのが適切な特定のケースもあります。

  • まとまった単位として使用することを想定したパッケージの場合。パッケージが単一のユニットとして使用されることを意図している場合、import * を使用すると、必要なすべてのシンボルにアクセスしやすくなります。
  • パッケージが __all__ を介して明確なインターフェースを定義している場合。__all__ リストを使用することで、エクスポートされるシンボルを制御できるため、import * を安全に使用できます。
  • Python REPL(Read-Eval-Print Loop)のような対話的な使用の場合。対話的な環境では、一度にすべてのシンボルをインポートするのが便利なことがあります。

import * でのテスト

一度にすべてのシンボルをインポートできることを確認するために、別のテストファイルを作成しましょう。touch コマンドを使用してファイルを作成します。

touch ~/project/test_import_all.py

次に、test_import_all.py ファイルを開き、以下の内容を追加します。このコードは、structly パッケージからすべてのシンボルをインポートし、いくつかの重要なシンボルが利用可能かどうかをテストします。

## Test importing everything at once

from structly import *

## Try using the imported symbols
print(f"Structure symbol is available: {Structure is not None}")
print(f"read_csv_as_instances symbol is available: {read_csv_as_instances is not None}")
print(f"create_formatter symbol is available: {create_formatter is not None}")
print(f"print_table symbol is available: {print_table is not None}")

print("All symbols successfully imported!")

ファイルを保存し、エディタを終了します。では、テストを実行しましょう。まず、cd コマンドを使用してプロジェクトディレクトリに移動し、その後 Python スクリプトを実行します。

cd ~/project
python test_import_all.py

すべてが正しく設定されている場合、すべてのシンボルが正常にインポートされたことを確認できるはずです。

コードの整理のためのモジュール分割

Python プロジェクトが拡大するにつれて、単一のモジュールファイルが非常に大きくなり、関連するが異なる複数のコンポーネントを含むようになることがあります。このような場合、モジュールをサブモジュールを持つパッケージに分割するのが良いプラクティスです。このアプローチにより、コードがより整理され、保守が容易になり、拡張性も向上します。

現在の構造の理解

tableformat.py モジュールは、大きなモジュールの良い例です。このモジュールにはいくつかのフォーマッタクラスが含まれており、それぞれが異なる方法でデータをフォーマットする役割を持っています。

  • TableFormatter (基底クラス): これは他のすべてのフォーマッタクラスの基底クラスです。他のクラスが継承して実装する基本的な構造とメソッドを定義しています。
  • TextTableFormatter: このクラスはデータをプレーンテキストでフォーマットします。
  • CSVTableFormatter: このクラスはデータを CSV (Comma-Separated Values) 形式でフォーマットします。
  • HTMLTableFormatter: このクラスはデータを HTML (Hypertext Markup Language) 形式でフォーマットします。

このモジュールを、各フォーマッタタイプに対して別々のファイルを持つパッケージ構造に再編成します。これにより、コードがよりモジュール化され、管理が容易になります。

ステップ 1: キャッシュファイルのクリーンアップ

コードを再編成する前に、Python のキャッシュファイルをクリーンアップするのが良い考えです。これらのファイルは Python によって作成され、コードの実行を高速化するためのものですが、コードを変更する際に問題を引き起こすことがあります。

cd ~/project/structly
rm -rf __pycache__

上記のコマンドで、cd ~/project/structly は現在のディレクトリをプロジェクトの structly ディレクトリに変更します。rm -rf __pycache____pycache__ ディレクトリとそのすべての内容を削除します。-r オプションは再帰的 (recursive) を意味し、__pycache__ ディレクトリ内のすべてのファイルとサブディレクトリを削除します。-f オプションは強制 (force) を意味し、確認を求めることなくファイルを削除します。

ステップ 2: 新しいパッケージ構造の作成

では、パッケージの新しいディレクトリ構造を作成しましょう。tableformat という名前のディレクトリと、その中に formats という名前のサブディレクトリを作成します。

mkdir -p tableformat/formats

mkdir コマンドはディレクトリを作成するために使用されます。-p オプションは親ディレクトリ (parents) を意味し、必要なすべての親ディレクトリが存在しない場合に作成します。したがって、tableformat ディレクトリが存在しない場合は最初に作成され、その後 formats ディレクトリがその中に作成されます。

ステップ 3: 元のファイルの移動と名前変更

次に、元の tableformat.py ファイルを新しい構造に移動し、formatter.py に名前を変更します。

mv tableformat.py tableformat/formatter.py

mv コマンドはファイルを移動または名前変更するために使用されます。この場合、tableformat.py ファイルを tableformat ディレクトリに移動し、formatter.py に名前を変更しています。

ステップ 4: コードを別々のファイルに分割する

ここで、各フォーマッタ用のファイルを作成し、関連するコードをそれらに移動する必要があります。

1. 基底フォーマッタファイルの作成

touch tableformat/formatter.py

touch コマンドは空のファイルを作成するために使用されます。この場合、tableformat ディレクトリに formatter.py という名前のファイルを作成しています。

このファイルには、TableFormatter 基底クラスと print_tablecreate_formatter のような一般的なユーティリティ関数を保持します。ファイルは次のようになるはずです。

## Base TableFormatter class and utility functions

__all__ = ['TableFormatter', 'print_table', 'create_formatter']

class TableFormatter:
    def headings(self, headers):
        '''
        Emit table headings.
        '''
        raise NotImplementedError()

    def row(self, rowdata):
        '''
        Emit a single row of table data.
        '''
        raise NotImplementedError()

def print_table(objects, columns, formatter):
    '''
    Make a nicely formatted table from a list of objects and attribute names.
    '''
    formatter.headings(columns)
    for obj in objects:
        rowdata = [getattr(obj, name) for name in columns]
        formatter.row(rowdata)

def create_formatter(fmt):
    '''
    Create an appropriate formatter given an output format name.
    '''
    if fmt == 'text':
        from .formats.text import TextTableFormatter
        return TextTableFormatter()
    elif fmt == 'csv':
        from .formats.csv import CSVTableFormatter
        return CSVTableFormatter()
    elif fmt == 'html':
        from .formats.html import HTMLTableFormatter
        return HTMLTableFormatter()
    else:
        raise ValueError(f'Unknown format {fmt}')

__all__ 変数は、from module import * を使用したときにインポートされるシンボルを指定するために使用されます。この場合、TableFormatterprint_tablecreate_formatter シンボルのみがインポートされるように指定しています。

TableFormatter クラスは他のすべてのフォーマッタクラスの基底クラスです。headingsrow の 2 つのメソッドを定義しており、サブクラスによって実装されることを意図しています。

print_table 関数は、オブジェクトのリスト、列名のリスト、およびフォーマッタオブジェクトを受け取り、データを整形された表形式で出力するユーティリティ関数です。

create_formatter 関数は、フォーマット名を引数として受け取り、適切なフォーマッタオブジェクトを返すファクトリ関数です。

これらの変更を加えた後、ファイルを保存して終了します。

2. テキストフォーマッタの作成

touch tableformat/formats/text.py

このファイルには TextTableFormatter クラスのみを追加します。

## Text formatter implementation

__all__ = ['TextTableFormatter']

from ..formatter import TableFormatter

class TextTableFormatter(TableFormatter):
    '''
    Emit a table in plain-text format
    '''
    def headings(self, headers):
        print(' '.join('%10s' % h for h in headers))
        print(('-'*10 + ' ')*len(headers))

    def row(self, rowdata):
        print(' '.join('%10s' % d for d in rowdata))

__all__ 変数は、from module import * を使用したときにインポートされるシンボルを指定しており、この場合 TextTableFormatter シンボルのみがインポートされるように指定しています。

from ..formatter import TableFormatter 文は、親ディレクトリの formatter.py ファイルから TableFormatter クラスをインポートします。

TextTableFormatter クラスは TableFormatter クラスを継承し、headingsrow メソッドを実装してデータをプレーンテキストでフォーマットします。

これらの変更を加えた後、ファイルを保存して終了します。

3. CSV フォーマッタの作成

touch tableformat/formats/csv.py

このファイルには CSVTableFormatter クラスのみを追加します。

## CSV formatter implementation

__all__ = ['CSVTableFormatter']

from ..formatter import TableFormatter

class CSVTableFormatter(TableFormatter):
    '''
    Output data in CSV format.
    '''
    def headings(self, headers):
        print(','.join(headers))

    def row(self, rowdata):
        print(','.join(str(d) for d in rowdata))

前のステップと同様に、__all__ 変数を指定し、TableFormatter クラスをインポートし、headingsrow メソッドを実装してデータを CSV 形式でフォーマットします。

これらの変更を加えた後、ファイルを保存して終了します。

4. HTML フォーマッタの作成

touch tableformat/formats/html.py

このファイルには HTMLTableFormatter クラスのみを追加します。

## HTML formatter implementation

__all__ = ['HTMLTableFormatter']

from ..formatter import TableFormatter

class HTMLTableFormatter(TableFormatter):
    '''
    Output data in HTML format.
    '''
    def headings(self, headers):
        print('<tr>', end='')
        for h in headers:
            print(f'<th>{h}</th>', end='')
        print('</tr>')

    def row(self, rowdata):
        print('<tr>', end='')
        for d in rowdata:
            print(f'<td>{d}</td>', end='')
        print('</tr>')

再び、__all__ 変数を指定し、TableFormatter クラスをインポートし、headingsrow メソッドを実装してデータを HTML 形式でフォーマットします。

これらの変更を加えた後、ファイルを保存して終了します。

ステップ 5: パッケージ初期化ファイルの作成

Python では、__init__.py ファイルはディレクトリを Python パッケージとしてマークするために使用されます。tableformatformats の両方のディレクトリに __init__.py ファイルを作成する必要があります。

1. tableformat パッケージ用のファイルを作成する

touch tableformat/__init__.py

ファイルに次の内容を追加します。

## Re-export the original symbols from tableformat.py
from .formatter import *

この文は formatter.py ファイルからすべてのシンボルをインポートし、tableformat パッケージをインポートしたときにそれらを利用可能にします。

これらの変更を加えた後、ファイルを保存して終了します。

2. formats パッケージ用のファイルを作成する

touch tableformat/formats/__init__.py

このファイルは空のままにしても、簡単なドキュメント文字列を追加しても構いません。

'''
Format implementations for different output formats.
'''

ドキュメント文字列は、formats パッケージの機能について簡単な説明を提供します。

これらの変更を加えた後、ファイルを保存して終了します。

ステップ 6: 新しい構造のテスト

変更が正しく機能することを確認するために、簡単なテストを作成しましょう。

cd ~/project
touch test_tableformat.py

test_tableformat.py ファイルに次の内容を追加します。

## Test the tableformat package restructuring

from structly import *

## Create formatters of each type
text_fmt = create_formatter('text')
csv_fmt = create_formatter('csv')
html_fmt = create_formatter('html')

## Define some test data
class TestData:
    def __init__(self, name, value):
        self.name = name
        self.value = value

## Create a list of test objects
data = [
    TestData('apple', 10),
    TestData('banana', 20),
    TestData('cherry', 30)
]

## Test text formatter
print("\nText Format:")
print_table(data, ['name', 'value'], text_fmt)

## Test CSV formatter
print("\nCSV Format:")
print_table(data, ['name', 'value'], csv_fmt)

## Test HTML formatter
print("\nHTML Format:")
print_table(data, ['name', 'value'], html_fmt)

このテストコードは、structly パッケージから必要な関数とクラスをインポートし、各タイプのフォーマッタを作成し、いくつかのテストデータを定義し、それぞれのフォーマッタをテストしてデータを対応する形式で出力します。

これらの変更を加えた後、ファイルを保存して終了します。では、テストを実行しましょう。

python test_tableformat.py

同じデータが 3 つの異なる形式(テキスト、CSV、HTML)でフォーマットされた結果が表示されるはずです。期待される出力が表示されれば、コードの再編成が成功したことを意味します。

まとめ

この実験では、いくつかの重要な Python パッケージの組織化技術を学びました。まず、__all__ 変数を使用して、モジュールがエクスポートするシンボルを明示的に定義する方法を習得しました。次に、トップレベルのパッケージからサブモジュールのシンボルを再エクスポートすることで、より使いやすいパッケージインターフェースを作成しました。

これらの技術は、クリーンで保守可能で使いやすい Python パッケージを作成するために不可欠です。これらにより、ユーザーの視点を制御し、インポートプロセスを簡素化し、プロジェクトが拡大するにつれてコードを論理的に整理することができます。