無為閣

為無為,事無事

在Python用Data Object傳遞資料 II

之前我從在在Python 用Data Object傳遞資料裡介紹了Data Object的寫法, 在這篇介紹另一種實做方式 - Metaclass

源碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class DataObjectType(type):
    def __init__(mbs, name, bases, dct):

        super(DataObjectType, mbs).__init__(name, bases, dct)
        # 將建好的class的__init__ function 設為 clsinit
        mbs.__init__ = clsinit
        # 將建好的class的__repr__ function 設為 clsrepr
        mbs.__repr__ = clsrepr

def clsinit(self, **kwargs):
    # 更新self, 也就是建構好的instance的attributes
    self.__dict__.update(kwargs)

def clsrepr(self):
    # 顯示 DataObject(a=1,b=2)這樣的字串
    return "{}({})".format(self.__class__.__name__,
            ','.join([ "{}={}".format(k,v) for k,v in self.__dict__.items()]))

# 將Value class 的 type 改成 DataObjectType, 原本應該是object的type (也就是type)
# 你可以在python interpreter裡打 type(object) 看一下結果 :p
class Value(object):
    __metaclass__ = DataObjectType

# here we go!
v = Value(x=1,y=2,z=3)
print v.x
# 1

而如果你想要跟collections.namedtuple 一樣可以動態產生class的話, 像這樣

1
2
3
Value = dataobject('Value', 'x,y,z')
print Value.x
# None

其實做方式如下

1
2
3
4
def dataobject(name, attrsstr):
    for attrname in attrsstr.split(','):
        dct[attrname] = None
    return DataObjectType(name, (), dct)

ok, 我們來檢查一下class的行為是不是符合我們的預期.

a: 建立名為Value的class,含有x,y屬性

1
>>> dataobject('Value', 'x,y')

b: Value class 含有y 屬性, 正確

1
2
>>> dataobject('Value', 'x,y').y == None
True

c: Value class 沒有包含z屬性(因為沒有定義), 正確

1
2
3
4
>>> dataobject('Value', 'x,y').z == None
Traceback (most recent call last):
  File "", line 1, in
AttributeError: type object 'Value' has no attribute 'z'

讓DataObject的屬性為唯讀

到目前為止我們已經可以動態產生data object class, 也可以動態設定data object 的值. 但卻沒辦法像namedtuple一樣, 強迫設值的動作只能在建立instance時進行. 範例如下

1
2
3
4
5
6
7
8
9
10
11
12
>>> from collections import namedtuple
>>> cls = namedtuple('TestClass', 'x,y')
# 建立instance
>>> obj = cls(x=1,y=2)
# 讀取instance的x 屬性
>>> obj.x
1
# 設定instance 的 x 屬性 (喔喔, 不能設定)
>>> obj.x = 3
Traceback (most recent call last):
  File "", line 1, in
AttributeError: can't set attribute

要達成相似的行為非常簡單, 只要對DataObjectType 的 setattr method 動點手腳 :p 先寫一個會檢查instance有沒有readonly的變數, 若有檢查其值, 為真時, 則不允許設值

1
2
3
4
5
6
7
8
9
def clssetattr(self, k, v):
    try:
        # check readonly attributes
        readonly = self.readonly
    except AttributeError:
        readonly = False
    if readonly:
        raise AttributeError("can't set attribute")
    object.__setattr__(self, k, v)

再把DataObjectType產生的class的setattr換掉

1
2
3
4
5
6
7
class DataObjectType(type):
    def __init__(mbs, name, bases, dct):
        super(DataObjectType, mbs).__init__(name, bases, dct)
        mbs.__init__ = clsinit
        mbs.__repr__ = clsrepr
        mbs.__setattr__ = clssetattr
.....

完成! 來看看結果吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#首先, 沒有設定readonly的狀態下
>>> obj = Value(x=3)
#成功設定x為4
>>> obj.x=4
#再來把Value設為readonly
>>> Value.readonly = True
>>> obj = Value(x=3)
# 沒辦法把x設成4, 成功!
>>> obj.x=4
Traceback (most recent call last):
  File "", line 1, in
  File "metaclass.py", line 41, in clssetattr
    raise AttributeError("can't set attribute")
AttributeError: can't set attribute

結論

在Refactory一書中, 建議使用data object來減少需要傳遞的參數, 而Python又可以使用property使得getter function 使用方式與讀取attribute 一模一樣, 使得data object在python裡面更powerful.

推薦大家使用 :)

Comments