Python の名前空間とスコープ

shomah4a
2011/11/21 11:30

プログラムのロジックを考え、実装を行う上で、変数の名前空間やスコープはとても重要です。 これらはロジックを組み立てる上での複雑さに直結し、ソースコードの読みやすさにダイレクトに関係してくるためです。 この記事では、私が Python で開発をする上で気をつけるようにしている名前空間やスコープに関するお話をします。

対象環境

この記事では、 Python 2.7.2 でソースコードを実行して確認しています。

コーディングスタイルについて

名前空間やスコープの前に、まずは基本的なコーディングスタイルについて軽くお話しします。

Python のコーディングスタイルというと、 PEP 8 – Style Guide for Python Code (日本語訳は こちら )が有名です。 これは、 Python でプログラムを書く上で守っておくとよいお作法について書かれており、 Python のコーディングスタイルとしてはデファクトスタンダードといえるでしょう。

この PEP8、例えば以下のようなことが書かれています。

  • インデントは 4 スペースで
  • タブとスペースを混ぜて使わない

といったような Python らしいインデントの話から

  • 演算子の前後にスペースを入れる
  • 代入演算子を縦にそろえるためスペースを入れることはしない
  • モジュール・クラス・変数・関数などの名規則
  • 推奨される例外処理方法

などの書き方に関することまで広範に及びます。

Python で開発をするのであれば一読しておくことをオススメします。 また、このスタイルを守ると多くの Pythonista にとって読みやすいソースコードになるのではないでしょうか。

Python におけるスコープ

名前空間とスコープは切っても切れない関係です。 変数の名前空間という意味では、スコープにそれぞれの名前空間が存在するからです。

スコープに関して正しく理解をすることで、「どこで定義された変数がどこから見えるのか」ということがわかるようになり、ソースコードを読む際に役立ちます。

Python では、スコープは大まかに言って以下の二種類しかありません。

  • モジュールのグローバルスコープ
  • 関数のローカルスコープ

クラスを定義する際にもスコープが存在すると言えますが、若干性質が違うため割愛します。

他の言語ではこれだけでなく、さらに細かく分けられている場合があります。

C++ でのスコープ

例えば C++ では、波括弧で囲まれたブロック単位でスコープを作ることができます。 以下のように if 文のブロック内という局所的なスコープが存在します。

const int y = 20;

if (condition)
{
    // if 文のブロック内のスコープ
    const int x = 10;
}

std::cout << x + y << std::endl; // x が見つからず、エラーになる

同様に for, while, do, try 等でもスコープが導入されますし、以下のように波括弧を付けただけであっても局所的なスコープが作られます。

const int y = 20;

{
    const int x = 30;
}

std::cout << x + y << std::endl; // x が見つからないためにエラー

Python のスコープ

Python においては、モジュールのスコープと関数のスコープしかないため、以下のようなソースは実行できます。

def test():

    if condition:
        x = 20
    else:
        x = 30

    print x

if のようにブロックを伴う文の中であっても外側と同じスコープであるため、 if のブロック内で初めて出てきた変数名であっても、 if 文が終わった後であっても使えます。 for, while, try 等のその他の制御構文であっても同様です。

スコープを利用した書き方

このように、 ブロックの中で変数を定義しても、ブロックを抜けた後に参照できるため、以下のようなイディオムが存在します。

try:
    import cPickle as pickle
except ImportError:
    import pickle

これは、 cPickle モジュールが使えない環境で pickle モジュールを代わりに使うという書き方です。この例では try でインポートエラーが発生するかどうかを見ていますが、例えば if 文で Windows 環境, Linux 環境, MacOS X の環境であることを調べ、それぞれの環境で別のモジュールを読み込んだり、環境に合わせた処理を行うといったことができます。環境間の差異を吸収するようなコードを書く場合にはよく使います。

このようなブロックの内外で同一スコープであることを利用した書き方は、他の言語から移ってきた人にとっては違和感を覚えるものかもしれません。

スコープ中の名前空間

Python においては、スコープの範囲が関数単位と大きく、中に含まれる変数の数が増えがちです。 変数の数が増えるということは、一つの関数の中で把握するべき状態の組み合わせが増えるということですので、なるべく細かな単位で関数に分け、それらを呼び出すようにした方がいいでしょう。 また、そうした細かな関数を多く定義する方がユニットテストを書く上でも楽になります。

私の場合は、細かくするときは一つの分岐する処理 for, if ごとに関数で分けることもあり、関数が増えがちです。 あまりに細かな関数が増えてしまうような場合は、次章で紹介する関数中関数を定義することによって、外にその関数を見せないようにするという手も使えます。

関数の中の関数

Python では、関数の中で関数を定義できます。

def outer(x):

    def inner(y):
        return x + y # 外側の環境の x と y を足す

    return inner # outer 関数の中で定義した inner 関数を返す

result = outer(10)

print result(20) #=> 30

outer 関数の中で定義した inner 関数では、 inner 関数のローカルスコープだけでなく、外側にある outer 関数のスコープも参照できます。 また、 outer 関数を呼ぶと inner 関数を生成して返し、 outer 関数を抜けてしまいますが、 return 文で返された inner 関数では生成されたときの outer 関数実行時の環境を持っており、 inner 関数ではその保持された環境を参照します。

このような仕組みは「クロージャ」や「レキシカルスコープ」と呼ばれ、関数型言語などでよく見られます。

このような関数は、関数の中で局所的に使うような map, filter, reduce 等の高階関数に渡すための関数を定義する場合によく使います。

def list_mul(items):

    def mul(x):
        return x*2

    return map(mul, items)

print list_mul(range(5)) #=> [0, 2, 4, 6, 8]

この程度であれば mul 関数を定義せずに、 lambda x: x*2 と無名関数を使うこともできますが、煩雑な処理が増えてるような場合は関数として定義します。

このように、ネストした関数を定義しておくと、関数のスコープに限定した名前空間ができ、その内部のみで完結するようになるため、ネストの外側のスコープと切り離して考えることができます。

ネストした関数の使いどころ

ネストした関数は、上記のような用法の他に主にデコレータの定義などで使われることが多いですが、私の場合は GUI アプリケーション開発などで使っています。

例えば、以下のような PySide のソースコードがあるとします。

#-*- coding:utf-8 -*-

import sys
from PySide import QtGui as gui, QtCore as core

app = gui.QApplication(sys.argv)


window = gui.QFrame(None)

layout = gui.QVBoxLayout(None)

b1 = gui.QPushButton('add')
b2 = gui.QPushButton('remove')
listw = gui.QListWidget()
text = gui.QLineEdit()

layout.addWidget(listw)
layout.addWidget(text)
layout.addWidget(b1)
layout.addWidget(b2)

window.setLayout(layout)

window.show()

app.exec_()

これを実行すると以下のようなリストボックス, テキストボックス, ボタン二つを持つウィンドウが表示されます。

guisample4.png

サンプルのスクリーンショット

ここで、「add ボタンをクリックしたらリストにアイテムを追加」、「remove ボタンをクリックしたら選択したアイテムを削除」という動作を付けようと思います。

これを実装する際はクリック時のイベントを定義して、それぞれボタンに適用するという処理を書く必要があります。 これを、ただ関数を定義するのではなく、関数とその中で定義した関数で書くと以下のようになります。

#-*- coding:utf-8 -*-
def create_event(itemlist, text, add, remove):
    u'''
    リストコントロール、テキストボックス、ボタン二つにイベントを追加する
    '''

    def add_clicked():
        u'''
        追加ボタンがクリックされたら入力されたテキストをリストに追加する
        '''

        value = text.text()
        itemlist.addItem(value)


    def remove_clicked():
        u'''
        削除ボタンがクリックされたら選択されたアイテムを削除する
        '''

        index = itemlist.currentRow()

        if index > 0:
            itemlist.takeItem(index)

    # イベントハンドラを追加
    add.clicked.connect(add_clicked)
    remove.clicked.connect(remove_clicked)

そして、以下のように使います。

create_event(listw, text, b1, b2)

このように、ただ関数を二つ定義するのではなく、「4つのコントロールに対するイベントハンドラ生成関数」として定義してあげると、これらのコントロールのセットが増えた際にも同じ関数を呼び出すだけでイベントが適用できるため、手間が省けます。 処理を生成する関数を呼び出す、と言った感覚でしょうか。

このように、イベントハンドラの定義と適用を生成関数に閉じこめることで、再利用性の高いコードになります。 また、ロジックと関連するデータが小さな範囲にまとめられるため、見るべき変数が減って、わかりやすくなります。

ただし、このような関数の中で行う処理が増えてきたり、一つの関数が長くなったりするとソースコード上であっちこっちに処理が飛んでしまい、デバッグ時の見通しは悪くなってしまうので注意が必要です。 あまりに長くなるような生成関数を使う場合は、素直にクラス化してまとめた方がよいでしょう。

スコープまとめ

私がプログラムを書く上でスコープを扱う際に気をつけているのは、「スコープを小さく保ち、把握するべき状態数を減らす」ということです。 作るプログラムの規模が大きくなってくればなってくるほどに、このような細かな気配りが効いてくるため、スコープについて意識することはとても重要です。

モジュールインポート

モジュールのインポートというものは、対象のスコープの名前空間に対して変数を導入するという構文であると捉えることができます。 ここでは、私なりのモジュールのインポートスタイルについて語ってみます。

PEP8 でのインポートスタイル

PEP8 では以下のようなインポートが推奨されています。

import os
import sys

from subprocess import Popen, PIPE

また、以下のようなインポートスタイルは良くないものであるとされています。

import os, sys

また、モジュールのインポート順序として

  • 標準モジュール
  • サードパーティモジュール
  • 作成しているライブラリやアプリケーションのモジュール

というものが推奨されています。

また、それぞれの種類のインポート文はまとめて記述するような書き方が推奨されます。

以上を踏まえると以下のような書き方を行うことになるでしょう。

# 標準モジュール
import sys
import os

# サードパーティモジュール
from lxml import etree, html
from zope.pagetemplate import pagetemplate

# アプリケーション or ライブラリ固有のモジュール
from myapp.mymod import somefunc

自分流インポートスタイル

私はインポートの方針は基本的に PEP8 の方針に従っているのですが、若干違う部分もあります。

モジュール内一括インポート

まず、私の中では絶対にやってはいけないパターンというものがあります。

from module import *

このように、特定のモジュールから一括でインポートするようなことはまずありません。PEP8 ではこのパターンを使用する側への言及はありませんが、無節操にインポートしてモジュールの名前空間を汚してしまうため、あまり推奨されるものではないと思います。 何をインポートするかを明示しないという点で The Zen of Python の一文にある “explicit is better than implicit” にも反しているのではないでしょうか。

また、このようにインポートすると、ソースコード中で出てくる変数・関数・クラスがいったいどのモジュールから来たものなのか、もしくはモジュールレベルで宣言されているものなのかがわかりにくくなります。特に import * が複数あるような場合は凶悪度が格段に上がります。

このようなインポート文は意外なことにライブラリのサンプルコードに書いてあることがあったりして、若干残念な気持ちになります( PySideチュートリアル とか...。複数モジュールから import * する例)。

このようなチュートリアルなどではどのモジュールから読み込んだものなのかがわかりやすく書いてある方がいいですよね。

rom mod import n のパターン

「from mod import n」のようなインポートを書く際にも若干気をつけていることがあります。 それは、「from 〜 import でインポートするのはモジュールまで」ということです。

上記「import *」についても言えることですが、以下のような状態を回避するという意味でこのように書くようにしています。

たとえば、 module.py というファイルに以下のような定義があるとします。

# module.py
values = ['aaa']

そして、このモジュールを以下のように読み込むソースコード source.py があるとします。

# source.py
from module import values

この場合の source.py の values と module.py の values は、 source.py でのインポート時点では同じオブジェクトになっています。 しかし、もし module.values の値が以下のように書き換えられてしまった場合、これ以降に読み込んだ module.values と source.py の values が別のオブジェクトになってしまいます。 そもそもモジュールのトップレベルスコープに存在する変数を書き換えるという行為をあまり行うべきではありませんが、あまり起きてほしくない状況です。

import module
module.values = {}

ただ、このポリシに関しては、テストコードに関してはあまり気にしていません。 私は基本的にテストコードはモジュール単位でファイルを分けるようにしているので、テスト対象の関数やクラスなどは from 〜 import で直接インポートしてしまいます。このようにインポートしたとしても、テストコードなので実行単位がとても小さいというのも理由としてあげられます。

メリット

このポリシを適用して私が嬉しいと思っていることは、「ソースコードの一部を見る際に参照している変数の出自が絞られる」ことだと思っています。

例えばこのポリシにおいて、あるモジュールの一部分のソースが以下のようなものであったとします。

class SomeClass(object):

    # 省略

    def some_method(self, x, y):

        v = y * x

        return function(self.x) + mod.calc(v)

このポリシを守っておくと、some_method のスコープの中で . によって属性アクセスしないで書いて参照できるスコープは

  • 関数のローカルスコープ
  • ネストされた外側のスコープ
  • モジュールグローバルスコープ
  • __builtin__ モジュール

のみとなり、ソースの一部分を読む際に頭の中にキャッシュしておくべき情報が少なくて済みます。

実装上のメリットというよりも、補完等を全く使わない環境においての読みやすさを念頭に入れた書き方です。

デメリット

ただ、この「from 〜 import でインポートするのはモジュールまで」というポリシに関しては、そうでない場合と比べて若干のデメリットもあります。

第一に記述が長くなることが挙げられます。 これに関しては

from outermodule import innermodule as inner

といったように as を使うことで短くなります。

第二に、外部モジュールのオブジェクトを参照するためには . による属性アクセスを行う必要があり、辞書ルックアップが発生してしまうことです。 これに関しては仕方がないとしか言えません。 Python の仕様ですので。

このレベルでの最適化がモジュール単位で必要な状況というのはあまり考えられませんし、モジュールのトップレベルスコープのことを気にするよりも、もっと小さな関数のスコープにおいてモジュール名前空間のオブジェクトをローカル変数で参照させるなどを行って、以下のように辞書ルックアップの回数を減らす方が効果があるのではないかと思います。

import os

# 省略

def func():
    u'''
    os.listdir を大量に呼ぶ関数であるとする
    '''

    listdir = os.listdir

    # ローカル変数 listdir を使って os.listdir を呼び出すようにする

それぞれそれほど大きなデメリットとは思っていませんので、あまり気にはしていません。

まとめ

私の書くソースコードの基本的な方針としては、「スコープと名前空間に気を遣うことでソースコードを俯瞰したときに、状態を把握しやすくし、ソースコードを読む効率を上げる」ことであるといえると思います。 ソースコードは書くよりも読むことが多いため、読みやすくすることが開発効率を上げることにつながります。

以上、私が Python でソースコードを書く上で気にしているスコープと名前空間に関してお話しました。

Bookfair

O'Reilly Japanのセレクションフェア、全国の書店さんで開催
[ブックフェアのページへ]

Feedback

皆さんのご意見をお聞かせください。ご購入いただいた書籍やオライリー・ジャパンへのご感想やご意見、ご提案などをお聞かせください。より良い書籍づくりやサービス改良のための参考にさせていただきます。
[feedbackページへ]