試験運用中なLinux備忘録・旧記事

はてなダイアリーで公開していた2007年5月-2015年3月の記事を保存しています。

Pythonでオブジェクトのプロパティを使用する

PythonのオブジェクトにおいてC#言語やVala言語にあるようなプロパティ(関連記事)を用いる方法についてを扱う。

プロパティについて再び

オブジェクトのメンバ変数は、(オブジェクト指向における)オブジェクトとその利用側の管理上の理由により、直接外からいじるのではなく、公開されたメンバ関数(アクセサ)を通して読み書きするのが好ましいとされる。また、そうすることにより読み書きに制限を付けることもできるのだが、クラスの外からは関数を呼ぶ形となることもあり、扱いにくい。
プロパティはこの扱いを簡単にし、クラスの外からメンバ変数の扱いに近い感覚でそのメンバ関数を呼び出すことができるようになる。

メモ

objectクラスをもとにしたクラスにおいて、値を格納するメンバとそのメンバの値を読み書きするメンバ関数(アクセサ)を用意し、Pythonの組み込み関数property()でそれらのアクセサを引数に指定した戻り値としてのメンバをプロパティとして用いる。*1
データの書き込みを行うメンバ関数は代入された値を引数として受け取るが、その引数の名前は「value」でなくてもよい。

class TestClass(object):
  __a = 0  # このメンバは外からは直接参照できない
  def __init__(self, default_a):
    self.a = default_a
  def get_a(self):
    """
    get(読み取り)アクセサ/getter
    """
    return self.__a
  def set_a(self, new_a):  # 代入された値が引数になる
    """
    set(書き込み)アクセサ/setter
    """
    if (new_a > 100):
      self.__a = new_a
    else:
      print '*** WARNING *** new_a(%d) must be greater than 100.' % new_a
  # プロパティ
  # クラスの外から「a」の名前で読み書きするように扱う
  # 内部ではここで関連付けたメンバ関数が呼ばれる
  a = property(get_a, set_a)

これでクラスを元にして作成されたオブジェクト(インスタンス)側ではメンバaとして値を取り出したり書き込んだりできるようになる。上の例ではsetアクセサで100を超える値のみ受け付け、そうでないときにはメッセージを表示して値は変わらない。
これとは別に、読み取り専用にしたいプロパティがあるとき、同名のメンバ関数*2に「@property」という記述(関数デコレータ)を付けることで、書き込みを試みたときにAttributeError例外を出すようにできる。

class TestClass(object):
  def __init__(self, default_a):
    self.__foo = 12345    # 読み取り専用にすることにする
  # 別の読み取り専用プロパティ
  @property
  def foo(self):
    return self.__foo

上の例ではクラスの外からは[オブジェクト].fooとして(メンバ変数__fooの値の)読み取りができるが、代入はできないようになる。
なお、Pythonproperty()によるプロパティでは、「オブジェクト作成時にのみ代入可であとは読み取りのみ」といった高度なアクセス制御は行えない。

[任意]ファイル名: propertytest.py

#! /usr/bin/python
# -*- encoding: utf-8 -*-

# Pythonにおけるプロパティのテスト

class TestClass(object):
  __a = 0  # このメンバは外からは直接参照できない
  def __init__(self, default_a):
    self.a = default_a
    self.__foo = 12345    # 読み取り専用にすることにする
  def get_a(self):
    """
    get(読み取り)アクセサ/getter
    """
    return self.__a
  def set_a(self, new_a):  # 代入された値が引数になる
    """
    set(書き込み)アクセサ/setter
    """
    if (new_a > 100):
      self.__a = new_a
    else:
      print '*** WARNING *** new_a(%d) must be greater than 100.' % new_a
  # プロパティ
  # クラスの外から「a」の名前で読み書きするように扱う
  # 内部ではここで関連付けたメンバ関数が呼ばれる
  a = property(get_a, set_a)
  # 別の読み取り専用プロパティ
  @property
  def foo(self):
    return self.__foo

print '>>> obj = TestClass(999)'
obj = TestClass(999)
print 'obj.a:%d\n' % obj.a

# 受け付けない範囲を入れても反映されない
print '>>> obj.a = 10'
obj.a = 10
print 'obj.a:%d\n' % obj.a

# 正しい範囲の値を入れると反映される
print '>>> obj.a = 123'
obj.a = 123
print 'obj.a:%d\n' % obj.a

# 読み取り専用プロパティの参照と間違った書き込み
print 'obj.foo:%d' % obj.foo
print '>>> obj.foo = 10000'
try:
  obj.foo = 10000
except AttributeError, (msg):
  print 'AttributeError: %s' % msg

下は実行結果。

>>> obj = TestClass(999)
obj.a:999

>>> obj.a = 10
*** WARNING *** new_a(10) must be greater than 100.
obj.a:999

>>> obj.a = 123
obj.a:123

obj.foo:12345
>>> obj.foo = 10000
AttributeError: can't set attribute

(2010/2/25)オブジェクト初期化時以外にプロパティに書き込めないようにしたい場合、__init__()の中でコンストラクタ引数に受け取った値を対象のメンバに書き込むようにしつつそのメンバは(「@property」による)読み取り専用のプロパティで取り出せるようにすればよい。

#! /usr/bin/python
# -*- encoding: utf-8 -*-

# オブジェクト初期化時にコンストラクタ引数でメンバに値を設定し
# それを読み取り専用プロパティで取り出す形のテスト

class TestClass(object):
  def __init__(self, default_b):
    self.__b = default_b
  @property
  def b(self):
    return self.__b

print '>>> obj = TestClass("TEST TEST TEST")'
obj = TestClass("TEST TEST TEST")
print 'obj.b:%s' % obj.b
print '>>> obj.b = "abc"'
try:
  obj.b = "abc"
except AttributeError, (msg):
  print 'AttributeError: %s' % msg

下は実行結果。

>>> obj = TestClass("TEST TEST TEST")
obj.b:TEST TEST TEST
>>> obj.b = "abc"
AttributeError: can't set attribute

関連記事:

使用したバージョン:

*1:property()の1番目の引数のメンバ関数は値の取り出し時、2番目の引数のメンバ関数は値の代入時に呼ばれる

*2:getのアクセサで、やりとりするメンバをreturnする内容