0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

這可能是Python面向?qū)ο缶幊痰淖罴褜?shí)踐

WpOh_rgznai100 ? 來源:YXQ ? 2019-06-22 12:07 ? 次閱讀

Python 是支持面向?qū)ο蟮?,很多情況下使用面向?qū)ο?a href="http://www.ttokpm.com/v/tag/1315/" target="_blank">編程會(huì)使得代碼更加容易擴(kuò)展,并且可維護(hù)性更高,但是如果你寫的多了或者某一對象非常復(fù)雜了,其中的一些寫法會(huì)相當(dāng)相當(dāng)繁瑣,而且我們會(huì)經(jīng)常碰到對象和 JSON 序列化及反序列化的問題,原生的 Python 轉(zhuǎn)起來還是很費(fèi)勁的。

可能這么說大家會(huì)覺得有點(diǎn)抽象,那么這里舉幾個(gè)例子來感受一下。

首先讓我們定義一個(gè)對象吧,比如顏色。我們常用 RGB 三個(gè)原色來表示顏色,R、G、B 分別代表紅、綠、藍(lán)三個(gè)顏色的數(shù)值,范圍是 0-255,也就是每個(gè)原色有 256 個(gè)取值。如 RGB(0, 0, 0) 就代表黑色,RGB(255, 255, 255) 就代表白色,RGB(255, 0, 0) 就代表紅色,如果不太明白可以具體看看 RGB 顏色的定義哈。

好,那么我們現(xiàn)在如果想定義一個(gè)顏色對象,那么正常的寫法就是這樣了,創(chuàng)建這個(gè)對象的時(shí)候需要三個(gè)參數(shù),就是 R、G、B 三個(gè)數(shù)值,定義如下:

class Color(object): “”“ Color Object of RGB ”“” def __init__(self, r, g, b): self.r = r self.g = g self.b = b

其實(shí)對象一般就是這么定義的,初始化方法里面?zhèn)魅敫鱾€(gè)參數(shù),然后定義全局變量并賦值這些值。其實(shí)挺多常用語言比如 Java、PHP 里面都是這么定義的。但其實(shí)這種寫法是比較冗余的,比如 r、g、b 這三個(gè)變量一寫就寫了三遍。

好,那么我們初始化一下這個(gè)對象,然后打印輸出下,看看什么結(jié)果:

color = Color(255, 255, 255)print(color)

結(jié)果是什么樣的呢?或許我們也就能看懂一個(gè) Color 吧,別的都沒有什么有效信息,像這樣子:

《__main__.Color object at 0x103436f60》

我們知道,在 Python 里面想要定義某個(gè)對象本身的打印輸出結(jié)果的時(shí)候,需要實(shí)現(xiàn)它的__repr__ 方法,所以我們比如我們添加這么一個(gè)方法:

def __repr__(self): return f‘{self.__class__.__name__}(r={self.r}, g={self.g}, b={self.b})’

這里使用了 Python 中的 fstring 來實(shí)現(xiàn)了 __repr__ 方法,在這里我們構(gòu)造了一個(gè)字符串并返回,字符串中包含了這個(gè) Color 類中的 r、g、b 屬性,這個(gè)返回的結(jié)果就是 print 的打印結(jié)果,我們再重新執(zhí)行一下,結(jié)果就變成這樣子了:

Color(r=255, g=255, b=255)

改完之后,這樣打印的對象就會(huì)變成這樣的字符串形式了,感覺看起來清楚多了吧?

再繼續(xù),如果我們要想實(shí)現(xiàn)這個(gè)對象里面的 __eq__、__lt__ 等各種方法來實(shí)現(xiàn)對象之間的比較呢?照樣需要繼續(xù)定義成類似這樣子的形式:

def __lt__(self, other): if not isinstance(other, self.__class__): return NotImplemented return (self.r, self.g, self.b) 《 (other.r, other.g, other.b)

這里是 __lt__ 方法,有了這個(gè)方法就可以使用比較符來對兩個(gè) Color 對象進(jìn)行比較了,但這里又把這幾個(gè)屬性寫了兩遍。

最后再考慮考慮,如果我要把 JSON 轉(zhuǎn)成 Color 對象,難道我要讀完 JSON 然后一個(gè)個(gè)屬性賦值嗎?如果我想把 Color 對象轉(zhuǎn)化為 JSON,又得把這幾個(gè)屬性寫幾遍呢?如果我突然又加了一個(gè)屬性比如透明度 a 參數(shù),那么整個(gè)類的方法和參數(shù)都要修改,這是極其難以擴(kuò)展的。不知道你能不能忍,反正我不能忍!

如果你用過 Scrapy、Django 等框架,你會(huì)發(fā)現(xiàn) Scrapy 里面有一個(gè) Item 的定義,只需要定義一些 Field 就可以了,Django 里面的 Model 也類似這樣,只需要定義其中的幾個(gè)字段屬性就可以完成整個(gè)類的定義了,非常方便。

說到這里,我們能不能把 Scrapy 或 Django 里面的定義模式直接拿過來呢?能是能,但是沒必要,因?yàn)槲覀冞€有專門為 Python 面向?qū)ο蠖鴮iT誕生的庫,沒錯(cuò),就是 attrs 和 cattrs 這兩個(gè)庫。

有了 attrs 庫,我們就可以非常方便地定義各個(gè)對象了,另外對于 JSON 的轉(zhuǎn)化,可以進(jìn)一步借助 cattrs 這個(gè)庫,非常有幫助。

說了這么多,還是沒有介紹這兩個(gè)庫的具體用法,下面我們來詳細(xì)介紹下。

安裝

安裝這兩個(gè)庫非常簡單,使用 pip 就好了,命令如下:

pip3 install attrs cattrs

安裝好了之后我們就可以導(dǎo)入并使用這兩個(gè)庫了。

簡介與特性

首先我們來介紹下 attrs 這個(gè)庫,其官方的介紹如下:

attrs 是這樣的一個(gè) Python 工具包,它能將你從繁綜復(fù)雜的實(shí)現(xiàn)上解脫出來,享受編寫 Python 類的快樂。它的目標(biāo)就是在不減慢你編程速度的前提下,幫助你來編寫簡潔而又正確的代碼。

其實(shí)意思就是用了它,定義和實(shí)現(xiàn) Python 類變得更加簡潔和高效。

基本用法

首先明確一點(diǎn),我們現(xiàn)在是裝了 attrs 和 cattrs 這兩個(gè)庫,但是實(shí)際導(dǎo)入的時(shí)候是使用 attr 和 cattr 這兩個(gè)包,是不帶 s 的。

在 attr 這個(gè)庫里面有兩個(gè)比較常用的組件叫做 attrs 和 attr,前者是主要用來修飾一個(gè)自定義類的,后者是定義類里面的一個(gè)字段的。有了它們,我們就可以將上文中的定義改寫成下面的樣子:

from attr import attrs, attrib@attrsclass Color(object): r = attrib(type=int, default=0) g = attrib(type=int, default=0) b = attrib(type=int, default=0)if __name__ == ‘__main__’: color = Color(255, 255, 255) print(color)

看我們操作的,首先我們導(dǎo)入了剛才所說的兩個(gè)組件,然后用 attrs 里面修飾了 Color 這個(gè)自定義類,然后用 attrib 來定義一個(gè)個(gè)屬性,同時(shí)可以指定屬性的類型和默認(rèn)值。最后打印輸出,結(jié)果如下:

Color(r=255, g=255, b=255)

怎么樣,達(dá)成了一樣的輸出效果!

觀察一下有什么變化,是不是變得更簡潔了?r、g、b 三個(gè)屬性都只寫了一次,同時(shí)還指定了各個(gè)字段的類型和默認(rèn)值,另外也不需要再定義 __init__ 方法和 __repr__ 方法了,一切都顯得那么簡潔。一個(gè)字,爽!

實(shí)際上,主要是 attrs 這個(gè)修飾符起了作用,然后根據(jù)定義的 attrib 屬性自動(dòng)幫我們實(shí)現(xiàn)了__init__、__repr__、__eq__、__ne__、__lt__、__le__、__gt__、__ge__、__hash__ 這幾個(gè)方法。

如使用 attrs 修飾的類定義是這樣子:

from attr import attrs, attrib@attrsclass SmartClass(object): a = attrib() b = attrib()

其實(shí)就相當(dāng)于已經(jīng)實(shí)現(xiàn)了這些方法:

class RoughClass(object): def __init__(self, a, b): self.a = a self.b = b def __repr__(self): return “RoughClass(a={}, b={})”.format(self.a, self.b) def __eq__(self, other): if other.__class__ is self.__class__: return (self.a, self.b) == (other.a, other.b) else: return NotImplemented def __ne__(self, other): result = self.__eq__(other) if result is NotImplemented: return NotImplemented else: return not result def __lt__(self, other): if other.__class__ is self.__class__: return (self.a, self.b) 《 (other.a, other.b) else: return NotImplemented def __le__(self, other): if other.__class__ is self.__class__: return (self.a, self.b) 《= (other.a, other.b) else: return NotImplemented def __gt__(self, other): if other.__class__ is self.__class__: return (self.a, self.b) 》 (other.a, other.b) else: return NotImplemented def __ge__(self, other): if other.__class__ is self.__class__: return (self.a, self.b) 》= (other.a, other.b) else: return NotImplemented def __hash__(self): return hash((self.__class__, self.a, self.b))

所以說,如果我們用了 attrs 的話,就可以不用再寫這些冗余又復(fù)雜的代碼了。

翻看源碼可以發(fā)現(xiàn),其內(nèi)部新建了一個(gè) ClassBuilder,通過一些屬性操作來動(dòng)態(tài)添加了上面的這些方法,如果想深入研究,建議可以看下 attrs 庫的源碼。

別名使用

這時(shí)候大家可能有個(gè)小小的疑問,感覺里面的定義好亂啊,庫名叫做 attrs,包名叫做 attr,然后又導(dǎo)入了 attrs 和 attrib,這太奇怪了。為了幫大家解除疑慮,我們來梳理一下它們的名字。

首先庫的名字就叫做 attrs,這個(gè)就是裝 Python 包的時(shí)候這么裝就行了。但是庫的名字和導(dǎo)入的包的名字確實(shí)是不一樣的,我們用的時(shí)候就導(dǎo)入 attr 這個(gè)包就行了,里面包含了各種各樣的模塊和組件,這是完全固定的。

好,然后接下來看看 attr 包里面包含了什么,剛才我們引入了 attrs 和 attrib。

首先是 attrs,它主要是用來修飾 class 類的,而 attrib 主要是用來做屬性定義的,這個(gè)就記住它們兩個(gè)的用法就好了。

翻了一下源代碼,發(fā)現(xiàn)其實(shí)它還有一些別名:

s = attributes = attrsib = attr = attrib

也就是說,attrs 可以用 s 或 attributes 來代替,attrib 可以用 attr 或 ib 來代替。

既然是別名,那么上面的類就可以改寫成下面的樣子:

from attr import s, ib@sclass Color(object): r = ib(type=int, default=0) g = ib(type=int, default=0) b = ib(type=int, default=0)if __name__ == ‘__main__’: color = Color(255, 255, 255) print(color)

是不是更加簡潔了,當(dāng)然你也可以把 s 改寫為 attributes,ib 改寫為 attr,隨你怎么用啦。

不過我覺得比較舒服的是 attrs 和 attrib 的搭配,感覺可讀性更好一些,當(dāng)然這個(gè)看個(gè)人喜好。

所以總結(jié)一下:

庫名:attrs

導(dǎo)入包名:attr

修飾類:s 或 attributes 或 attrs

定義屬性:ib 或 attr 或 attrib

OK,理清了這幾部分內(nèi)容,我們繼續(xù)往下深入了解它的用法吧。

聲明和比較

在這里我們再聲明一個(gè)簡單一點(diǎn)的數(shù)據(jù)結(jié)構(gòu),比如叫做 Point,包含 x、y 的坐標(biāo),定義如下:

from attr import attrs, attrib@attrsclass Point(object): x = attrib() y = attrib()

其中 attrib 里面什么參數(shù)都沒有,如果我們要使用的話,參數(shù)可以順次指定,也可以根據(jù)名字指定,如:

p1 = Point(1, 2)print(p1)p2 = Point(x=1, y=2)print(p2)

其效果都是一樣的,打印輸出結(jié)果如下:

Point(x=1, y=2)Point(x=1, y=2)

OK,接下來讓我們再驗(yàn)證下類之間的比較方法,由于使用了 attrs,相當(dāng)于我們定義的類已經(jīng)有了 __eq__、__ne__、__lt__、__le__、__gt__、__ge__ 這幾個(gè)方法,所以我們可以直接使用比較符來對類和類之間進(jìn)行比較,下面我們用實(shí)例來感受一下:

print(‘Equal:’, Point(1, 2) == Point(1, 2))print(‘Not Equal(ne):’, Point(1, 2) != Point(3, 4))print(‘Less Than(lt):’, Point(1, 2) 《 Point(3, 4))print(‘Less or Equal(le):’, Point(1, 2) 《= Point(1, 4), Point(1, 2) 《= Point(1, 2))print(‘Greater Than(gt):’, Point(4, 2) 》 Point(3, 2), Point(4, 2) 》 Point(3, 1))print(‘Greater or Equal(ge):’, Point(4, 2) 》= Point(4, 1))

運(yùn)行結(jié)果如下:

Same: FalseEqual: TrueNot Equal(ne): TrueLess Than(lt): TrueLess or Equal(le): True TrueGreater Than(gt): True TrueGreater or Equal(ge): True

可能有的朋友不知道 ne、lt、le 什么的是什么意思,不過看到這里你應(yīng)該明白啦,ne 就是 Not Equal 的意思,就是不相等,le 就是 Less or Equal 的意思,就是小于或等于。

其內(nèi)部怎么實(shí)現(xiàn)的呢,就是把類的各個(gè)屬性轉(zhuǎn)成元組來比較了,比如 Point(1, 2) 《 Point(3, 4) 實(shí)際上就是比較了 (1, 2) 和 (3, 4) 兩個(gè)元組,那么元組之間的比較邏輯又是怎樣的呢,這里就不展開了,如果不明白的話可以參考官方文檔:https://docs.python.org/3/library/stdtypes.html#comparisons。

屬性定義

現(xiàn)在看來,對于這個(gè)類的定義莫過于每個(gè)屬性的定義了,也就是 attrib 的定義。對于 attrib 的定義,我們可以傳入各種參數(shù),不同的參數(shù)對于這個(gè)類的定義有非常大的影響。

下面我們就來詳細(xì)了解一下每個(gè)屬性的具體參數(shù)和用法吧。

首先讓我們概覽一下總共可能有多少可以控制一個(gè)屬性的參數(shù),我們用 attrs 里面的 fields 方法可以查看一下:

from attr import attrs, attrib, fields@attrsclass Point(object): x = attrib() y = attrib()print(fields(Point))

這就可以輸出 Point 的所有屬性和對應(yīng)的參數(shù),結(jié)果如下:

(Attribute(name=‘x’, default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), Attribute(name=‘y’, default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False))

輸出出來了,可以看到結(jié)果是一個(gè)元組,元組每一個(gè)元素都其實(shí)是一個(gè) Attribute 對象,包含了各個(gè)參數(shù),下面詳細(xì)解釋下幾個(gè)參數(shù)的含義:

name:屬性的名字,是一個(gè)字符串類型。

default:屬性的默認(rèn)值,如果沒有傳入初始化數(shù)據(jù),那么就會(huì)使用默認(rèn)值。如果沒有默認(rèn)值定義,那么就是 NOTHING,即沒有默認(rèn)值。

validator:驗(yàn)證器,檢查傳入的參數(shù)是否合法。

init:是否參與初始化,如果為 False,那么這個(gè)參數(shù)不能當(dāng)做類的初始化參數(shù),默認(rèn)是 True。

metadata:元數(shù)據(jù),只讀性的附加數(shù)據(jù)。

type:類型,比如 int、str 等各種類型,默認(rèn)為 None。

converter:轉(zhuǎn)換器,進(jìn)行一些值的處理和轉(zhuǎn)換器,增加容錯(cuò)性。

kw_only:是否為強(qiáng)制關(guān)鍵字參數(shù),默認(rèn)為 False。

屬性名

對于屬性名,非常清楚了,我們定義什么屬性,屬性名就是什么,例如上面的例子,定義了:

x = attrib()

那么其屬性名就是 x。

默認(rèn)值

對于默認(rèn)值,如果在初始化的時(shí)候沒有指定,那么就會(huì)默認(rèn)使用默認(rèn)值進(jìn)行初始化,我們看下面的一個(gè)實(shí)例:

from attr import attrs, attrib, fields@attrsclass Point(object): x = attrib() y = attrib(default=100)if __name__ == ‘__main__’: print(Point(x=1, y=3)) print(Point(x=1))

在這里我們將 y 屬性的默認(rèn)值設(shè)置為了 100,在初始化的時(shí)候,第一次都傳入了 x、y 兩個(gè)參數(shù),第二次只傳入了 x 這個(gè)參數(shù),看下運(yùn)行結(jié)果:

Point(x=1, y=3)Point(x=1, y=100)

可以看到結(jié)果,當(dāng)設(shè)置了默認(rèn)參數(shù)的屬性沒有被傳入值時(shí),他就會(huì)使用設(shè)置的默認(rèn)值進(jìn)行初始化。

那假如沒有設(shè)置默認(rèn)值但是也沒有初始化呢?比如執(zhí)行下:

Point()

那么就會(huì)報(bào)錯(cuò)了,錯(cuò)誤如下:

TypeError: __init__() missing 1 required positional argument: ‘x’

所以說,如果一個(gè)屬性,我們一旦沒有設(shè)置默認(rèn)值同時(shí)沒有傳入的話,就會(huì)引起錯(cuò)誤。所以,一般來說,為了穩(wěn)妥起見,設(shè)置一個(gè)默認(rèn)值比較好,即使是 None 也可以的。

初始化

如果一個(gè)類的某些屬性不想?yún)⑴c初始化,比如想直接設(shè)置一個(gè)初始值,一直固定不變,我們可以將屬性的 init 參數(shù)設(shè)置為 False,看一個(gè)實(shí)例:

from attr import attrs, attrib@attrsclass Point(object): x = attrib(init=False, default=10) y = attrib()if __name__ == ‘__main__’: print(Point(3))

比如 x 我們只想在初始化的時(shí)候設(shè)置固定值,不想初始化的時(shí)候被改變和設(shè)定,我們將其設(shè)置了 init 參數(shù)為 False,同時(shí)設(shè)置了一個(gè)默認(rèn)值,如果不設(shè)置默認(rèn)值,默認(rèn)為 NOTHING。然后初始化的時(shí)候我們只傳入了一個(gè)值,其實(shí)也就是為 y 這個(gè)屬性賦值。

這樣的話,看下運(yùn)行結(jié)果:

Point(x=10, y=3)

沒什么問題,y 被賦值為了我們設(shè)置的值 3。

那假如我們非要設(shè)置 x 呢?會(huì)發(fā)生什么,比如改寫成這樣子:

Point(1, 2)

報(bào)錯(cuò)了,錯(cuò)誤如下:

TypeError: __init__() takes 2 positional arguments but 3 were given

參數(shù)過多,也就是說,已經(jīng)將 init 設(shè)置為 False 的屬性就不再被算作可以被初始化的屬性了。

強(qiáng)制關(guān)鍵字

強(qiáng)制關(guān)鍵字是 Python 里面的一個(gè)特性,在傳入的時(shí)候必須使用關(guān)鍵字的名字來傳入,如果不太理解可以再了解下 Python 的基礎(chǔ)。

設(shè)置了強(qiáng)制關(guān)鍵字參數(shù)的屬性必須要放在后面,其后面不能再有非強(qiáng)制關(guān)鍵字參數(shù)的屬性,否則會(huì)報(bào)這樣的錯(cuò)誤:

ValueError: Non keyword-only attributes are not allowed after a keyword-only attribute (unless they are init=False)

好,我們來看一個(gè)例子,我們將最后一個(gè)屬性設(shè)置 kw_only 參數(shù)為 True:

from attr import attrs, attrib, fields@attrsclass Point(object): x = attrib(default=0) y = attrib(kw_only=True)if __name__ == ‘__main__’: print(Point(1, y=3))

如果設(shè)置了 kw_only 參數(shù)為 True,那么在初始化的時(shí)候必須傳入關(guān)鍵字的名字,這里就必須指定 y 這個(gè)名字,運(yùn)行結(jié)果如下:

Point(x=1, y=3)

如果沒有指定 y 這個(gè)名字,像這樣調(diào)用:

Point(1, 3)

那么就會(huì)報(bào)錯(cuò):

TypeError: __init__() takes from 1 to 2 positional arguments but 3 were given

所以,這個(gè)參數(shù)就是設(shè)置初始化傳參必須要用名字來傳,否則會(huì)出現(xiàn)錯(cuò)誤。

注意,如果我們將一個(gè)屬性設(shè)置了 init 為 False,那么 kw_only 這個(gè)參數(shù)會(huì)被忽略。

驗(yàn)證器

有時(shí)候在設(shè)置一個(gè)屬性的時(shí)候必須要滿足某個(gè)條件,比如性別必須要是男或者女,否則就不合法。對于這種情況,我們就需要有條件來控制某些屬性不能為非法值。

下面我們看一個(gè)實(shí)例:

from attr import attrs, attribdef is_valid_gender(instance, attribute, value): if value not in [‘male’, ‘female’]: raise ValueError(f‘gender {value} is not valid’)@attrsclass Person(object): name = attrib() gender = attrib(validator=is_valid_gender)if __name__ == ‘__main__’: print(Person(name=‘Mike’, gender=‘male’)) print(Person(name=‘Mike’, gender=‘mlae’))

在這里我們定義了一個(gè)驗(yàn)證器 Validator 方法,叫做 is_valid_gender。然后定義了一個(gè)類 Person 還有它的兩個(gè)屬性 name 和 gender,其中 gender 定義的時(shí)候傳入了一個(gè)參數(shù) validator,其值就是我們定義的 Validator 方法。

這個(gè) Validator 定義的時(shí)候有幾個(gè)固定的參數(shù):

instance:類對象

attribute:屬性名

value:屬性值

這是三個(gè)參數(shù)是固定的,在類初始化的時(shí)候,其內(nèi)部會(huì)將這三個(gè)參數(shù)傳遞給這個(gè) Validator,因此 Validator 里面就可以接受到這三個(gè)值,然后進(jìn)行判斷即可。在 Validator 里面,我們判斷如果不是男性或女性,那么就直接拋出錯(cuò)誤。

下面做了兩個(gè)實(shí)驗(yàn),一個(gè)就是正常傳入 male,另一個(gè)寫錯(cuò)了,寫的是 mlae,觀察下運(yùn)行結(jié)果:

Person(name=‘Mike’, gender=‘male’)TypeError: __init__() missing 1 required positional argument: ‘gender’

OK,結(jié)果顯而易見了,第二個(gè)報(bào)錯(cuò)了,因?yàn)槠渲挡皇钦5男詣e,所以程序直接報(bào)錯(cuò)終止。

注意在 Validator 里面返回 True 或 False 是沒用的,錯(cuò)誤的值還會(huì)被照常復(fù)制。所以,一定要在 Validator 里面 raise 某個(gè)錯(cuò)誤。

另外 attrs 庫里面還給我們內(nèi)置了好多 Validator,比如判斷類型,這里我們再增加一個(gè)屬性 age,必須為 int 類型:

age = attrib(validator=validators.instance_of(int))

這時(shí)候初始化的時(shí)候就必須傳入 int 類型,如果為其他類型,則直接拋錯(cuò):

TypeError: (“‘a(chǎn)ge’ must be 《class ‘int’》 (got ‘x’ that is a 《class ‘str’》)。

另外還有其他的一些 Validator,比如與或運(yùn)算、可執(zhí)行判斷、可迭代判斷等等,可以參考官方文檔:https://www.attrs.org/en/stable/api.html#validators。

另外 validator 參數(shù)還支持多個(gè) Validator,比如我們要設(shè)置既要是數(shù)字,又要小于 100,那么可以把幾個(gè) Validator 放到一個(gè)列表里面并傳入:

from attr import attrs, attrib, validatorsdef is_less_than_100(instance, attribute, value): if value 》 100: raise ValueError(f‘a(chǎn)ge {value} must less than 100’)@attrsclass Person(object): name = attrib() gender = attrib(validator=is_valid_gender) age = attrib(validator=[validators.instance_of(int), is_less_than_100])if __name__ == ‘__main__’: print(Person(name=‘Mike’, gender=‘male’, age=500))

這樣就會(huì)將所有的 Validator 都執(zhí)行一遍,必須每個(gè) Validator 都滿足才可以。這里 age 傳入了 500,那么不符合第二個(gè) Validator,直接拋錯(cuò):

ValueError: age 500 must less than 100

轉(zhuǎn)換器

其實(shí)很多時(shí)候我們會(huì)不小心傳入一些形式不太標(biāo)準(zhǔn)的結(jié)果,比如本來是 int 類型的 100,我們傳入了字符串類型的 100,那這時(shí)候直接拋錯(cuò)應(yīng)該不好吧,所以我們可以設(shè)置一些轉(zhuǎn)換器來增強(qiáng)容錯(cuò)機(jī)制,比如將字符串自動(dòng)轉(zhuǎn)為數(shù)字等等,看一個(gè)實(shí)例:

from attr import attrs, attribdef to_int(value): try: return int(value) except: return None@attrsclass Point(object): x = attrib(converter=to_int) y = attrib()if __name__ == ‘__main__’: print(Point(‘100’, 3))

看這里,我們定義了一個(gè)方法,可以將值轉(zhuǎn)化為數(shù)字類型,如果不能轉(zhuǎn),那么就返回 None,這樣保證了任何可以被轉(zhuǎn)數(shù)字的值都被轉(zhuǎn)為數(shù)字,否則就留空,容錯(cuò)性非常高。

運(yùn)行結(jié)果如下:

Point(x=100, y=3)

類型

為什么把這個(gè)放到最后來講呢,因?yàn)?Python 中的類型是非常復(fù)雜的,有原生類型,有 typing 類型,有自定義類的類型。

首先我們來看看原生類型是怎樣的,這個(gè)很容易理解了,就是普通的 int、float、str 等類型,其定義如下:

from attr import attrs, attrib@attrsclass Point(object): x = attrib(type=int) y = attrib()if __name__ == ‘__main__’: print(Point(100, 3)) print(Point(‘100’, 3))

這里我們將 x 屬性定義為 int 類型了,初始化的時(shí)候傳入了數(shù)值型 100 和字符串型 100,結(jié)果如下:

Point(x=100, y=3)Point(x=‘100’, y=3)

但我們發(fā)現(xiàn),雖然定義了,但是不會(huì)被自動(dòng)轉(zhuǎn)類型的。

另外我們還可以自定義 typing 里面的類型,比如 List,另外 attrs 里面也提供了類型的定義:

from attr import attrs, attrib, Factoryimport typing@attrsclass Point(object): x = attrib(type=int) y = attrib(type=typing.List[int]) z = attrib(type=Factory(list))

這里我們引入了 typing 這個(gè)包,定義了 y 為 int 數(shù)字組成的列表,z 使用了 attrs 里面定義的 Factory 定義了同樣為列表類型。

另外我們也可以進(jìn)行類型的嵌套,比如像這樣子:

from attr import attrs, attrib, Factoryimport typing@attrsclass Point(object): x = attrib(type=int, default=0) y = attrib(type=int, default=0)@attrsclass Line(object): name = attrib() points = attrib(type=typing.List[Point])if __name__ == ‘__main__’: points = [Point(i, i) for i in range(5)] print(points) line = Line(name=‘line1’, points=points) print(line)

在這里我們定義了 Point 類代表離散點(diǎn),隨后定義了線,其擁有 points 屬性是 Point 組成的列表。在初始化的時(shí)候我們聲明了五個(gè)點(diǎn),然后用這五個(gè)點(diǎn)組成的列表聲明了一條線,邏輯沒什么問題。

運(yùn)行結(jié)果:

[Point(x=0, y=0), Point(x=1, y=1), Point(x=2, y=2), Point(x=3, y=3), Point(x=4, y=4)]Line(name=‘line1’, points=[Point(x=0, y=0), Point(x=1, y=1), Point(x=2, y=2), Point(x=3, y=3), Point(x=4, y=4)])

可以看到這里我們得到了一個(gè)嵌套類型的 Line 對象,其值是 Point 類型組成的列表。

以上便是一些屬性的定義,把握好這些屬性的定義,我們就可以非常方便地定義一個(gè)類了。

序列轉(zhuǎn)換

在很多情況下,我們經(jīng)常會(huì)遇到 JSON 等字符串序列和對象互相轉(zhuǎn)換的需求,尤其是在寫 REST API、數(shù)據(jù)庫交互的時(shí)候。

attrs 庫的存在讓我們可以非常方便地定義 Python 類,但是它對于序列字符串的轉(zhuǎn)換功能還是比較薄弱的,cattrs 這個(gè)庫就是用來彌補(bǔ)這個(gè)缺陷的,下面我們再來看看 cattrs 這個(gè)庫。

cattrs 導(dǎo)入的時(shí)候名字也不太一樣,叫做 cattr,它里面提供了兩個(gè)主要的方法,叫做 structure 和 unstructure,兩個(gè)方法是相反的,對于類的序列化和反序列化支持非常好。

基本轉(zhuǎn)換

首先我們來看看基本的轉(zhuǎn)換方法的用法,看一個(gè)基本的轉(zhuǎn)換實(shí)例:

from attr import attrs, attribfrom cattr import unstructure, structure@attrsclass Point(object): x = attrib(type=int, default=0) y = attrib(type=int, default=0)if __name__ == ‘__main__’: point = Point(x=1, y=2) json = unstructure(point) print(‘json:’, json) obj = structure(json, Point) print(‘obj:’, obj)

在這里我們定義了一個(gè) Point 對象,然后調(diào)用 unstructure 方法即可直接轉(zhuǎn)換為 JSON 字符串。如果我們再想把它轉(zhuǎn)回來,那就需要調(diào)用 structure 方法,這樣就成功轉(zhuǎn)回了一個(gè) Point 對象。

看下運(yùn)行結(jié)果:

json: {‘x’: 1, ‘y’: 2}obj: Point(x=1, y=2)

當(dāng)然這種基本的來回轉(zhuǎn)用的多了就輕車熟路了。

多類型轉(zhuǎn)換

另外 structure 也支持一些其他的類型轉(zhuǎn)換,看下實(shí)例:

》》》 cattr.structure(1, str)‘1’》》》 cattr.structure(”1“, float)1.0》》》 cattr.structure([1.0, 2, ”3“], Tuple[int, int, int])(1, 2, 3)》》》 cattr.structure((1, 2, 3), MutableSequence[int])[1, 2, 3]》》》 cattr.structure((1, None, 3), List[Optional[str]])[‘1’, None, ‘3’]》》》 cattr.structure([1, 2, 3, 4], Set){1, 2, 3, 4}》》》 cattr.structure([[1, 2], [3, 4]], Set[FrozenSet[str]]){frozenset({‘4’, ‘3’}), frozenset({‘1’, ‘2’})}》》》 cattr.structure(OrderedDict([(1, 2), (3, 4)]), Dict){1: 2, 3: 4}》》》 cattr.structure([1, 2, 3], Tuple[int, str, float])(1, ‘2’, 3.0)

這里面用到了 Tuple、MutableSequence、Optional、Set 等類,都屬于 typing 這個(gè)模塊,后面我會(huì)寫內(nèi)容詳細(xì)介紹這個(gè)庫的用法。

不過總的來說,大部分情況下,JSON 和對象的互轉(zhuǎn)是用的最多的。

屬性處理

上面的例子都是理想情況下使用的,但在實(shí)際情況下,很容易遇到 JSON 和對象不對應(yīng)的情況,比如 JSON 多個(gè)字段,或者對象多個(gè)字段。

我們先看看下面的例子:

from attr import attrs, attribfrom cattr import structure@attrsclass Point(object): x = attrib(type=int, default=0) y = attrib(type=int, default=0)json = {‘x’: 1, ‘y’: 2, ‘z’: 3}print(structure(json, Point))

在這里,JSON 多了一個(gè)字段 z,而 Point 類只有 x、y 兩個(gè)字段,那么直接執(zhí)行 structure 會(huì)出現(xiàn)什么情況呢?

TypeError: __init__() got an unexpected keyword argument ‘z’

不出所料,報(bào)錯(cuò)了。意思是多了一個(gè)參數(shù),這個(gè)參數(shù)并沒有被定義。

這時(shí)候一般的解決方法的直接忽略這個(gè)參數(shù),可以重寫一下 structure 方法,定義如下:

def drop_nonattrs(d, type): if not isinstance(d, dict): return d attrs_attrs = getattr(type, ‘__attrs_attrs__’, None) if attrs_attrs is None: raise ValueError(f‘type {type} is not an attrs class’) attrs: Set[str] = {attr.name for attr in attrs_attrs} return {key: val for key, val in d.items() if key in attrs}def structure(d, type): return cattr.structure(drop_nonattrs(d, type), type)

這里定義了一個(gè) drop_nonattrs 方法,用于從 JSON 里面刪除對象里面不存在的屬性,然后調(diào)用新的 structure 方法即可,寫法如下:

from typing import Setfrom attr import attrs, attribimport cattr@attrsclass Point(object): x = attrib(type=int, default=0) y = attrib(type=int, default=0)def drop_nonattrs(d, type): if not isinstance(d, dict): return d attrs_attrs = getattr(type, ‘__attrs_attrs__’, None) if attrs_attrs is None: raise ValueError(f‘type {type} is not an attrs class’) attrs: Set[str] = {attr.name for attr in attrs_attrs} return {key: val for key, val in d.items() if key in attrs}def structure(d, type): return cattr.structure(drop_nonattrs(d, type), type)json = {‘x’: 1, ‘y’: 2, ‘z’: 3}print(structure(json, Point))

這樣我們就可以避免 JSON 字段冗余導(dǎo)致的轉(zhuǎn)換問題了。

另外還有一個(gè)常見的問題,那就是數(shù)據(jù)對象轉(zhuǎn)換,比如對于時(shí)間來說,在對象里面聲明我們一般會(huì)聲明為 datetime 類型,但在序列化的時(shí)候卻需要序列化為字符串。

所以,對于一些特殊類型的屬性,我們往往需要進(jìn)行特殊處理,這時(shí)候就需要我們針對某種特定的類型定義特定的 hook 處理方法,這里就需要用到 register_unstructure_hook 和 register_structure_hook 方法了。

下面這個(gè)例子是時(shí)間 datetime 轉(zhuǎn)換的時(shí)候進(jìn)行的處理:

import datetimefrom attr import attrs, attribimport cattrTIME_FORMAT = ‘%Y-%m-%dT%H:%M:%S.%fZ’@attrsclass Event(object): happened_at = attrib(type=datetime.datetime)cattr.register_unstructure_hook(datetime.datetime, lambda dt: dt.strftime(TIME_FORMAT))cattr.register_structure_hook(datetime.datetime, lambda string, _: datetime.datetime.strptime(string, TIME_FORMAT))event = Event(happened_at=datetime.datetime(2019, 6, 1))print(‘event:’, event)json = cattr.unstructure(event)print(‘json:’, json)event = cattr.structure(json, Event)print(‘Event:’, event)

在這里我們對 datetime 這個(gè)類型注冊了兩個(gè) hook,當(dāng)序列化的時(shí)候,就調(diào)用 strftime 方法轉(zhuǎn)回字符串,當(dāng)反序列化的時(shí)候,就調(diào)用 strptime 將其轉(zhuǎn)回 datetime 類型。

看下運(yùn)行結(jié)果:

event: Event(happened_at=datetime.datetime(2019, 6, 1, 0, 0))json: {‘happened_at’: ‘2019-06-01T00:00:00.000000Z’}Event: Event(happened_at=datetime.datetime(2019, 6, 1, 0, 0))

這樣對于一些特殊類型的屬性處理也得心應(yīng)手了。

嵌套處理

最后我們再來看看嵌套類型的處理,比如類里面有個(gè)屬性是另一個(gè)類的類型,如果遇到這種嵌套類的話,怎樣類轉(zhuǎn)轉(zhuǎn)換呢?我們用一個(gè)實(shí)例感受下:

from attr import attrs, attribfrom typing import Listfrom cattr import structure, unstructure@attrsclass Point(object): x = attrib(type=int, default=0) y = attrib(type=int, default=0)@attrsclass Color(object): r = attrib(default=0) g = attrib(default=0) b = attrib(default=0)@attrsclass Line(object): color = attrib(type=Color) points = attrib(type=List[Point])if __name__ == ‘__main__’: line = Line(color=Color(), points=[Point(i, i) for i in range(5)]) print(‘Object:’, line) json = unstructure(line) print(‘JSON:’, json) line = structure(json, Line) print(‘Object:’, line)

這里我們定義了兩個(gè) Class,一個(gè)是 Point,一個(gè)是 Color,然后定義了 Line 對象,其屬性類型一個(gè)是 Color 類型,一個(gè)是 Point 類型組成的列表,下面我們進(jìn)行序列化和反序列化操作,轉(zhuǎn)成 JSON 然后再由 JSON 轉(zhuǎn)回來,運(yùn)行結(jié)果如下:

Object: Line(color=Color(r=0, g=0, b=0), points=[Point(x=0, y=0), Point(x=1, y=1), Point(x=2, y=2), Point(x=3, y=3), Point(x=4, y=4)])JSON: {‘color’: {‘r’: 0, ‘g’: 0, ‘b’: 0}, ‘points’: [{‘x’: 0, ‘y’: 0}, {‘x’: 1, ‘y’: 1}, {‘x’: 2, ‘y’: 2}, {‘x’: 3, ‘y’: 3}, {‘x’: 4, ‘y’: 4}]}Object: Line(color=Color(r=0, g=0, b=0), points=[Point(x=0, y=0), Point(x=1, y=1), Point(x=2, y=2), Point(x=3, y=3), Point(x=4, y=4)])

可以看到,我們非常方便地將對象轉(zhuǎn)化為了 JSON 對象,然后也非常方便地轉(zhuǎn)回了對象。

這樣我們就成功實(shí)現(xiàn)了嵌套對象的序列化和反序列化,所有問題成功解決!

結(jié)語

本節(jié)介紹了利用 attrs 和 cattrs 兩個(gè)庫實(shí)現(xiàn) Python 面向?qū)ο缶幊痰膶?shí)踐,有了它們兩個(gè)的加持,Python 面向?qū)ο缶幊滩辉偈请y事。

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報(bào)投訴
  • 編程
    +關(guān)注

    關(guān)注

    88

    文章

    3521

    瀏覽量

    93262
  • python
    +關(guān)注

    關(guān)注

    53

    文章

    4753

    瀏覽量

    84068

原文標(biāo)題:這可能是Python面向?qū)ο缶幊痰淖罴褜?shí)踐

文章出處:【微信號(hào):rgznai100,微信公眾號(hào):rgznai100】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    Python面向對象編程詳解

    一般編程可分為面向過程編程,和面向對象編程Python
    發(fā)表于 09-04 16:35 ?452次閱讀
    <b class='flag-5'>Python</b>的<b class='flag-5'>面向</b><b class='flag-5'>對象</b><b class='flag-5'>編程</b>詳解

    labview面向對象編程

    點(diǎn)擊學(xué)習(xí)>>《龍哥手把手教你學(xué)LabVIEW視覺設(shè)計(jì)》視頻教程使用LabVIEW面向對象編程方法,對于大型測試應(yīng)用程序來講,面向對象相對于
    發(fā)表于 11-24 11:01

    labview面向對象編程

    有沒有大神在labview中用面向對象編程的?請教一下怎樣做?書上的對這方面的知識(shí)很少,無從下手,急急急急急急!
    發(fā)表于 08-08 14:26

    3分鐘看懂Python面向對象

    Python雖然是解釋型語言,但從設(shè)計(jì)之初就已經(jīng)是一門面向對象的語言,對于Python來說一切皆為對象。正因?yàn)槿绱?,?/div>
    發(fā)表于 06-08 14:20

    面向對象編程及其三大特性 精選資料分享

    編程語言分為面向過程編程、函數(shù)式編程面向對象編程
    發(fā)表于 07-21 08:38

    基于面向對象的LabVIEW編程有哪些優(yōu)勢

    基于面向對象的LabVIEW編程有哪些優(yōu)勢?如何去學(xué)習(xí)基于面向對象的LabVIEW編程?
    發(fā)表于 08-24 07:22

    談?wù)?b class='flag-5'>面向對象編程

    在工業(yè)自動(dòng)化領(lǐng)域,梯形圖邏輯仍然是最常用的編程語言之一,但對于更加復(fù)雜的控制對象面向對象編程不失為一種高效率的方式。下面先來談?wù)?/div>
    發(fā)表于 09-08 07:47

    面向對象編程語言的特點(diǎn)

    在工業(yè)自動(dòng)化領(lǐng)域,梯形圖邏輯仍然是最常用的編程語言之一,但對于更加復(fù)雜的控制對象,面向對象編程不失為一種高效率的方式。下面先來談?wù)?/div>
    發(fā)表于 09-08 07:44

    面向對象編程介紹

    目錄一、面向對象編程介紹1.面向過程編程2.函數(shù)式編程3.
    發(fā)表于 12-13 07:22

    Python編程實(shí)用指南

    Python 是一種解釋型、面向對象、動(dòng)態(tài)數(shù)據(jù)類型的高級(jí)程序設(shè)計(jì)語言。通過 Python 編程,我們能夠解決現(xiàn)實(shí)生活中的很多任務(wù)。本書是一本
    發(fā)表于 09-27 06:21

    面向對象編程練習(xí)

    實(shí)驗(yàn) 3 面向對象編程練習(xí) 一、實(shí)驗(yàn)?zāi)康?    通過編程和上機(jī)實(shí)驗(yàn)理解 Java 語言是如何體現(xiàn)面向
    發(fā)表于 09-23 18:57 ?3009次閱讀

    plc面向對象編程架構(gòu)與實(shí)現(xiàn)

    面向對象編程是計(jì)算機(jī)高級(jí)語言的一種先進(jìn)的編程模式,在工業(yè)控制系統(tǒng)的PLC程序中也可以采用這種設(shè)計(jì)思想,雖然我們無法實(shí)現(xiàn)面向
    發(fā)表于 01-31 15:00 ?4139次閱讀
    plc<b class='flag-5'>面向</b><b class='flag-5'>對象</b><b class='flag-5'>編程</b>架構(gòu)與實(shí)現(xiàn)

    史上最全Python面向對象編程的資料合集

    面向對象編程和函數(shù)式編程面向過程編程)都是程序設(shè)計(jì)的方法,不過稍有區(qū)別。
    的頭像 發(fā)表于 03-30 10:11 ?3070次閱讀

    西門子PLC面向對象編程

    面向對象編程是計(jì)算機(jī)高級(jí)語言的一種高級(jí)編程模式,這種設(shè)計(jì)思想也可以應(yīng)用于工業(yè)控制系統(tǒng)的plc程序中。雖然我們 無法實(shí)現(xiàn)面向
    發(fā)表于 04-17 11:41 ?4次下載
    西門子PLC<b class='flag-5'>面向</b><b class='flag-5'>對象</b><b class='flag-5'>編程</b>

    Python中的類和對象詳解

    Python 是一種面向對象編程語言,它支持類和對象。類是一種用戶自定義的數(shù)據(jù)類型,用于定義對象
    的頭像 發(fā)表于 04-20 16:53 ?960次閱讀