定義前の変数、関数、クラスを
参照するときは
関数定義内で参照する。
# 1. たとえば
# 1. 定義前の変数の参照
# NG コード
未定義なので、エラーで返ってきます。
# 未定義の変数 a, b
print(a + b)
>>> # 未定義の変数 a, b
>>> print(a + b)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'a' is not defined
>>>
# OK コード
# 未定義の変数 a, b
def main(a, b):
print(a + b)
# a, b を定義する。
a = 1
b = 2
main(a, b)
>>> # 未定義の変数 a, b
>>> def main(a, b):
... print(a + b)
...
>>>
>>> # a, b を定義する。
>>> a = 1
>>> b = 2
>>>
>>> main(a, b)
3
>>>
Java, C から流れてきた人にとっては main 関数を頭にかけば プロトタイプ宣言のように使えると考えるとわかりやすいかもしれません。
ちなみに残念ながらプロトタイプ宣言は Python ではないようです。 でも、プロトタイプ宣言がなくても困りません。 何故なら、関数は定義時には実行されないからです。
# 2. 定義前の関数の参照
# NG コード
date は呼び出せません。何故なら、まだ定義していないから
# 未定義の関数 date
date()
>>> # 未定義の関数 date
>>> date()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'date' is not defined
>>>
# OK コード
def main():
# 未定義の関数 date
date()
def date():
print("Hello, world!")
main()
>>> def main():
... # 未定義の関数 date
... date()
...
>>>
>>> def date():
... print("Hello, world!")
...
>>>
>>> main()
Hello, world!
>>>
# 3. 定義前のクラスの参照
あるクラスに、定義前の別のクラスを属性として登録することを考えてみましょう。 例えば BoyFriend クラスに属性として GirlFriend クラスのオブジェクト girl_friend を登録してみましょう。
# NG コード
class BoyFriend:
# 未定義のガールフレンド(仮)クラス
# もれなく弾かれます。
girl_friend = GirlFriend()
>>> class BoyFriend:
... # 未定義のガールフレンド(仮)クラス
... # もれなく弾かれます。
... girl_friend = GirlFriend()
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in BoyFriend
NameError: name 'GirlFriend' is not defined
>>>
# OK コード
関数の定義内であれば、未定義の変数、関数、クラスも参照できることを利用して __init__ 関数の中で定義前のクラスを参照します。
__init__ 関数の中で属性を参照、変更する際には self が必要です。
class BoyFriend:
def __init__(self):
# 未定義の GirlFriend クラス
# => 未定義なので直接クラス内では参照できない。
# 関数の中で属性を参照する場合は self が必要
self.girl_friend = GirlFriend()
class GirldFriend:
# 定義済 の BoyFriend クラス
# => 定義済みなので直接クラス内で参照できる。
# 直接クラス定義の中で参照する場合は self は不要
boy_friend = BoyFriend()
# 成功!
boy_friend = BoyFriend()
>>> class BoyFriend:
... def __init__(self):
... # 未定義の GirlFriend クラス
... # => 未定義なので直接クラス内では参照できない。
...
... # 関数の中で属性を参照する場合は self が必要
... self.girl_friend = GirlFriend()
...
...
>>>
>>> class GirldFriend():
... # 定義済 の BoyFriend クラス
... # => 定義済みなので直接クラス内で参照できる。
...
... # 直接クラス定義の中で参照する場合は self は不要
... boy_friend = BoyFriend()
>>>
>>>
>>> # 成功!
>>> boy_friend = BoyFriend()
>>>
# 補足: 「インスタンス変数」と「ローカル変数」を区別する。
ちなみに __init__ 関数内で self を記載しなかった変数は、外部から参照できません。 なぜなら __init__ 関数の中だけで参照できる変数として定義されてしまっているからです。 このように関数の外側から見れない変数を「ローカル変数」と呼びます。
class GirlFriend:
def __init__(self, name, place_of_birth):
# OK: インスタンス変数
self.name = name
# NG: ローカル変数
place_of_birth = place_of_birth
girl_friend = GirlFriend('やるみ', '東京')
# OK
girl_friend.name
# NG
girl_friend.place_of_birth
>>> # OK
... girl_friend.name
'やるみ'
>>>
>>> # NG
... girl_friend.place_of_birth
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
AttributeError: 'GirlFriend' object has no attribute 'place_of_birth'
>>>
「ローカル変数」については、こちらで説明させていただきました。
# 補足: 「クラス変数」と「インスタンス変数」を区別する。
とりあえず、いまは "上のコードは、エラーにならない" ということだけを確認しておいてください。 実は GirlFriend クラスの設計は、間違っています。 正しい設計、「クラス変数」と「インスタンス変数」の違いについては、 最後にご説明させていただきます。
# 間違った設計
class BoyFriend:
def __init__(self):
self.girl_friend = GirlFriend()
class GirldFriend:
# クラス変数
boy_friend = BoyFriend()
# 正しい設計
class BoyFriend:
def __init__(self):
self.girl_friend = GirlFriend()
class GirldFriend:
def __init__(self):
# インスタンス変数
self.boy_friend = BoyFriend()
# 2. スクリプトとモジュール
スクリプトをモジュールとして再利用しやすくすることを考えます。
ツールとして利用したい時 python sample.py
と、
ライブラリとして利用したい時 import sample
の2種類があるかなと思います。
ここでは関数が、定義時に実行されないことを利用して、 自分が作ったスクリプト sample.py をライブラリ、モジュールとして再利用しやすくして見たいと思います。
# 4. 最初に実行する処理を頭に持って来る。
コードを追いかけやすくすること 、 即ちどこから読めばいいようにするかをわかりやすくすることは、 コードを理解しやすくすること と同じくらい可読性において重要です。
まずコードの可読性を最適化しよう - POSTD (opens new window)
ディベロッパがコードを読んでいる時、 作業を大幅に遅らせる2つの根本的な要因があります。
- コードが理解しづらい
- コードが追いかけづらい
そこで、関数は定義時には処理が実行されないことを利用して、 一番最初に実行する処理を関数にまとめて上の方に書くことが稀にあります。
そのとき、処理を頭に持ってくるために書く関数名は main と 記載されていることが多い気がします。別に決まりではありません。
なぜ main が多いかと言うと Java や C 言語だと main 関数は最初に実行される仕様になっていて、 それに習っているような感じです。
def main():
# 未定義の関数 date
date()
def date():
print("Hello, world!")
main()
この方がおっしゃられているのは、 おそらく C 言語の話だと思うのですが、 Python に置き換えても通じるところはあるかなと思ったりもします。
コード書くときに割と有用な注意点として、main関数内は基本的に関数呼び出しに抑えて、実行させたい処理は関数でまとめておくといい
— コミさん (@komi_edtr_1230) January 28, 2019
再利用性が高まるし可読性も上がるのでコードレビューがしやすいし、リファクタリングするのにも手がかからないので、コードを書く際は気をつけるといいと思う
# 5. import 時に処理を実行させない
作ったスクリプトの関数をインタラクティブシェルで、 ちょっと触って動作確認したい時や、 あるいはツールとしてスクリプトを利用したい時があります。
# 1. こんなスクリプトを
# ファイル名は sample.py
def main():
# 未定義の関数 date
date()
def date():
print("Hello, world!")
main()
しかし import すると同時にスクリプト全体が実行されてしまいます。 import と同時に main() が実行され "Hello, world!" が出力されます。
$ # 2. インタラクティブシェルで
$ python3
>>> # 3. ちょっと触って動作確認したい
>>> import sample
Hello, world!
>>>
>>> sample.date()
Hello, world!
>>>
本当はこんな感じになってほしい。
>>> # import しても何も実行されない。
>>> import sample
>>>
>>> sample.date()
Hello, world!
>>>
# 6. if __name__ == '__main__':
このようにするには main 関数に加えて
if __name__ == '__main__':
を合わせて書きます。
def main():
# 未定義の関数 date
date()
def date():
print("Hello, world!")
if __name__ == '__main__':
main()
このコードを追加することで、 このファイルが import できる モジュール であると同時に スクリプト としても使えるようになります。
6.1.1. モジュールをスクリプトとして実行する (opens new window)
# 7. スクリプトとして実行
「スクリプト」として実行すると、__name__ には '__main__' が代入されます。 if 文内が実行されます。
$ # 1. スクリプトとして実行
$ python3 sample.py
Hello, world!
$
Python モジュールを
$ python3 fibo.py arguments
と実行すると、__name__ に __main__ が設定されている点を除いて import したときと同じようにモジュール内のコードが実行されます。
この書き方も決まりではありませんが、たまに見かけます。 Python に標準で付いてくる timeit という実行時間を計測するモジュールでも、 この書き方がなされているのを見たことがあります。
# 8. モジュールとして実行
「モジュール」として import されると、__name__ にはモジュールの名前が代入されます。 簡単に言えば、例えばファイル名が sample.py だったら 'sample' が代入されます。if 文内は実行されません。
$ # 2. モジュールとして実行
$ python3
>>>
>>> import sample
>>>
>>> # モジュール内の関数を利用できる。
>>> sample.date()
Hello, world!
>>>
モジュールの中では、(文字列の) モジュール名をグローバル変数 __name__ で取得できます。
6. モジュール (module) - Python チュートリアル (opens new window)
# 9. 「スクリプト」と「モジュール」を区別する。
「スクリプト」と「モジュール」を区別することは、 import 文でエラーにぶつかった時に、重要になります。
なぜなら python sample.py
で「スクリプト」として実行した時と
import python
で「モジュール」として import した時で挙動が違うからです。
なにが違うかというと「パス」と「パッケージの構成」が変わってきます。 「パッケージ」と言うのは、「モジュール」をディレクトリで1つにまとめたものです。
いま見ていただく必要は全くないのですが、 以下の資料にまとめました。この辺で、かなりハマりました。
import 文から例外を投げ返された時は、「パス」と「パッケージの構成」と言うのを 心の片隅に置いておいていただけると、対応が速いかなと感じます。
# 3. クラス変数とインスタンス変数
クラス変数とインスタンス変数の違いについて見ていきます。
# 疑問: なんで関数では未定義の変数等を参照できたのにクラスでは未定義の変数等を参照できなかったの?
答え: クラスを定義した段階で、処理が実行されてしまうから
まず、以下の例について考えてみます。 なんと "Hello, world!" が出力されています。 クラスが定義された段階で処理が実行されていることがわかります。
class BoyFriend:
print("Hello, world!")
girl_friend = GirlFriend()
>>> class BoyFriend:
... print("Hello, world!")
... girl_friend = GirlFriend()
...
Hello, world!
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in BoyFriend2
NameError: name 'GirlFriend2' is not defined
>>>
次に関数を見てみます。処理が実行されません。
def date():
print('Hello, world!')
girl_friend = GirlFriend()
>>> def date():
... print('Hello, world!')
... girl_friend = GirlFriend()
...
>>>
クラスは、単純にモジュールを分割したものです。簡単に言えば、クラスはモジュールの子供みたいなものです。
クラスオブジェクトは、基本的にはクラス定義で作成された名前空間の内容をくるむラッパ (wrapper) です。
9.3.1. クラス定義の構文 - Python チュートリアル (opens new window)
クラスには、モジュールよりも2つの機能が付け加えられています。 1つ目は、クラスは、インスタンス化してオブジェクトを生成できること。 2つ目は、クラスは、他のクラスを継承できること。
クラスが、モジュールを分割しただけでシンプルに表現できているなんて、すごい設計ですよね。 ワイだったらもっと複雑怪奇なものしか思いつかない.. これを見た時は、感動しました。
# 疑問: じゃあ __init__ 関数の中で全部属性の代入してしまえば、いいんじゃないの?
答え: ダメです。
インスタンスオブジェクト間で
共有
される変数ならば...
→
クラス変数
に代入します。
→ それをするには、クラス定義文の中で属性に代入します。
インスタンスオブジェクトに
固有
の変数ならば...
→
インスタンス変数
に代入します。
→ それをするには、__init__ 関数の中で属性に代入します。
# 具体的にどういうことだってばよ...
恋愛ゲームについて考えましょう。 彼女 GirlFriend には名前 name, 彼氏への親密度 intimacy があり最小値 0, 最大値 100 とします。これでクラス設計をすると
# NG
class GirlFriend:
max_intimacy = 100
min_intimacy = 0
intimacy = 0
name = None
boy_friend = None
実は、これ間違っています。 何故なら、名前 name と親密性 intimacy がすべての GirlFriend インスタンスオブジェクトで共有される クラスオブジェクト変数 として定義されているからです。
各インスタンスオブジェクトで共有しない属性、名前 name と親密性 intimacy は、__init__ 関数の中で定義して インスタンス変数 に代入します。
# OK
class GirlFriend:
# インスタンスオブジェクト間で共有される変数
max_intimacy = 100
min_intimacy = 0
# インスタンスオブジェクト間で共有しない変数
def __init__(self, new_name=''):
self.name = new_name
self.intimacy = 0
self.boy_friend = None
# 疑問: クラス変数とインスタンス変数って、何か違いがあるの?
答え: あります。
以下の記事でご説明させていただきました。