Pythonで読み込み専用フィールドを作る

読み込み専用のフィールドをPythonを作るとき、propertyを使う方法があると思います。しかし単純にpropertyを使うだけだと、フィールドが変更することは不可能ではありません。

  def test_property(self):
    class A(object):
      def __init__(self, i):
        self.i = i
      def get_i(self):
        return self.i
      def set_i(self, i):
        self.i = i
      x = property(get_i)
    a = A(2)
    self.assertEqual(2, a.i)
    self.assertEqual(2, a.x)
    def f(a):
      a.x = 3
    self.assertRaises(AttributeError, f, a)
    a.set_i(3)
    self.assertEqual(3, a.i)
    self.assertEqual(3, a.x)

本当の意味で読み込み専用にする方法としては、tupleを継承する方法があります。この方法はjinja2のlexer.Tokenクラスの実装で実際に使われている方法です。

import operator
class Token(tuple):
  type, value = (property(operator.itemgetter(x)) for x in range(2))
  def __new__(cls, type, value):
    return tuple.__new__(cls, (type, value))

Tokenは次のテストをパスします。Class Aのiフィールドのような、余計なものがないので、変数をセットすることも不可能です。

#!/usr/bin/env python2.6
import unittest
from test import test_support
from MY_jinja2.lexer import *

class LexerTests(unittest.TestCase):
  def test_Token(self):
    data = u'hello, world'
    token = Token('data', data)
    self.assertEqual('data', token.type)
    self.assertEqual(data, token.value)
  def test_Token_is_immutable(self):
    data = u'hello, world'
    token = Token('data', data)
    def f_value(token):
      token.value = 'value'
    def f_type(token):
      token.type = 'type'
    self.assertRaises(AttributeError, f_value, token)
    self.assertRaises(AttributeError, f_type, token)

if __name__ == '__main__':
  test_support.run_unittest(LexerTests)

なお、operator.itemgetterの動作については次のようなものです。

  def test_itemgetter(self):
    f = operator.itemgetter(3)
    l = range(10)
    self.assertEqual( l[3], f(l))