この記事は、ニフティグループ Advent Calendar 2023 22日目の記事です。
こんにちは。ニフティ会員システムグループの増井です。
みなさん業務や趣味でPythonを使っていますか。
高い汎用性とシンプルな文法で知られるPythonは、データ分析や機械学習のプロジェクトから、Web開発、さらには日々の作業を自動化するスクリプトまで、幅広い分野で活躍しています。
私の所属するニフティ契約者向けオプションサービスを開発するチームではサーバーサイドロジックを記述する言語としてPythonを採用しています。
今回のブログはそんなPythonの超基礎的な話です。
動機
先日、他の人が書いたPythonのテストコードを書いていてこんなことがありました。
1 2 3 4 5 6 |
with self.assertLogs(logger, level='WARNING') as log: response = obj.request(HogeHogeClass(param=some_value)) # responseの確認 self.assertIsInstance(response, HogeHogeResponse) self.assertEqual(response.fugafuga, 0) |
自分「なんでresponseがwithブロックの外で参照できてるの?」
これ、ちゃんとPythonの仕様を理解している人からしたら至極当たり前のことかもしれないのですが、withブロックの中で定義した変数はwithブロックを抜けたあとも参照できます。
しかし自分はwithのコードブロックはスコープを形成するものだと思い込んでいたので、このコードを読んで非常に違和感を感じたのです。
また、ifやfor, while, tryブロックについても自分の直感に反してスコープを形成しないということに気づきました。
そこで自分への戒めを込めてPythonのスコープについてという初歩的な記事を書こうと思ったのでした。よく理解している人は笑いながらお読みください。
主な対象者
- 初学者
- 他の言語を書ける状態からPythonを速習で学んだのでちゃんと仕様を理解できている自信がない人
- Pythonを書いているが他の言語も並行して書いているのでよく言語間の仕様がごっちゃになる人
スコープとは
基礎的な話なのでまず変数のスコープについての説明も書いておきます。
スコープとはその変数が参照可能な範囲のことです。
アセンブリのような低レイヤーな言語を除いて大半の高級言語に変数スコープはあると思います。
例えばJavaだとif文などで {} で囲われた中で宣言された変数は {} 内のみで有効で、{} を脱出すると寿命が尽きてその変数は参照できなくなります。
なぜ今まで気づかなかったか
自分が冒頭で書いたようなPythonの仕様に今まで気づかなかった大きな理由は、
- これまではコードを1人で書くことが多かった
自分は数ヶ月前に中途で入社した者で、前職では社内に手を動かしてコードを書ける人があまりいなかったため自分一人でコーディングすることが多かったです。
先ほどのようなコードを一人で記述するならいつもこう書いていました。
1 2 3 4 5 6 7 |
response = None with self.assertLogs(logger, level='WARNING') as log: response = obj.request(HogeHogeClass(param=some_value)) # responseの確認 self.assertIsInstance(response, HogeHogeResponse) self.assertEqual(response.fugafuga, 0) |
with句の中で値を入れる変数をwithの外で予め宣言しておき、ブロックスコープ(と思い込んでいる)の外でも確実に参照できるようにします。
実際にこれでも問題なく動くのでずっと気づくことができなかったのです。
また、最初に学習したときにwithを抜けた後にwithの中の変数を参照しているようなサンプルコードを見たことはあるが使われている変数はwithの前のどこかで宣言されているもの、と暗黙的な前提を置いてコードを読んでいた可能性もあるでしょう。
Pythonのスコープ
まず改めてPythonについて調べていて個人的に少し衝撃的だったのは
- Pythonにブロックスコープは存在しない
ということです。ブロックスコープとは変数が定義されたブロックの中でのみアクセス可能で、外側からアクセスできない範囲のことを指します。
ブロックとはPythonではインデントされたコードの塊、Python以外の多くの言語の場合はコードのまとまりを中括弧 {} で表すものが多いと思いますがその場合は {} で囲まれた部分の塊です。
Pythonにあるスコープは以下の4種類です。
- ローカルスコープ
関数内で定義された変数。関数が呼ばれると新しいスコープが作成され、関数が終了するとそのスコープも終了する。
1 2 3 4 5 |
def function() -> None: a = "nifty" function() print(a) # エラー |
- エンクロージングスコープ
ネストされた関数(内部関数)の外側にある関数内でスコープが作成される。内部関数が外側の関数の変数を参照できるが、内部関数からは外側の関数のローカル変数を変更することはできない。
1 2 3 4 5 6 7 8 9 10 11 12 |
def outer_func() -> None a = "nifty" def inner_func_read() -> None print(a) # nifty def inner_func_write() -> None a = "another_isp" # エラーにはならないがaはouter_func()直下のaとは別物として扱われる print(a) # another_isp inner_func_read() inner_func_write() |
通常は関数をこのようにネストして書くことは少なく、主にlambda式を記述するときに意識する。
- グローバルスコープ
モジュールレベルで定義された変数。プログラム全体からアクセス可能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
a = "nifty" # グローバル変数 def hoge() -> None: print(a) # nifty ←グローバルに宣言された変数を参照できる a = "another_isp" # 同名の変数が初期化されるとhoge()内のローカル変数扱いとなりグローバルなaとは別物になる hoge() print(a) # nifty def fuga() -> None: global a # global文でグローバルに宣言されたaを使うことを明示する a = "another_isp" # グローバル変数aの値が変わる fuga() print(a) # another_isp |
- ビルトインスコープ
Pythonの組み込み関数や属性にアクセスするスコープ。
グローバルスコープよりもさらに広い。
以下のコードではmain.pyでhogeモジュールをインポートしてグローバル変数を参照しています。
hoge.py
1 |
a = "nifty" #グローバル変数 |
main.py
1 2 3 |
import hoge print(hoge.a) # nifty |
しかし、モジュール名を指定しないと参照できません。
main.py
1 2 3 |
import hoge print(a) # モジュールを指定しないとエラー |
そこでhoge.pyでbuiltinsをimportしてビルトインスコープに加えるとimportさえしていればモジュール名を指定しなくても参照できるようになります。
hoge.py
1 2 3 |
import builtins builtins.a = "nifty" |
main.py
1 2 3 |
import hoge print(a) # nifty |
クラスとカプセル化について
もう一つ、スコープに絡めてクラスを作る上で陥りがちな罠を紹介しておきます。
主に他の言語でオブジェクト指向のものを書いたことがある人向けの内容になります。
- Pythonでclass直下に宣言されている変数はクラス変数
これはオブジェクト指向言語をメインで書いている人が陥りがちな罠だと思いますが、まず以下のコードを見てください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class TestClass: _a = "nifty" def set_a(self, value: str) -> None: self._a = value def get_a(self) -> str: return self._a instance = TestClass() instance.set_a("another_isp") print(instance.get_a()) print(instance._a) print(TestClass._a) |
出力はどうなると思いますか。
まず print(instance.get_a()) の部分は another_isp が出力されることが分かると思います。
では print(instance._a) の部分はどうなるでしょうか?
Pythonではクラス内でプライベートな使い方を意図した変数には「_」を付ける慣習があるのでそれを考慮するとエラーが出力されそうですが実は
- 「_」を付けるのはあくまで慣習に過ぎず、言語の機能的に変数をプライベート(参照可能範囲をクラス内に限定)にすることはできない
のです。
よってエラーにはならずちゃんと文字列が出力されます。
1 |
another_isp |
print(TestClass._a) はどうでしょうか。 _a はインスタンスに属する変数なのでエラーでしょうか。
これもちゃんと出力されます。
1 |
nifty |
「あれ、_a
は”another_isp”で上書きしたはずでは…」とも思うかもしれません(数週間前の自分なら思っていました)
- Pythonではクラス直下のフィールドに定義した変数はクラス変数
Javaのようなメジャーなオブジェクト指向言語では
static キーワードなどを付けることによって初めてクラス変数になりますが、Pythonでは変数に対して明示的に static
を付けることがありません(メソッドに対しては
@staticmethod がありますが)。
そういった事実を知らずにオブジェクト指向脳で上記のコードを読むと TestClass のインスタンス変数 _a
に
set_a() で値をセットしているように見えますが
- クラス変数 _a (”nifty”で初期化されている)
- インスタンス変数 _a ( set_a()をした時点で初めてインスタンスに変数が追加されている)
の2つの変数が別々に存在している状態になっています。
ネームマングリング
クラスを定義してオブジェクト指向的な書き方をしているのに変数をプライベートに出来ないのは不便です。そこでPythonではネームマングリングと言って変数の名前を自動で書き換えることで参照しづらくする(しかし厳密にプライベートにはできない)機構が備わっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class TestClass: __a = "nifty" def set_a(self, value: str) -> None: self.__a = value def get_a(self) -> str: return self.__a instance = TestClass() instance.set_a("another_isp") print(instance.get_a()) # another_isp print(instance.__a) # エラー print(TestClass.__a) # エラー print(instance._TestClass__a) # another_isp print(TestClass._TestClass__a) # nifty |
変数名の先頭に「__(アンダースコア2つ)」を付けて「__name」などとすることで「_(クラス名)__name」という変数に内部的に置き換えることができます。
普段からプライベート変数を定義するとき当たり前にこうしている人にとっては「未だにアンダースコア1つでプライベートの意味を持たせてたの!」と思われてしまいそうですが。
ちなみに上記のコードでクラス変数とインスタンス変数がそれぞれ別個に作られるのは先ほどと同様です。
まとめ
以上、Pythonのスコープやクラス変数で陥りがちな罠についてでした。
冒頭で述べたようにちゃんと理解している人にとっては笑ってしまうような内容だったかもしれませんが、言語の仕様は今一度しっかりと確認してから使いたいですね。
今後新しい言語を学習するときも既存の言語の仕様から思い込みで判断していないか気をつけながら学んで行きたいところです。
ニフティでは私のチーム以外にもPythonを使った開発をしているチームが数多くあります。Pythonを使ったWebサービスの開発などに興味がある方はぜひ下のリンクからお気軽にご連絡ください!