※この記事は、Pythonのクラスの特殊性とスコープについて私が独自に検証・調査した記事ですが、私自身がPythonのスペシャリストと言える程の腕前の者ではないので、内容はあくまでご参考程度にお願い致します。ただ、指摘は歓迎します。
ちなみに、検証に用いたPythonのバージョンは3.9.1です。
発端
Python言語の創始者であるGuidoさんが、ある時、↓のツイートをしていました。
どういうことかと言うと、↓のコードを実行すると、結果として「0 1」が出力されます(3.9.1で実行)。
x = 0
y = 0
def f():
x = 1
y = 1
class C:
print(x, y) # What does this print?
x = 2
f()
仮に上から順に実行されるとして、素直に考えると、「1 1」が出力されそうに思います。ですが、そうではありません。
なぜでしょうね…?
この記事では、Pythonのスコープの特殊性に関して、調べたことをまとめています。
疑問点の整理
- インスタンス化されていないクラス(Cクラス)の中が、なぜ実行されているのか?
- なぜ、xとyで結果の値が異なるのか?
1は知ってる人は知っているのかもしれません。
実行順序については、
x = 0
y = 0
f( )
のはずですね。
Pythonのクラスの特性
オブジェクト指向(OOP)では、クラスの要素としては変数ないし関数(やプロパティ)が定義されることが普通だと思うので、定義の直下にprint
などと書くケースは不自然に思えます。処理を定義して使い回すなら、関数(上の例で言えばdef
)を使用するのが自然でしょう。
ただ、Pythonはクラス定義の直下に直接print
などの処理を記述できるようです(不思議なことに)。
例えば、↓のようなコードを実行してみます。
class C:
x = 'inside Class C'
print(x)
def F():
x = 'inside Function F'
print(x)
a = C()
print(a.x)
F()
結果は、こうなります。
inside Class C
inside Class C
inside Function F
class C
は定義時点で一度実行されていることが分かります。関数F()
は定義時点では実行されていません(呼び出した時に実行されている)。
ここから分かるクラスの特性として、
- クラスは定義段階で中の処理が実行される
- クラス定義直下に処理(C#で言うステートメント)を記述できる
ただ、こういう、複雑な書き方は普通しないと思います。Pythonのクラスはちょっと不思議ですね(何か理由があるのかもしれませんが…そこまでは調べていません)。(1)の回答としては、こんなところでしょうか。
クラスのスコープにまつわる不思議
もう1つの疑問は、「なぜ、xとyで結果の値が異なるのか?」でした。
念のため補足すると、関数の下にクラスを書き、そのクラスを関数の如く実行するという変(というか複雑)な作り方は、普通はしないと思います。コードが大規模かつ複雑になってくると、意図せず作り込んでしまう可能性はありそうですが。
ただ、Pythonのこの辺の特殊性を事前に把握しておくことで、意図せぬバグの作り込みを防げるかもしれません(あと、私の凝り性の性格もあるので調べた)。
ヒントになりそうな情報源
英語になりますが、stack overflowにあった以下の質問・回答が参考になるかもしれません。質問のコードも、今回↑で挙げたものと、ほぼ同様です。
- Python class scoping rules (stack overflow)
This behaviour has existed since Python 2.1 PEP 227: Nested Scopes, and was known back then. If a name is assigned to within a class body (like
Python class scoping rulesy
), then it is assumed to be a local/global variable; if it is not assigned to (x
), then it also can potentially point to a closure cell.
要するに、
- Python 2.1から存在する挙動
- クラス内部で変数の割当があるとローカルもしくはグローバルな変数と見なされる
- 割当がない場合はclosure cellを指し示すことがある(これは意味がよく分からん)
closure cellは、要するにクロージャのまとまりのことであると解釈してよいでしょう。クロージャについてはPython特有の概念ではないので、詳しくはググって頂くか、クロージャ(Wikipedia)をご覧下さい。要するに、関数の中に関数を書くという形で、参照の範囲を限定するものであるようです(実務で使用した経験はありませんが)。
もう1つ、Python公式サイトのドキュメントにヒントがありました。上の引用のリンクですが、PEP 227: 入れ子状のスコープです。PEPはPython Enhancement Proposaの略ですが、詳しくは公式サイトのWhat is a PEP?をご覧下さい。
Python 2.1の最も重要な変更点は、この問題を解決するために静的なスコープが追加されたことです。. . . (中略). . . 簡単に言えば、指定された引数名が関数内の値に割り当てられない場合(
PEP 227: 入れ子状のスコープdef
、class
またはimport
ステートメントの割り当てによって)、変数の参照は外側のスコープのローカル名前空間で検索されます。ルールや実装の詳細はPEPで参照できます。
静的スコープ(英語ではlexical scope, static scopeと呼ぶらしい)がバージョン2.1から導入され、こういう挙動になり(上で書いた通り)、変数の割当がない場合は外側のスコープで名前が検索されるらしいです。最初に↑で挙げた例で言えば、y
はclass C
で割当されていないのでdef f()
の値が使用される、ということになるようです。
バグではなく、こういう挙動(要するに仕様)である、ということですね。
ちなみに、PEP 227では、こういう説明も付け加えられています。問題を起こしうる書き方ですが、そもそも、そういうコーディングをする時点で問題があります。
この変更は、同じ変数名がモジュールレベルと関数の定義が含まれている関数内のローカルの両方で変数名として使用されているコードで、互換性の問題を引き起こす可能性があります。ですがむしろ気にしなくてよいでしょう。そのようなコードはそもそも最初から相当こんがらかっているので。
補足(内部関数の場合を試してみる)
クラスを関数に変えてみると、どうなるでしょうか。
関数の下のクラスを関数に変えてみましたが、これはエラーになります。
# 動かない例
x = 0
y = 0
def f():
x = 1
y = 1
def sub_f():
print(x, y)
x = 2
sub_f()
f()
こんなエラーメッセージが出る。
UnboundLocalError: local variable 'x' referenced before assignment
ただ、x = 2
を無くすと、クロージャとして機能してくれるようです。「1 1」が出力結果として、帰ってきます。
# 動く例
x = 0
y = 0
def f():
x = 1
y = 1
def sub_f():
print(x, y)
# x = 2
sub_f()
f()
Python 2.1は2001年4月に公開されたとのことですが、Python(というかCPython)って実は歴史的な積み重ねが色々とあるのですね。
静的スコープや動的スコープについては、また別の記事にまとめてみたいと思います(暇があれば)。
以上です。