namedtuple簡易實現(xiàn)

在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.元類:

陌生的 metaclass

2.exec:

官方文檔

3.描述符:

描述符是一種特殊的對象慧起,這種對象實現(xiàn)了 __get____set__ 册倒,__delete__ 這三個特殊方法中任意的一個

其中蚓挤,實現(xiàn)了 __get__ 以及 __set__ / __delete__ 的是 Data descriptors ,而只實現(xiàn)了 __get__ 的是Non-Data descriptor 。這兩者有什么區(qū)別呢灿意?

我們調用一個屬性估灿,順序如下:

  1. 如果attr出現(xiàn)在類的__dict__中,且attr是一個Data descriptor缤剧,那么調用__get__
  2. 如果attr出現(xiàn)在實例的__dict__中馅袁, 那么直接返回
  3. 如果attr出現(xiàn)在類的__dict__中:
    3.1 如果是Non-Data descriptor, 那么調用其__get__方法
    3.2 返回cls.__dict__['attr']
  4. 若有__getattr__方法則調用
  5. 否則拋出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) 特性捍岳。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末富寿,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子锣夹,更是在濱河造成了極大的恐慌页徐,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件银萍,死亡現(xiàn)場離奇詭異变勇,居然都是意外死亡,警方通過查閱死者的電腦和手機贴唇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進店門搀绣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人戳气,你說我怎么就攤上這事链患。” “怎么了物咳?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵锣险,是天一觀的道長蹄皱。 經常有香客問我览闰,道長芯肤,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任压鉴,我火速辦了婚禮崖咨,結果婚禮上,老公的妹妹穿的比我還像新娘油吭。我一直安慰自己击蹲,他們只是感情好,可當我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布婉宰。 她就那樣靜靜地躺著歌豺,像睡著了一般。 火紅的嫁衣襯著肌膚如雪心包。 梳的紋絲不亂的頭發(fā)上类咧,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天,我揣著相機與錄音蟹腾,去河邊找鬼痕惋。 笑死,一個胖子當著我的面吹牛娃殖,可吹牛的內容都是我干的值戳。 我是一名探鬼主播,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼炉爆,長吁一口氣:“原來是場噩夢啊……” “哼堕虹!你這毒婦竟也來了?” 一聲冷哼從身側響起芬首,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤鲫凶,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后衩辟,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體螟炫,經...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年艺晴,在試婚紗的時候發(fā)現(xiàn)自己被綠了昼钻。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡封寞,死狀恐怖然评,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情狈究,我是刑警寧澤碗淌,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響亿眠,放射性物質發(fā)生泄漏碎罚。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一纳像、第九天 我趴在偏房一處隱蔽的房頂上張望荆烈。 院中可真熱鬧,春花似錦竟趾、人聲如沸憔购。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽玫鸟。三九已至,卻和暖如春犀勒,著一層夾襖步出監(jiān)牢的瞬間屎飘,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工账蓉, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留枚碗,地道東北人。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓铸本,卻偏偏與公主長得像肮雨,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子箱玷,可洞房花燭夜當晚...
    茶點故事閱讀 43,490評論 2 348

推薦閱讀更多精彩內容