namedtuple簡易實現
在python中,namedtuple建立一個和tuple類似的物件,可以使用名稱來訪問元素的資料物件,通常用來增強程式碼的可讀性, 在訪問一些tuple型別的資料時尤其好用。
我們可以這樣使用:
from collections import namedtuple
User = namedtuple('User', ['id', 'name'])
u = User(1, 'aa')
print(u.name) # aa
那麼,namedtuple是如何實現的呢。
見名知意,通過namedtuple的名字,我們可以推測,namedtuple繼承了tuple,並使我們定義的欄位名和tuple下標建立某種聯絡,使得通過欄位名來訪問資料成為可能。
顯然,我們無法預知使用者傳入的欄位名是什麼。比如上面的例子User = namedtuple('User', ['id', 'name'])
欄位名id和name,下次有可能需要新增一個age欄位。這就要求我們要動態地建立類,在python中就需要通過元類來實現。
如何修改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內建不可變型別的例項化行為,需要我們實現__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類實現如上,它繼承tuple並重寫了__new__
方法,根據我們傳入的引數包裝成一個可迭代物件,最後呼叫父類的__new__
方法。但它還是有個嚴重的問題:不能夠動態接收引數。這裡我們傳的是id和name作為欄位名,下一次我們可能希望傳入id、name、age作欄位名。有人可能會想到用*args
,*args
雖然能解決以上問題,但又會產生新的問題:無法對引數數量進行限制。我們最終定義的函式應該像這樣:def name_tuple(cls_name, field_names)
。它接收兩個引數cls_name為生成類的類名,我們最終希望通過obj.欄位名
的方式去獲取tuple中的元素,所以還需要傳入第二個引數:field_names,field_names為一系列欄位名,可以是一個可迭代物件,或是一個字串。我們希望根據field_names中欄位的數量,去動態控制__new__
方法中可接受的引數數量。
那麼究竟應該怎麼做?如果我們有一個模板,並動態往裡面填充我們想要的欄位名作為引數,不就實現了這一需求了嗎。就像這樣:
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 是一門動態語言,在 Python 中,exec()能夠動態地執行復雜的Python程式碼,它能夠接收一個字串,並將其作為Python程式碼執行,比如:
exec('a=1')
print(globals().get('a')) # 1
目前為止,我們能實現如下程式碼:
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進行處理,生成傳入模板作為引數的字串。
之後定義了namespace和template變數,並將它們作為引數傳入exec。
exec能接收三個引數:
- object:必選引數,表示需要被指定的Python程式碼。它必須是字串或code物件。如果object是一個字串,該字串會先被解析為一組Python語句,然後在執行(除非發生語法錯誤)。如果object是一個code物件,那麼它只是被簡單的執行。
- globals:可選引數,表示全域性名稱空間(存放全域性變數),如果被提供,則必須是一個字典物件。
- locals:可選引數,表示當前區域性名稱空間(存放區域性變數),如果被提供,可以是任何對映物件。如果該引數被忽略,那麼它將會取與globals相同的值。
- 如果globals與locals都被忽略,那麼它們將取exec()函式被呼叫環境下的全域性名稱空間和區域性名稱空間。
執行後產生的__new__
方法可以通過namespace['__new__']
獲取。
最後一句return type(cls_name, (tuple,), class_namespace)
非常關鍵,它表示生成一個名為cls_name的類,且繼承自tuple。第三個引數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'
可以發現最後一句報錯了,因為我們並沒有在class_namespace字典中新增名為name的屬性。
現在要考慮的是如何新增這些鍵值對,屬性名我們很容易拿到,接下來要做的就是獲取值;此外,不僅要獲取,而且還要和tuple一致,保證這些屬性是隻讀,不可變的(immutable)。
通過property可以實現上述操作。通常,我們會這麼使用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
本身又建立了另一個裝飾器@name.setter
,負責把一個setter方法變成屬性賦值,若不定義這一方法,則表示name屬性是隻讀的。
property還有另一種寫法:
class User():
__name = 'private'
def name(self):
return self.__name
name = property(fget=name)
以上兩種property的用法是等價的。理解了這些之後,我們繼續實現程式碼:
for i, v in enumerate(field_names):
rv = itemgetter(i)
class_namespace[v] = property(rv)
itemgetter函式如下:
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已經實現。關於namedtuple的官方完整實現可以參考它的原始碼。
擴充套件
1.元類:
2.exec:
3.描述符:
描述符是一種特殊的物件,這種物件實現了 __get__
,__set__
,__delete__
這三個特殊方法中任意的一個
其中,實現了 __get__
以及 __set__
/ __delete__
的是 Data descriptors ,而只實現了 __get__
的是Non-Data descriptor 。這兩者有什麼區別呢?
我們呼叫一個屬性,順序如下:
- 如果attr出現在類的
__dict__
中,且attr是一個Data descriptor
,那麼呼叫__get__
- 如果attr出現在例項的
__dict__
中, 那麼直接返回 - 如果attr出現在類的
__dict__
中:
3.1 如果是Non-Data descriptor
, 那麼呼叫其__get__
方法
3.2 返回cls.__dict__['attr']
- 若有
__getattr__
方法則呼叫 - 否則丟擲AttributeError
更多與描述符相關的內容可以參考官方文件
4.property
一種property的模擬實現:
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
物件的例項,它也是一個描述符,當一個變數成為一個描述符後,它將改變正常的呼叫邏輯,現在當我們 u.name='public'
的時候,因為我們的name是一個 Data descriptors ,那麼不管我們的例項字典中是否有 name 的存在,我們都會觸發其 __set__
方法,由於在我們初始化該變數時,沒有為其傳入 fset
的方法,因此,我們 __set__
方法在執行過程中將會丟擲 AttributeError("can't set attribute")
的異常。我們在簡易實現namedtuple時使用了property,這保證了它將遵循了 tuple
的 不可變 (immutable) 特性。
相關文章
- 簡易版 vue實現Vue
- 簡易實現一個expressExpress
- 簡易RPC框架實現RPC框架
- 實現一個簡易版WebpackWeb
- 實現一個簡易的vueVue
- Go 實現簡易 RPC 框架GoRPC框架
- UNIX Domain Socket實現簡易聊天AI
- 學習Promise && 簡易實現PromisePromise
- 模擬實現簡易版shell
- QT實現簡易串列埠助手QT串列埠
- 簡易執行緒池實現執行緒
- 簡易實現 HTTPS (一) 自動實現 sslHTTP
- 基於Vue的簡易MVVM實現VueMVVM
- NodeJS實現簡易區塊鏈NodeJS區塊鏈
- C++實現簡易計算器C++
- KOA的簡易模板引擎實現方式
- 基於Websocket的簡易webshell實現Webshell
- node的讀寫流簡易實現
- Python實現簡易記事本Python
- go實現簡易分散式系統Go分散式
- 讓動畫實現更簡單,Flutter 動畫簡易教程!動畫Flutter
- 簡易實現Redis監控自動簡訊告警Redis
- 簡易 Vue 評論框架的實現————父元件的實現Vue框架元件
- Go 實現簡易的 Redis 客戶端GoRedis客戶端
- 基於react的hash路由簡易實現React路由
- 從零手動實現簡易TomcatTomcat
- 【node】檔案上傳功能簡易實現
- 微信小程式 簡易搜尋功能實現微信小程式
- 來實現一個簡易版的 PromisePromise
- 支援向量機python實現(簡易版)Python
- 利用 trait 簡易 Facade 實現AI
- javascript 非同步模組載入 簡易實現JavaScript非同步
- NATAPP實現內網穿透簡易教程APP內網穿透
- 簡易版的Spring框架之IOC簡單實現Spring框架
- 簡易撲克牌遊戲簡單實現,歡迎指正遊戲
- Python namedtuplePython
- js實現簡易的touch事件(es5)JS事件
- react/vue中dom-diff簡易版實現ReactVue