Blog

初心に戻って、Pythonのスコープとクラスの罠について

この記事は、ニフティグループ Advent Calendar 2023 22日目の記事です。

こんにちは。ニフティ会員システムグループの増井です。

みなさん業務や趣味でPythonを使っていますか。

高い汎用性とシンプルな文法で知られるPythonは、データ分析や機械学習のプロジェクトから、Web開発、さらには日々の作業を自動化するスクリプトまで、幅広い分野で活躍しています。

私の所属するニフティ契約者向けオプションサービスを開発するチームではサーバーサイドロジックを記述する言語としてPythonを採用しています。

今回のブログはそんなPythonの超基礎的な話です。

動機

先日、他の人が書いたPythonのテストコードを書いていてこんなことがありました。

自分「なんでresponseがwithブロックの外で参照できてるの?」

これ、ちゃんとPythonの仕様を理解している人からしたら至極当たり前のことかもしれないのですが、withブロックの中で定義した変数はwithブロックを抜けたあとも参照できます。

しかし自分はwithのコードブロックはスコープを形成するものだと思い込んでいたので、このコードを読んで非常に違和感を感じたのです。

また、ifやfor, while, tryブロックについても自分の直感に反してスコープを形成しないということに気づきました。

そこで自分への戒めを込めてPythonのスコープについてという初歩的な記事を書こうと思ったのでした。よく理解している人は笑いながらお読みください。

主な対象者

  • 初学者
  • 他の言語を書ける状態からPythonを速習で学んだのでちゃんと仕様を理解できている自信がない人
  • Pythonを書いているが他の言語も並行して書いているのでよく言語間の仕様がごっちゃになる人

スコープとは

基礎的な話なのでまず変数のスコープについての説明も書いておきます。

スコープとはその変数が参照可能な範囲のことです。

アセンブリのような低レイヤーな言語を除いて大半の高級言語に変数スコープはあると思います。

例えばJavaだとif文などで {} で囲われた中で宣言された変数は {} 内のみで有効で、{} を脱出すると寿命が尽きてその変数は参照できなくなります。

なぜ今まで気づかなかったか

自分が冒頭で書いたようなPythonの仕様に今まで気づかなかった大きな理由は、

  • これまではコードを1人で書くことが多かった

自分は数ヶ月前に中途で入社した者で、前職では社内に手を動かしてコードを書ける人があまりいなかったため自分一人でコーディングすることが多かったです。

先ほどのようなコードを一人で記述するならいつもこう書いていました。

with句の中で値を入れる変数をwithの外で予め宣言しておき、ブロックスコープ(と思い込んでいる)の外でも確実に参照できるようにします。

実際にこれでも問題なく動くのでずっと気づくことができなかったのです。

また、最初に学習したときにwithを抜けた後にwithの中の変数を参照しているようなサンプルコードを見たことはあるが使われている変数はwithの前のどこかで宣言されているもの、と暗黙的な前提を置いてコードを読んでいた可能性もあるでしょう。

Pythonのスコープ

まず改めてPythonについて調べていて個人的に少し衝撃的だったのは

  • Pythonにブロックスコープは存在しない

ということです。ブロックスコープとは変数が定義されたブロックの中でのみアクセス可能で、外側からアクセスできない範囲のことを指します。

ブロックとはPythonではインデントされたコードの塊、Python以外の多くの言語の場合はコードのまとまりを中括弧 {} で表すものが多いと思いますがその場合は {} で囲まれた部分の塊です。

Pythonにあるスコープは以下の4種類です。

  • ローカルスコープ

関数内で定義された変数。関数が呼ばれると新しいスコープが作成され、関数が終了するとそのスコープも終了する。

  • エンクロージングスコープ

ネストされた関数(内部関数)の外側にある関数内でスコープが作成される。内部関数が外側の関数の変数を参照できるが、内部関数からは外側の関数のローカル変数を変更することはできない。

通常は関数をこのようにネストして書くことは少なく、主にlambda式を記述するときに意識する。

  • グローバルスコープ

モジュールレベルで定義された変数。プログラム全体からアクセス可能。

  • ビルトインスコープ

Pythonの組み込み関数や属性にアクセスするスコープ。

グローバルスコープよりもさらに広い。

以下のコードではmain.pyでhogeモジュールをインポートしてグローバル変数を参照しています。

hoge.py

main.py

しかし、モジュール名を指定しないと参照できません。

main.py

そこでhoge.pyでbuiltinsをimportしてビルトインスコープに加えるとimportさえしていればモジュール名を指定しなくても参照できるようになります。

hoge.py

main.py

クラスとカプセル化について

もう一つ、スコープに絡めてクラスを作る上で陥りがちな罠を紹介しておきます。

主に他の言語でオブジェクト指向のものを書いたことがある人向けの内容になります。

  • Pythonでclass直下に宣言されている変数はクラス変数

これはオブジェクト指向言語をメインで書いている人が陥りがちな罠だと思いますが、まず以下のコードを見てください。

出力はどうなると思いますか。

まず print(instance.get_a()) の部分は another_isp が出力されることが分かると思います。

では print(instance._a) の部分はどうなるでしょうか?

Pythonではクラス内でプライベートな使い方を意図した変数には「_」を付ける慣習があるのでそれを考慮するとエラーが出力されそうですが実は

  • 「_」を付けるのはあくまで慣習に過ぎず、言語の機能的に変数をプライベート(参照可能範囲をクラス内に限定)にすることはできない

のです。

よってエラーにはならずちゃんと文字列が出力されます。

print(TestClass._a) はどうでしょうか。 _a はインスタンスに属する変数なのでエラーでしょうか。

これもちゃんと出力されます。

「あれ、_a は”another_isp”で上書きしたはずでは…」とも思うかもしれません(数週間前の自分なら思っていました)

  • Pythonではクラス直下のフィールドに定義した変数はクラス変数

Javaのようなメジャーなオブジェクト指向言語では static キーワードなどを付けることによって初めてクラス変数になりますが、Pythonでは変数に対して明示的に static を付けることがありません(メソッドに対しては @staticmethod がありますが)。

そういった事実を知らずにオブジェクト指向脳で上記のコードを読むと TestClass のインスタンス変数 _a set_a() で値をセットしているように見えますが

  • クラス変数 _a (”nifty”で初期化されている)
  • インスタンス変数 _a ( set_a()をした時点で初めてインスタンスに変数が追加されている)

の2つの変数が別々に存在している状態になっています。

ネームマングリング

クラスを定義してオブジェクト指向的な書き方をしているのに変数をプライベートに出来ないのは不便です。そこでPythonではネームマングリングと言って変数の名前を自動で書き換えることで参照しづらくする(しかし厳密にプライベートにはできない)機構が備わっています。

変数名の先頭に「__(アンダースコア2つ)」を付けて「__name」などとすることで「_(クラス名)__name」という変数に内部的に置き換えることができます。

普段からプライベート変数を定義するとき当たり前にこうしている人にとっては「未だにアンダースコア1つでプライベートの意味を持たせてたの!」と思われてしまいそうですが。

ちなみに上記のコードでクラス変数とインスタンス変数がそれぞれ別個に作られるのは先ほどと同様です。

まとめ

以上、Pythonのスコープやクラス変数で陥りがちな罠についてでした。

冒頭で述べたようにちゃんと理解している人にとっては笑ってしまうような内容だったかもしれませんが、言語の仕様は今一度しっかりと確認してから使いたいですね。

今後新しい言語を学習するときも既存の言語の仕様から思い込みで判断していないか気をつけながら学んで行きたいところです。

ニフティでは私のチーム以外にもPythonを使った開発をしているチームが数多くあります。Pythonを使ったWebサービスの開発などに興味がある方はぜひ下のリンクからお気軽にご連絡ください!

ニフティでは、
さまざまなプロダクトへ挑戦する
エンジニアを絶賛募集中です!
ご興味のある方は以下の採用サイトより
お気軽にご連絡ください!

ニフティに興味をお持ちの方は
キャリア登録をぜひお願いいたします!

connpassでニフティグループに
参加いただくと
イベントの
お知らせが届きます!