Pythonの名前空間

beanz (2008年11月13日 14:34) | コメント(0) | トラックバック(0)

Pythonの名前空間はちょっとした曲者だ。
モジュールのトップレベルでだけでプログラミングしているだけなら気にする事は無いが関数を定義してたりすると注意が必要だ。
Pythonで関数定義をした場合新たに関数内だけのスコープが作られる。いわゆるローカルスコープだ。
ただし、変数を参照するだけならばグローバルな変数にもアクセスできる。
例えば

name = 'nobunaga'

def func():
  print name

func()

というコードがあったとしたら問題なく'nobunaga'と出力される。 これは関数内からグローバル変数'name'にアクセスしているということになる。 次は関数内で変数に対して代入を行った場合。

def func():
  name = 'hideyoshi'
  print name

今度は'hideyoshi'と出力される。まあ、想像通りの結果だろう。 ただ、これはグローバル変数の'name'を上書きしている訳ではなく関数内で新たにローカル変数'name'を作成している。 なので関数の外では相変わらず'nobunaga'が入った変数'name'は健在だ。

では次のようなコードだとどうなるだろう。

def func():
  print name
  name = 'hideyoshi'

この後関数funcを呼び出すとエラーが起きる。
これは関数内で変数に代入したらその関数内すべてでその変数はローカル変数になってしまうという事だろう。
Javascriptも同じような挙動になる(エラーにはならないが変数が未定義値になる)。
最初の'name'はいかにもグローバル変数の'nobunaga'が出力されるんじゃないかと思ってしまうが、実際はローカルな'name'にアクセスしようとしている。
ただしこの時点ではローカル変数'name'が存在しないためにエラーとなってしまう。
要は変数への代入は出来るだけ関数内の最初の方で行えという事だ。
ちなみに関数内でグローバル変数に対して代入や上書きを行いたい時はglobal文を使うとそれが可能になる。

def func():
  global name
  print name
  name = 'hideyoshi'

funcを実行すると'nobunaga'と表示された後グローバル変数'name'は'hideyoshi'に変わってしまっている。

クラスを使うともっとややこしい話になってくる。
クラスはそのクラスを基にインスタンスやサブクラスを生成する事が出来る。
そしてそのそれぞれで名前空間が存在する。ただしインスタンスやサブクラスからは基になったクラスの変数(属性)を参照する事は出来る。
たとえば

class Employee:
  count = 0
  def __init__(self, name):
    self.name = name
    Employee.count += 1


class Engineer(Employee):
  pass
  
class Designer(Employee):
  pass

こんなようなコードがあったとしよう。
インタラクティブシェルを立ち上げ上記のコードをインポートした後。

y = Designer('やまだ')

などとすると'Designer'クラスのインスタンス'd'が作成される。 この時点で'd'は'name'という属性を持つ。

y.name

とすれば'やまだ'が得られる。
さらにインスタンスはクラスからすべての属性を継承するので

y.count

とすれば'1'が得られる。
インスタンス'd'自体にcountという属性は無いが基となったクラス、さらにはそのクラスのスーパークラスから属性を検索してクラス'Employee'で見つかる。
その値を表示しているという事だ。
いわばクラスの属性はそのクラスから派生したサブクラスやインスタンスすべてで共有されている訳だ。
なので

Engineer.count

Designer.count

Employee.count

も、すべて同じ数値(この時点では1)を参照している。
続いてインスタンスをどんどん作っていってみる。

s = Engineer('さとう')
i = Employee('いとう')
k = Designer('かとう')

と、この時点でそれぞれのインスタンスから'count'属性を表示させてみると全部4だ。
では

s.count = 100

とするとどうなるだろう。
さっきの関数の話の後なので想像は付くと思うがインスタンス's'に新たな属性'count'が作られる。
そして以降

s.count

を実行すると100が表示されて今までアクセスできていたEmployeeクラスの属性'count'には同じ方法ではアクセスできなくなってしまう。
他のインスタンスは相変わらず4が表示される。
関数の時と同じように変数の参照はまず一番狭い範囲から検索され見つからなければ一つ上の階層へ、そこでも見つからなければさらに上へと検索していく。
最後まで見つからなければエラーになる。逆に見つかった時点で検索は即終了となる。
ところが変数への代入の場合はとにかく一番狭い範囲での変数に対してだけ行う。上位への検索は一切しない。無ければ新しく作る。
という事らしい。

ではでは、こんなコードはどうなんだろう?

i.count += 10

これは、参照と代入を一緒に行っている。
通常Pythonでは存在しない変数を参照したらエラーになってしまう。
だが、この場合は先ほどの約束事からすると上位へ検索していきEmployeeクラスで見つかる。
なので参照は出来た(そして4という数値を得る)。次に参照した数値に10プラスして代入を試みるが、ここでインスタンス'i'自身の属性として新たに'count'が作成される。
結果'i.count'は14になるが'Employee.count'は4のままなのである。
ただし、関数ではこの考え方では駄目らしい(エラーになってしまった)。

上のコードのEmployeeクラスを定義している

class Employee:
  count = 0
  def __init__(self, name):
    self.name = name
    Employee.count += 1

この部分で例えばクラス名を直接書きたくないとしてこう書く事も出来る。

class Employee:
  count = 0
  def __init__(self, name):
    self.name = name
    self.__class__.count += 1

ただしこう書いた場合は注意が必要になってくる。
selfと書いている時点で"そのインスタンスの〜"ということになるのでどのクラスから作成されたかが関わってくる。
インタラクティブシェルを立ち上げ直して実験してみる。

最初にEmployeeクラスのインスタンスを作成する

y = Employee('やまだ')

と、この時点でのそれぞれのcount属性は

Employee.count => 1
Engineer.count => 1
Designer.count => 1
y.count => 1

こうなっている。

次Engineerクラスのインスタンスを作成

s = Engineer('さとう')

すると

Employee.count => 1
Engineer.count => 2
Designer.count => 1
y.count => 1
s.count => 2

とすでにおかしなことになっている。
これは最初はEngineerクラスやDesignerクラスには'count'属性が存在しないからだ。
Employeeクラスのインスタンスだけを作っている間はEngineerクラス、DesignerクラスはそれぞれスーパークラスであるEngineerクラスから'count'属性を参照できる。
ただし、上の例のようにEngineerクラスのインスタンスを作った時点で新たにEngineerクラスの属性'count'が作られ、それ以降はEngineerクラスの'count'属性が参照されるからだ。
Engineerクラスの属性'count'が作られる時はそれまで参照していたEmployeeクラスの数値に1プラスして自身の属性とするのでEngineerクラスだけが2に増えているという事なのだ。
なのでこの場合はそれぞれのクラスにはなから'count'属性を用意しておかなければならない。

class Engineer(Employee):
  count = 0
  
class Designer(Employee):
  count = 0

こんなふうに。
まあ、分かってしまえばなんて事の無い話なのだが。

カテゴリ:

トラックバック(0)

トラックバックURL: http://blog.beanz-net.jp/beanz_mtos/mt-tb.cgi/20

コメントする