Lesson 8

特別な属性

Lesson 8 Chapter 1
はじめに

このレッスンではPythonに標準で用意されている特殊な変数について学習していきます。具体的には以下の変数について学びます。

  • __name__(__main__)
  • __path__
  • __dict__
  • __annotations__

これらの概要を学ぶことで、今まで"おまじない"として見ていた部分の意味を理解することができたり、実際に変数を活用してより便利なコードの記述方法を習得できます。 見た目は特殊ですがその実態はシンプルな物が多いので、焦らずに学習していきましょう。

Lesson 8 Chapter 2
__name__と__main__

__name__変数はプログラム実行時に自動で生成され、実行中のスクリプトのモジュール名が格納されます。 例えば、test.pyがメインのスクリプトとして実行されるとき、__name__は"__main__"になります。 また、test.pyというモジュールが他のスクリプトにインポートされている場合、test.pyの__name__は "test"になります。 これにより、プログラムが直接実行されているか、他のスクリプトからインポートされているかを判断することができます。

実際にPythonファイルを用意して実行してみましょう。また、このレッスンでは「Visual Studio Code」というエディターを用いてテストをしていきますが、ご自身が普段使用しているエディターでも構いません。 フォルダツリーと各ファイル内容は以下の通りです。

TEST_PROJECT
  ├ test_01.py
  └ test_02.py
test_01.py
from test_02 import sample_func

print(__name__)
sample_func()
test_02.py
def sample_func():
    print(__name__)

このプログラムは、test_01.pytest_02.py__name__をそれぞれ出力するプログラムとなっています。 これらのファイルを用意できたらtest_01.pyを実行します。実行方法については各環境によって少し異なる場合がありますが、基本的にはIDEのコンソール画面からpythonコマンドでtest_01.pyを実行する方法と、IDE上に用意されているプログラム実行ボタンを押して実行します。

$ python test_01.py

出力結果

__main__
test_02

するとこのような出力結果となります。どちらもprint(__name__)という同じ処理ですが、モジュールとしてインポートされたtest_02.pyの方では自身のモジュール名(ファイル名)が__name__に格納されていることが確認できます。 一方で、スクリプトとして実行したtest_01.pyには__main__という文字が格納されています。これが__name__の仕組みです。

__main__の主な使用用途

また、これらの変数を使用することで、インポートされたスクリプトなのか、スクリプトとして実行されているのかを判断することができます。これは、スクリプト内で特定のセットアップや処理を行う必要がある場合に利用されます。 例えば、スクリプトがインポートされる場合は特定のセットアップを行わないようにするために、次のように書くことができます。

sample.py
if __name__ == "__main__":
    # この中にスクリプトとして実行される場合にのみ実行される処理を記述する

__name__変数は、そのスクリプトがメインとして実行されるときに__main__となるため、上記の条件式はTrueになります。 以上が、__name__変数と__main__の概要となります。

Lesson 8 Chapter 3
__path__

__path__変数は、Pythonのパッケージを探索するために使用される検索パスのリストを表す特殊変数です。これは、Pythonがインポートするパッケージの場所(パス)を参照するために使用されます。 例えば、test_packageというパッケージが/home/user/scriptsにある場合__path__は「/home/user/scripts」になります。この変数は、パッケージがインポートされるときに自動的に設定されます。

このチャプターでも実際の使用方法を確認してみましょう。先程のtest_projectを流用して以下の構成とします。また、__init__.pyの中身には何も記述をしません。

TEST_PROJECT
  ├ test_package
  │   └ __init__.py
  ├ test_01.py
  └ test_02.py
test_01.py
import test_02
import test_package

print(test_package.__path__)

出力結果

['/Users/developer/test_project/test_package']

上記の出力結果は実行環境によって異なる場合がありますので、ご自身の環境でご確認ください。 ここではtest_packageというフォルダ(パッケージ)を用意して、test_01.pyでそれをインポートし、パッケージの__path__を出力するといった処理の流れになっています。

では、内容を少し変更してtest_02モジュールのパスを出力してみます。

test_01.py
import test_02
import test_package

print(test_02.__path__)

出力結果

traceback (most recent call last):
  File "/Users/developer/test_project/test_01.py", line 4, in <module>
    print(test_02.__path__)
AttributeError: module 'test_02' has no attribute '__path__'. Did you mean: '__name__'?

どうやら上手く出力出来なかった様です。これはtest_02がパッケージではなくモジュールであることが原因となります。

パッケージとモジュールの違い

パッケージやモジュールと行った概念が出てきたと思いますが、具体的な違いについて学んでいきましょう。 パッケージは複数のモジュールをまとめており、モジュールは〇〇.pyファイル単体を指すことが多いです。

また、Pythonのパッケージはフォルダ(ディレクトリ)として存在することが一般的ですが、主な見分け方としては、そのファイル内に__init__.pyファイルがあるということです。 Pythonではこの__init__.pyファイルがある場合に、そのフォルダをパッケージとして認識します。 以上が、__path__の概要となります。

Lesson 8 Chapter 4
__dict__

__dict__変数は、オブジェクトが持つ変数やメソッドを格納した辞書型オブジェクトです。主にオブジェクトの中に格納されている変数やメソッドにアクセスするために使用されます。 オブジェクト指向プログラミングにおいて、オブジェクトは、変数(属性やプロパティ)とメソッドを持ちます。__dict__は、変数やメソッドの名前をキー、その値を値として格納されています。 また__dict__変数を使用することで、オブジェクトの中に格納されている変数や関数にアクセスしたり、変数や関数を追加・削除したりすることができます。

プロパティの参照

では、実際に__dict__を使用してオブジェクトの内容を確認してみましょう。

sample.py
class MyClass:
    def __init__(self):
        self.x = 1
        self.y = 2

    def my_method(self):
        print("Hello, World!")

obj = MyClass()
print(obj.__dict__)

出力結果

{'x': 1, 'y': 2}

上記の例ではMyClassクラスのインスタンスであるobjの内容に関して出力しています。 コンストラクタによって定義されているインスタンス変数のxyがプロパティとして辞書型で格納されていることが確認できます。 また__dict__変数を使用して、既に存在する特定のプロパティを参照する場合は以下のようにします。

sample.py
〜省略〜

print(obj.__dict__["x"])

出力結果

1

特定のプロパティを参照する場合も、通常の辞書型と変わりありません。また、本来のオブジェクトに対してのプロパティ参照方法であるprint(obj.x)の記述と結果は変わりません。 注意点として、メソッドの内容は格納されません。また、プロパティが存在しないオブジェクトに対して__dict__変数を使用して参照しようとしてもエラーとなります。

プロパティの追加と変更

__dict__変数を使用したプロパティの追加方法については以下の通りです。

sample.py
class MyClass:
    def __init__(self):
        self.x = 1
        self.y = 2

obj = MyClass()
obj.__dict__["z"] = 3
print(obj.__dict__)

出力結果

{'x': 1, 'y': 2, 'z': 3}

このようにコンストラクタでは定義されていないzという変数を追加出来ている事が確認できます。また、こちらも obj.z = 3という記述でも結果は同じになります。

次に、既に存在するプロパティの変更については以下のとおりです。

sample.py
〜省略〜

obj = MyClass()
obj.__dict__["x"]= 3
print(obj.__dict__)

出力結果

{'x': 3, 'y': 2}

こちらも通常の辞書型と同じくインデックスにキーを指定し、そのバリューに対して値を代入(上書き)しています。 また、obj.x= 3という記述でも結果は同じになります。

プロパティの削除

__delattr__というクラスメソッドを使用することで、特定のプロパティを削除することが出来ます。

sample.py
class MyClass:
    def __init__(self):
        self.x = 1
        self.y = 2

obj = MyClass()
obj.__delattr__("x")
print(obj.__dict__)

出力結果

{'y': 2}

このように__delattr__()の引数に、削除対象のプロパティのキーを渡すことで削除することが出来ます。

以上が、__dict__を使用したオブジェクト情報の参照・操作となります。これらを使いこなすことで、複数のプロパティが存在するオブジェクトの情報を簡単に参照することができ、その内容を必要に応じて変更することが容易になります。

Lesson 8 Chapter 5
__annotations__

__annotations__はPython3.0から導入された機能で、関数の引数や返り値の型(アノテーション)に関する情報を参照し型チェックをする事ができます。 また、Pythonにおけるアノテーションとは"型ヒント"と呼ばれる機能を使用して定義します。こちらについては後ほど詳しく説明していきます。

アノテーションの宣言

アノテーションは関数やメソッドに記述します。以下は、引数aが整数型で, bが文字型であることを示す例です。

sample.py
def add(a: int, b: str):
    return a + int(b)

このようにアノテーションを使うことで、関数やメソッドがどのような型の引数を受け取り、返り値を返すのかが明確になります。
また、関数やメソッドの返り値にもアノテーションを宣言することが出来ます。

sample.py
def add(a: int, b: str) -> int:
    return a + int(b)

返り値の場合 -> [データ型]といった形で宣言します。 上記の例では返り値が整数型であることを示すintが宣言されています。
また、より詳細に関数の説明を記述するドキュメンテーションとしても使用することができます。以下は、関数の説明をアノテーションで記述する例です。

sample.py
def add(a: int, b: int) -> int:
    """
    2つの整数を足し合わせる関数です。
    :param a: 足し算の左辺の整数
    :param b: 足し算の右辺の整数
    :return: 足し算の結果
    """
    return a + b

アノテーションは実行時には影響を及ぼしません。しかし、アノテーションを使うことで、開発者がより正確に関数やメソッドを記述することができ、バグを減らすことができます。

Pythonにおける型ヒント

ここまで学習してきたアノテーションの宣言には、Pythonの型ヒントという構文が用いられています。ここでは型ヒントについてもう少し学んでいきましょう。

変数に対しての型ヒントの書き方は以下の通りです。

[変数]: [データ型] 

このように:(コロン)で変数とデータ型を記述することで使用することが出来ます。
また、データ型に使用できる代表的な型は以下の通りです。

データ型 説明
int 整数
float 浮動小数点数
bool 真偽値
str 文字列
list リスト
tuple タプル
dict 辞書
set 集合
None None型

それでは実際に__annotations__を使用して、これらのアノテーションを確認する方法について学んでいきましょう。

アノテーションの参照方法

__annotations__は関数(メソッド)に対して使用することが出来ます。以下の例ではsay_helloメソッドのアノテーションを参照しています。

sample.py
def my_hello(name: str, age: int) -> str:
    message = "私は" + name + "です。年齢は" + str(age) + "歳です。"
    return message

print(my_hello("太郎", 20))
print(my_hello.__annotations__)

出力結果

私は太郎です。年齢は20歳です。
{'name': <class 'str'>, 'age': <class 'int'>, 'return': <class 'str'>}

このように、アノテーション情報が辞書型で各属性に格納されていることが確認できます。

以上が__annotations__変数についての説明になります。アノテーションは、関数やメソッドがどのような型の引数を受け取り、どのような型の返り値を返すのかを明確にするための仕組みであり、ドキュメントとしても使用することができます。