在python中,namedtuple創(chuàng)建一個和tuple類似的對象虱颗,可以使用名稱來訪問元素的數(shù)據(jù)對象罗丰,通常用來增強代碼的可讀性, 在訪問一些tuple類型的數(shù)據(jù)時尤其好用挠将。
我們可以這樣使用:
from collections import namedtuple
User = namedtuple('User', ['id', 'name'])
u = User(1, 'aa')
print(u.name) # aa
那么胳岂,namedtuple是如何實現(xiàn)的呢。
見名知意舔稀,通過namedtuple的名字乳丰,我們可以推測,namedtuple繼承了tuple内贮,并使我們定義的字段名和tuple下標建立某種聯(lián)系产园,使得通過字段名來訪問數(shù)據(jù)成為可能汞斧。
顯然,我們無法預知用戶傳入的字段名是什么什燕。比如上面的例子User = namedtuple('User', ['id', 'name'])
字段名id和name粘勒,下次有可能需要新增一個age字段。這就要求我們要動態(tài)地創(chuàng)建類屎即,在python中就需要通過元類來實現(xiàn)庙睡。
如何修改tuple的實例化行為呢,我們當然會首先想到繼承并重寫基類的構造方法技俐。比如下面這樣:
class MyTuple(tuple):
def __init__(self, iterable):
newiter = [i for i in iterable if i != 3]
tuple.__init__(newiter)
if __name__ == '__main__':
mytuple = MyTuple([1,2,3,4,5])
print(mytuple)
運行代碼乘陪,我們將看到打印結果為(1, 2, 3, 4, 5)
。這是因為雕擂,想要修改python內置不可變類型的實例化行為暂刘,需要我們實現(xiàn)__new__
方法。__new__
方法相當不常用捂刺,但是當繼承一個不可變的類型或使用元類時谣拣,它將派上用場。稍作修改的代碼如下:
class MyTuple(tuple):
def __new__(cls, id, name):
newiter = [i for i in iterable if i != 3]
return super(MyTuple, cls).__new__(cls, newiter)
if __name__ == '__main__':
mytuple = MyTuple([1,2,3,4,5])
print(mytuple)
這次族展,程序運行的結果就會是我們期望的(1, 2, 4, 5)
了
了解了以上知識后森缠,我們開始著手編寫代碼:
class User(tuple):
def __new__(cls, id, name):
iterable = (id, name)
return super(User, cls).__new__(cls, iterable)
if __name__ == '__main__':
user = User(1, 3)
print(user)
一個基本的User類實現(xiàn)如上,它繼承tuple并重寫了__new__
方法仪缸,根據(jù)我們傳入的參數(shù)包裝成一個可迭代對象贵涵,最后調用父類的__new__
方法。但它還是有個嚴重的問題:不能夠動態(tài)接收參數(shù)恰画。這里我們傳的是id和name作為字段名宾茂,下一次我們可能希望傳入id、name拴还、age作字段名跨晴。有人可能會想到用*args
,*args
雖然能解決以上問題片林,但又會產生新的問題:無法對參數(shù)數(shù)量進行限制端盆。我們最終定義的函數(shù)應該像這樣:def name_tuple(cls_name, field_names)
奸晴。它接收兩個參數(shù)cls_name為生成類的類名漩勤,我們最終希望通過obj.字段名
的方式去獲取tuple中的元素,所以還需要傳入第二個參數(shù):field_names沃斤,field_names為一系列字段名弓摘,可以是一個可迭代對象焚鹊,或是一個字符串。我們希望根據(jù)field_names中字段的數(shù)量韧献,去動態(tài)控制__new__
方法中可接受的參數(shù)數(shù)量末患。
那么究竟應該怎么做爷抓?如果我們有一個模板,并動態(tài)往里面填充我們想要的字段名作為參數(shù)阻塑,不就實現(xiàn)了這一需求了嗎蓝撇。就像這樣:
class_template = """
def __new__(_cls, {arg_list}):
return _tuple_new(_cls, ({arg_list}))'
"""
class_template.format(arg_list='id, name')
print(class_template)
最后生成的是個字符串,并不是我們需要的__new__
方法陈莽,如何將這一串字符串轉成方法呢渤昌?
眾所周知,Python 是一門動態(tài)語言走搁,在 Python 中独柑,exec()能夠動態(tài)地執(zhí)行復雜的Python代碼,它能夠接收一個字符串私植,并將其作為Python代碼執(zhí)行忌栅,比如:
exec('a=1')
print(globals().get('a')) # 1
目前為止,我們能實現(xiàn)如下代碼:
def name_tuple(cls_name, field_names):
if isinstance(field_names, str):
field_names = field_names.replace(',', ' ').split()
field_names = list(map(str, field_names))
arg_list = repr(field_names).replace("'", "")[1:-1]
tuple_new = tuple.__new__
namespace = {'_tuple_new': tuple_new, '__name__': f'namedtuple_{cls_name}'}
template = f'def __new__(_cls, {arg_list}): return _tuple_new(_cls, ({arg_list}))'
exec(template, namespace)
__new__ = namespace['__new__']
class_namespace = {
'__new__': __new__
}
return type(cls_name, (tuple,), class_namespace)
大概解釋一下上述代碼曲稼。首先對傳入的field_names進行處理索绪,若傳入的是字符串,則用split將其分割為列表贫悄,否則直接通過list(map(str, field_names))
將它轉為列表瑞驱。之后將field_names進行處理,生成傳入模板作為參數(shù)的字符串窄坦。
之后定義了namespace和template變量唤反,并將它們作為參數(shù)傳入exec。
exec能接收三個參數(shù):
- object:必選參數(shù)鸭津,表示需要被指定的Python代碼彤侍。它必須是字符串或code對象。如果object是一個字符串逆趋,該字符串會先被解析為一組Python語句盏阶,然后在執(zhí)行(除非發(fā)生語法錯誤)。如果object是一個code對象父泳,那么它只是被簡單的執(zhí)行般哼。
- globals:可選參數(shù)吴汪,表示全局命名空間(存放全局變量)惠窄,如果被提供,則必須是一個字典對象漾橙。
- locals:可選參數(shù)杆融,表示當前局部命名空間(存放局部變量),如果被提供霜运,可以是任何映射對象脾歇。如果該參數(shù)被忽略蒋腮,那么它將會取與globals相同的值。
- 如果globals與locals都被忽略藕各,那么它們將取exec()函數(shù)被調用環(huán)境下的全局命名空間和局部命名空間池摧。
執(zhí)行后產生的__new__
方法可以通過namespace['__new__']
獲取。
最后一句return type(cls_name, (tuple,), class_namespace)
非常關鍵激况,它表示生成一個名為cls_name的類作彤,且繼承自tuple。第三個參數(shù)class_namespace是一個包含屬性的字典乌逐,我們在其中添加了之前生成的__new__
方法竭讳。
讓我們測試一下:
User = name_tuple('User', ['id', 'name'])
print(User) # <class '__main__.User'>
u = User(1,'aa')
print(u) # (1, 'aa')
print(u.name) # AttributeError: 'User' object has no attribute 'name'
可以發(fā)現(xiàn)最后一句報錯了,因為我們并沒有在class_namespace字典中添加名為name的屬性浙踢。
現(xiàn)在要考慮的是如何添加這些鍵值對绢慢,屬性名我們很容易拿到,接下來要做的就是獲取值洛波;此外胰舆,不僅要獲取,而且還要和tuple一致蹬挤,保證這些屬性是只讀思瘟,不可變的(immutable)。
通過property可以實現(xiàn)上述操作闻伶。通常滨攻,我們會這么使用property:
class User():
__name = 'private'
@property
def name(self):
return self.__name
if __name__ == '__main__':
u = User()
print(u.name) # private
u.name = 'public' # AttributeError: can't set attribute
把一個方法變成屬性,只需要加上@property
裝飾器就可以了蓝翰,此時光绕,@property
本身又創(chuàng)建了另一個裝飾器@name.setter
,負責把一個setter方法變成屬性賦值畜份,若不定義這一方法诞帐,則表示name屬性是只讀的。
property還有另一種寫法:
class User():
__name = 'private'
def name(self):
return self.__name
name = property(fget=name)
以上兩種property的用法是等價的爆雹。理解了這些之后停蕉,我們繼續(xù)實現(xiàn)代碼:
for i, v in enumerate(field_names):
rv = itemgetter(i)
class_namespace[v] = property(rv)
itemgetter函數(shù)如下:
def itemgetter(item):
def func(obj):
return obj[item]
return func
完整代碼:
def itemgetter(item):
def func(obj):
return obj[item]
return func
def name_tuple(cls_name, field_names):
if isinstance(field_names, str):
field_names = field_names.replace(',', ' ').split()
field_names = list(map(str, field_names))
"a simple implementation of python's namedtuple"
arg_list = repr(field_names).replace("'", "")[1:-1]
tuple_new = tuple.__new__
namespace = {'_tuple_new': tuple_new, '__name__': f'namedtuple_{cls_name}'}
template = f'def __new__(_cls, {arg_list}): return _tuple_new(_cls, ({arg_list}))'
exec(template, namespace)
__new__ = namespace['__new__']
class_namespace = {
'__new__': __new__
}
for i, v in enumerate(field_names):
rv = itemgetter(i)
class_namespace[v] = property(rv)
return type(cls_name, (tuple,), class_namespace)
至此一個簡易版本的namedtuple已經實現(xiàn)。關于namedtuple的官方完整實現(xiàn)可以參考它的源碼钙态。
擴展
1.元類:
2.exec:
3.描述符:
描述符是一種特殊的對象慧起,這種對象實現(xiàn)了 __get__
,__set__
册倒,__delete__
這三個特殊方法中任意的一個
其中蚓挤,實現(xiàn)了 __get__
以及 __set__
/ __delete__
的是 Data descriptors ,而只實現(xiàn)了 __get__
的是Non-Data descriptor 。這兩者有什么區(qū)別呢灿意?
我們調用一個屬性估灿,順序如下:
- 如果attr出現(xiàn)在類的
__dict__
中,且attr是一個Data descriptor
缤剧,那么調用__get__
- 如果attr出現(xiàn)在實例的
__dict__
中馅袁, 那么直接返回 - 如果attr出現(xiàn)在類的
__dict__
中:
3.1 如果是Non-Data descriptor
, 那么調用其__get__
方法
3.2 返回cls.__dict__['attr']
- 若有
__getattr__
方法則調用 - 否則拋出AttributeError
更多與描述符相關的內容可以參考官方文檔
4.property
一種property的模擬實現(xiàn):
class Property(object):
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
if doc is None and fget is not None:
doc = fget.__doc__
self.__doc__ = doc
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError("unreadable attribute")
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can't delete attribute")
self.fdel(obj)
def getter(self, fget):
self.fget = fget
def setter(self, fset):
self.fset = fset
def deleter(self, fdel):
self.fdel = fdel
在之前的例子中荒辕,我們用@property裝飾器裝飾了name方法司顿,我們的 name就變成了一個 property
對象的實例,它也是一個描述符兄纺,當一個變量成為一個描述符后大溜,它將改變正常的調用邏輯,現(xiàn)在當我們 u.name='public'
的時候估脆,因為我們的name是一個 Data descriptors 钦奋,那么不管我們的實例字典中是否有 name 的存在,我們都會觸發(fā)其 __set__
方法疙赠,由于在我們初始化該變量時付材,沒有為其傳入 fset
的方法,因此圃阳,我們 __set__
方法在運行過程中將會拋出 AttributeError("can't set attribute")
的異常厌衔。我們在簡易實現(xiàn)namedtuple時使用了property,這保證了它將遵循了 tuple
的 不可變 (immutable) 特性捍岳。