home changes contents help options

124:デフォルトの値を設定する

collections.defaultdict - 初期化設定付きの辞書

Python 2.5 より collection モジュールに defaultdict が追加されました。 名前のとおり初期化を自分自身でおこなう辞書です。コンストラクタの第1引数 default_factory には 初期値を生成する callable オブジェクトを与えます。 たとえば int をあたえると 0 、 lambda: None をあたえると None が初期値となります。

含まないキーにアクセスすると default_factory() の戻り値で初期化しつつ値が返ります。:

   >>> d = collections.defaultdict(int)
   >>> d
   defaultdict(<type 'int'>, {})
   >>> d["a"]
   0
   >>> d["b"]
   0
   >>> d
   defaultdict(<type 'int'>, {'a': 0, 'b': 0})

しかし、 defaultdict の使い方の本命は別にあります。 default_factory にコンテナのコンストラクタを渡すとなかなかに楽しいです。 list を与えると [] が、 dict を与えると {} が、 set を与えると空のセットが初期値となります。 『119:1つのキーに複数の値が対応するハッシュを作る』の解でもあります:

   >>> import collections
   >>> dev = collections.defaultdict(set)
   >>> dev["lang"].add("C")
   >>> dev["lang"].add("perl")
   >>> dev["lang"].add("python")
   >>> dev["lang"].add("ruby")
   >>> dev["editor"].add("vim")
   >>> dev["editor"].add("sakuraEditor")
   >>> dev["browser"].add("Sleipnir")
   >>> for item in dev.items():
   ...     print item
   ...
   ('lang', set(['python', 'C', 'ruby', 'perl']))
   ('editor', set(['sakuraEditor', 'vim']))
   ('browser', set(['Sleipnir']))

キーがない場合でも空のセットが自動で用意されるので遠慮なく add メソッドを呼べます。

Python 2.4 以前では辞書型の setdefault メソッドを使うと同じようなことが可能です。

リストを初期化

Ruby には a1 = Array.new(10){|i| [] } などで 配列をその場で作る構文があります。

この辺のお手軽さでは Python は少々負けているかもしれませんね。 Python ではリストの * 演算子、リスト内包、そして copy.deepcopy を駆使することとなります。

初期値が不変の型の場合 * 演算子でリストを繰り返すのが楽です。:

   >>> a = [0] * 10
   >>> a
   [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

可変の型の場合は * 演算子で繰り返すと参照先を共有してしまうのでリスト内包を使います。:

   >>> b = [[0 for i in range(3)] for j in range(2)]
   >>> b
   [[0, 0, 0], [0, 0, 0]]
   >>> import StringIO
   >>> c = [StringIO.StringIO() for i in range(3)]
   >>> c
   [<StringIO.StringIO instance at 0x00B4C8F0>, <StringIO.StringIO instance at 0x00
   B4C918>, <StringIO.StringIO instance at 0x00B4C940>]

次元が大きすぎる多次元配列を作る際は Panopticon - Pythonの多次元リストをどのように作るべきか のアイディアが優れていると思います。:

   import copy

   def hyperlist(dimension=(), baselist=[]):
      if dimension:
         return hyperlist(dimension[1:], [ copy.deepcopy(baselist) for i in range(0, dimension[0]) ])
      else:
         return baselist

   >>> a = hyperlist((4,3,2))
   >>> a
   [[[[], [], [], []], [[], [], [], []], [[], [], [], []]],
    [[[], [], [], []], [[], [], [], []], [[], [], [], []]]]

Panopticon の方は、 copy.deepcopy() を使わずに済ます方法がないか探しているようでありました。 なにか思いついた方がいればぜひコメントを。

defaultdict みたいなリスト、いりますか?

辞書の便利さを持ったリスト…いくつか作ってみました。 とはいえこのような改造リストを使うより辞書や defaultdict をそのまま 使ったほうがよいような気がします。 作成に当たり odz buffer - 自分で拡張すればいいのに を 参考にさせていただいています。ありがとうございます。

dict.get みたいな関数です。:

   def list_get(list_, i, default=None):
       if not isinstance(i, int):
           raise KeyError('index must be integer')
       elif i < 0:
           raise KeyError('index out of range')

       if i >= len(list_):
           return default
       else:
           return list_[i]
   >>> L = range(10)
   >>> print list_get(L, 0, None)
   0
   >>> print list_get(L, 100, None)
   None

dict.get みたいなメソッドを持つリストです。:

   class List2(list):
       def get(self, i, default=None):
           if not isinstance(i, int):
               raise KeyError('index must be integer')
           elif i < 0:
               raise KeyError('index out of range')

           if i >= len(self):
               return default
           else:
               return self[i]
   >>> L = List2(range(10))
   >>> L
   [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
   >>> print L.get(0, None)
   0
   >>> print L.get(100, None)
   None

dict.get みたいなメソッドを持つリスト、先ほどの list_get 関数をつかえば 2 行で実装。 コピペコード量産自重。後の祭り。:

   class List3(list):
       get = list_get

長さより大きいインデックスにアクセスすると Ruby の配列っぽく自動で長くなるリスト、のつもりです。 テストできていません。興味のある方、検証お願いいたします。 オリジナルのコメント同様、スライスがらみの操作を行ったときが危ない気がします。:

   class defaultlist(list):
       def __init__(self, default_factory=lambda: None, *args, **kwargs):
           super(defaultlist, self).__init__(*args, **kwargs)
           self.default_factory = default_factory

       def __getitem__(self, key):
           self._check_range(key)

           try:
               if key >= len(self):
                   self.extend(self.default_factory() for i in xrange(len(self), key + 1))
           except TypeError:
               raise TypeError("%r is not callable" % self.default_factory)

           return super(defaultlist, self).__getitem__(key)

       def __setitem__(self, key, value):
           self._check_range(key)

           if key < len(self):
               return super(defaultlist, self).__setitem__(key, value)
           else:
               try:
                   self.extend(self.default_factory() for i in xrange(len(self), key))
               except TypeError:
                   raise TypeError("%r is not callable" % self.default_factory)
               return super(defaultlist, self).append(value)

       def _check_range(self, key):
           if not isinstance(key, int):
               raise KeyError('index must be integer')

   >>> L = defaultlist(list)
   >>> L[3].append(1)
   >>> L[3].append(2)
   >>> L[5].extend(['a', 'b', 'c'])
   >>> L
   [[], [], [], [1, 2], [], ['a', 'b', 'c']]
   >>> L2 = defaultlist()
   >>> L2[5] = 5
   >>> L2
   [None, None, None, None, None, 5]