您好,登錄后才能下訂單哦!
這篇文章主要介紹Python是如何實現面向對象編程的,文中介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們一定要看完!
Python 是支持面向對象的,很多情況下使用面向對象編程會使得代碼更加容易擴展,并且可維護性更高,但是如果你寫的多了或者某一對象非常復雜了,其中的一些寫法會相當相當繁瑣,而且我們會經常碰到對象和 JSON 序列化及反序列化的問題,原生的 Python 轉起來還是很費勁的。
可能這么說大家會覺得有點抽象,那么這里舉幾個例子來感受一下。
首先讓我們定義一個對象吧,比如顏色。我們常用 RGB 三個原色來表示顏色,R、G、B 分別代表紅、綠、藍三個顏色的數值,范圍是 0-255,也就是每個原色有 256 個取值。如 RGB(0, 0, 0) 就代表黑色,RGB(255, 255, 255) 就代表白色,RGB(255, 0, 0) 就代表紅色,如果不太明白可以具體看看 RGB 顏色的定義哈。
好,那么我們現在如果想定義一個顏色對象,那么正常的寫法就是這樣了,創建這個對象的時候需要三個參數,就是 R、G、B 三個數值,定義如下:
class Color(object): """ Color Object of RGB """ def __init__(self, r, g, b): self.r = r self.g = g self.b = b
其實對象一般就是這么定義的,初始化方法里面傳入各個參數,然后定義全局變量并賦值這些值。其實挺多常用語言比如 Java、PHP 里面都是這么定義的。但其實這種寫法是比較冗余的,比如 r、g、b 這三個變量一寫就寫了三遍。
好,那么我們初始化一下這個對象,然后打印輸出下,看看什么結果:
color = Color(255, 255, 255) print(color)
結果是什么樣的呢?或許我們也就能看懂一個 Color 吧,別的都沒有什么有效信息,像這樣子:
<__main__.Color object at 0x103436f60>
我們知道,在 Python 里面想要定義某個對象本身的打印輸出結果的時候,需要實現它的 __repr__ 方法,所以我們比如我們添加這么一個方法:
def __repr__(self): return f'{self.__class__.__name__}(r={self.r}, g={self.g}, b={self.b})'
這里使用了 Python 中的 fstring 來實現了 __repr__ 方法,在這里我們構造了一個字符串并返回,字符串中包含了這個 Color 類中的 r、g、b 屬性,這個返回的結果就是 print 的打印結果,我們再重新執行一下,結果就變成這樣子了:
Color(r=255, g=255, b=255)
改完之后,這樣打印的對象就會變成這樣的字符串形式了,感覺看起來清楚多了吧?
再繼續,如果我們要想實現這個對象里面的 __eq__、__lt__ 等各種方法來實現對象之間的比較呢?照樣需要繼續定義成類似這樣子的形式:
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__ 方法,有了這個方法就可以使用比較符來對兩個 Color 對象進行比較了,但這里又把這幾個屬性寫了兩遍。
最后再考慮考慮,如果我要把 JSON 轉成 Color 對象,難道我要讀完 JSON 然后一個個屬性賦值嗎?如果我想把 Color 對象轉化為 JSON,又得把這幾個屬性寫幾遍呢?如果我突然又加了一個屬性比如透明度 a 參數,那么整個類的方法和參數都要修改,這是極其難以擴展的。不知道你能不能忍,反正我不能忍!
如果你用過 Scrapy、Django 等框架,你會發現 Scrapy 里面有一個 Item 的定義,只需要定義一些 Field 就可以了,Django 里面的 Model 也類似這樣,只需要定義其中的幾個字段屬性就可以完成整個類的定義了,非常方便。
說到這里,我們能不能把 Scrapy 或 Django 里面的定義模式直接拿過來呢?能是能,但是沒必要,因為我們還有專門為 Python 面向對象而專門誕生的庫,沒錯,就是 attrs 和 cattrs 這兩個庫。
有了 attrs 庫,我們就可以非常方便地定義各個對象了,另外對于 JSON 的轉化,可以進一步借助 cattrs 這個庫,非常有幫助。
說了這么多,還是沒有介紹這兩個庫的具體用法,下面我們來詳細介紹下。
安裝
安裝這兩個庫非常簡單,使用 pip 就好了,命令如下:
pip3 install attrs cattrs
安裝好了之后我們就可以導入并使用這兩個庫了。
簡介與特性
首先我們來介紹下 attrs 這個庫,其官方的介紹如下:
attrs 是這樣的一個 Python 工具包,它能將你從繁綜復雜的實現上解脫出來,享受編寫 Python 類的快樂。它的目標就是在不減慢你編程速度的前提下,幫助你來編寫簡潔而又正確的代碼。
其實意思就是用了它,定義和實現 Python 類變得更加簡潔和高效。
基本用法
首先明確一點,我們現在是裝了 attrs 和 cattrs 這兩個庫,但是實際導入的時候是使用 attr 和 cattr 這兩個包,是不帶 s 的。
在 attr 這個庫里面有兩個比較常用的組件叫做 attrs 和 attr,前者是主要用來修飾一個自定義類的,后者是定義類里面的一個字段的。有了它們,我們就可以將上文中的定義改寫成下面的樣子:
from attr import attrs, attrib @attrs class 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)
看我們操作的,首先我們導入了剛才所說的兩個組件,然后用 attrs 里面修飾了 Color 這個自定義類,然后用 attrib 來定義一個個屬性,同時可以指定屬性的類型和默認值。最后打印輸出,結果如下:
Color(r=255, g=255, b=255)
怎么樣,達成了一樣的輸出效果!
觀察一下有什么變化,是不是變得更簡潔了?r、g、b 三個屬性都只寫了一次,同時還指定了各個字段的類型和默認值,另外也不需要再定義 __init__ 方法和 __repr__ 方法了,一切都顯得那么簡潔。一個字,爽!
實際上,主要是 attrs 這個修飾符起了作用,然后根據定義的 attrib 屬性自動幫我們實現了 __init__、__repr__、__eq__、__ne__、__lt__、__le__、__gt__、__ge__、__hash__ 這幾個方法。
如使用 attrs 修飾的類定義是這樣子:
from attr import attrs, attrib @attrs class SmartClass(object): a = attrib() b = attrib()
其實就相當于已經實現了這些方法:
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 的話,就可以不用再寫這些冗余又復雜的代碼了。
翻看源碼可以發現,其內部新建了一個 ClassBuilder,通過一些屬性操作來動態添加了上面的這些方法,如果想深入研究,建議可以看下 attrs 庫的源碼。
別名使用
這時候大家可能有個小小的疑問,感覺里面的定義好亂啊,庫名叫做 attrs,包名叫做 attr,然后又導入了 attrs 和 attrib,這太奇怪了。為了幫大家解除疑慮,我們來梳理一下它們的名字。
首先庫的名字就叫做 attrs,這個就是裝 Python 包的時候這么裝就行了。但是庫的名字和導入的包的名字確實是不一樣的,我們用的時候就導入 attr 這個包就行了,里面包含了各種各樣的模塊和組件,這是完全固定的。
好,然后接下來看看 attr 包里面包含了什么,剛才我們引入了 attrs 和 attrib。
首先是 attrs,它主要是用來修飾 class 類的,而 attrib 主要是用來做屬性定義的,這個就記住它們兩個的用法就好了。
翻了一下源代碼,發現其實它還有一些別名:
s = attributes = attrs ib = attr = attrib
也就是說,attrs 可以用 s 或 attributes 來代替,attrib 可以用 attr 或 ib 來代替。
既然是別名,那么上面的類就可以改寫成下面的樣子:
from attr import s, ib @s class 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)
是不是更加簡潔了,當然你也可以把 s 改寫為 attributes,ib 改寫為 attr,隨你怎么用啦。
不過我覺得比較舒服的是 attrs 和 attrib 的搭配,感覺可讀性更好一些,當然這個看個人喜好。
所以總結一下:
·庫名:attrs
·導入包名:attr
·修飾類:s 或 attributes 或 attrs
·定義屬性:ib 或 attr 或 attrib
OK,理清了這幾部分內容,我們繼續往下深入了解它的用法吧。
聲明和比較
在這里我們再聲明一個簡單一點的數據結構,比如叫做 Point,包含 x、y 的坐標,定義如下:
from attr import attrs, attrib @attrs class Point(object): x = attrib() y = attrib()
其中 attrib 里面什么參數都沒有,如果我們要使用的話,參數可以順次指定,也可以根據名字指定,如:
p1 = Point(1, 2) print(p1) p2 = Point(x=1, y=2) print(p2)
其效果都是一樣的,打印輸出結果如下:
Point(x=1, y=2) Point(x=1, y=2)
OK,接下來讓我們再驗證下類之間的比較方法,由于使用了 attrs,相當于我們定義的類已經有了 __eq__、__ne__、__lt__、__le__、__gt__、__ge__ 這幾個方法,所以我們可以直接使用比較符來對類和類之間進行比較,下面我們用實例來感受一下:
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))
運行結果如下:
Same: False Equal: True Not Equal(ne): True Less Than(lt): True Less or Equal(le): True True Greater Than(gt): True True Greater or Equal(ge): True
可能有的朋友不知道 ne、lt、le 什么的是什么意思,不過看到這里你應該明白啦,ne 就是 Not Equal 的意思,就是不相等,le 就是 Less or Equal 的意思,就是小于或等于。
其內部怎么實現的呢,就是把類的各個屬性轉成元組來比較了,比如 Point(1, 2) < Point(3, 4) 實際上就是比較了 (1, 2) 和 (3, 4) 兩個元組,那么元組之間的比較邏輯又是怎樣的呢,這里就不展開了,如果不明白的話可以參考官方文檔:https://docs.python.org/3/library/stdtypes.html#comparisons。
屬性定義
現在看來,對于這個類的定義莫過于每個屬性的定義了,也就是 attrib 的定義。對于 attrib 的定義,我們可以傳入各種參數,不同的參數對于這個類的定義有非常大的影響。
下面我們就來詳細了解一下每個屬性的具體參數和用法吧。
首先讓我們概覽一下總共可能有多少可以控制一個屬性的參數,我們用 attrs 里面的 fields 方法可以查看一下:
from attr import attrs, attrib, fields @attrs class Point(object): x = attrib() y = attrib() print(fields(Point))
這就可以輸出 Point 的所有屬性和對應的參數,結果如下:
(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))
輸出出來了,可以看到結果是一個元組,元組每一個元素都其實是一個 Attribute 對象,包含了各個參數,下面詳細解釋下幾個參數的含義:
·name:屬性的名字,是一個字符串類型。
·default:屬性的默認值,如果沒有傳入初始化數據,那么就會使用默認值。如果沒有默認值定義,那么就是 NOTHING,即沒有默認值。
·validator:驗證器,檢查傳入的參數是否合法。
·init:是否參與初始化,如果為 False,那么這個參數不能當做類的初始化參數,默認是 True。
·metadata:元數據,只讀性的附加數據。
·type:類型,比如 int、str 等各種類型,默認為 None。
·converter:轉換器,進行一些值的處理和轉換器,增加容錯性。
·kw_only:是否為強制關鍵字參數,默認為 False。
屬性名
對于屬性名,非常清楚了,我們定義什么屬性,屬性名就是什么,例如上面的例子,定義了:
x = attrib()
那么其屬性名就是 x。
默認值
對于默認值,如果在初始化的時候沒有指定,那么就會默認使用默認值進行初始化,我們看下面的一個實例:
from attr import attrs, attrib, fields @attrs class Point(object): x = attrib() y = attrib(default=100) if __name__ == '__main__': print(Point(x=1, y=3)) print(Point(x=1))
在這里我們將 y 屬性的默認值設置為了 100,在初始化的時候,第一次都傳入了 x、y 兩個參數,第二次只傳入了 x 這個參數,看下運行結果:
Point(x=1, y=3) Point(x=1, y=100)
可以看到結果,當設置了默認參數的屬性沒有被傳入值時,他就會使用設置的默認值進行初始化。
那假如沒有設置默認值但是也沒有初始化呢?比如執行下:
Point()
那么就會報錯了,錯誤如下:
TypeError: __init__() missing 1 required positional argument: 'x'
所以說,如果一個屬性,我們一旦沒有設置默認值同時沒有傳入的話,就會引起錯誤。所以,一般來說,為了穩妥起見,設置一個默認值比較好,即使是 None 也可以的。
初始化
如果一個類的某些屬性不想參與初始化,比如想直接設置一個初始值,一直固定不變,我們可以將屬性的 init 參數設置為 False,看一個實例:
from attr import attrs, attrib @attrs class Point(object): x = attrib(init=False, default=10) y = attrib() if __name__ == '__main__': print(Point(3))
比如 x 我們只想在初始化的時候設置固定值,不想初始化的時候被改變和設定,我們將其設置了 init 參數為 False,同時設置了一個默認值,如果不設置默認值,默認為 NOTHING。然后初始化的時候我們只傳入了一個值,其實也就是為 y 這個屬性賦值。
這樣的話,看下運行結果:
Point(x=10, y=3)
沒什么問題,y 被賦值為了我們設置的值 3。
那假如我們非要設置 x 呢?會發生什么,比如改寫成這樣子:
Point(1, 2)
報錯了,錯誤如下:
TypeError: __init__() takes 2 positional arguments but 3 were given
參數過多,也就是說,已經將 init 設置為 False 的屬性就不再被算作可以被初始化的屬性了。
強制關鍵字
強制關鍵字是 Python 里面的一個特性,在傳入的時候必須使用關鍵字的名字來傳入,如果不太理解可以再了解下 Python 的基礎。
設置了強制關鍵字參數的屬性必須要放在后面,其后面不能再有非強制關鍵字參數的屬性,否則會報這樣的錯誤:
ValueError: Non keyword-only attributes are not allowed after a keyword-only attribute (unless they are init=False)
好,我們來看一個例子,我們將最后一個屬性設置 kw_only 參數為 True:
from attr import attrs, attrib, fields @attrs class Point(object): x = attrib(default=0) y = attrib(kw_only=True) if __name__ == '__main__': print(Point(1, y=3))
如果設置了 kw_only 參數為 True,那么在初始化的時候必須傳入關鍵字的名字,這里就必須指定 y 這個名字,運行結果如下:
Point(x=1, y=3)
如果沒有指定 y 這個名字,像這樣調用:
Point(1, 3)
那么就會報錯:
TypeError: __init__() takes from 1 to 2 positional arguments but 3 were given
所以,這個參數就是設置初始化傳參必須要用名字來傳,否則會出現錯誤。
注意,如果我們將一個屬性設置了 init 為 False,那么 kw_only 這個參數會被忽略。
驗證器
有時候在設置一個屬性的時候必須要滿足某個條件,比如性別必須要是男或者女,否則就不合法。對于這種情況,我們就需要有條件來控制某些屬性不能為非法值。
下面我們看一個實例:
from attr import attrs, attrib def is_valid_gender(instance, attribute, value): if value not in ['male', 'female']: raise ValueError(f'gender {value} is not valid') @attrs class 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'))
在這里我們定義了一個驗證器 Validator 方法,叫做 is_valid_gender。然后定義了一個類 Person 還有它的兩個屬性 name 和 gender,其中 gender 定義的時候傳入了一個參數 validator,其值就是我們定義的 Validator 方法。
這個 Validator 定義的時候有幾個固定的參數:
·instance:類對象
·attribute:屬性名
·value:屬性值
這是三個參數是固定的,在類初始化的時候,其內部會將這三個參數傳遞給這個 Validator,因此 Validator 里面就可以接受到這三個值,然后進行判斷即可。在 Validator 里面,我們判斷如果不是男性或女性,那么就直接拋出錯誤。
下面做了兩個實驗,一個就是正常傳入 male,另一個寫錯了,寫的是 mlae,觀察下運行結果:
Person(name='Mike', gender='male') TypeError: __init__() missing 1 required positional argument: 'gender'
OK,結果顯而易見了,第二個報錯了,因為其值不是正常的性別,所以程序直接報錯終止。
注意在 Validator 里面返回 True 或 False 是沒用的,錯誤的值還會被照常復制。所以,一定要在 Validator 里面 raise 某個錯誤。
另外 attrs 庫里面還給我們內置了好多 Validator,比如判斷類型,這里我們再增加一個屬性 age,必須為 int 類型:
age = attrib(validator=validators.instance_of(int))
這時候初始化的時候就必須傳入 int 類型,如果為其他類型,則直接拋錯:
TypeError: ("'age' must be <class 'int'> (got 'x' that is a <class 'str'>).
另外還有其他的一些 Validator,比如與或運算、可執行判斷、可迭代判斷等等,可以參考官方文檔:https://www.attrs.org/en/stable/api.html#validators。
另外 validator 參數還支持多個 Validator,比如我們要設置既要是數字,又要小于 100,那么可以把幾個 Validator 放到一個列表里面并傳入:
from attr import attrs, attrib, validators def is_less_than_100(instance, attribute, value): if value > 100: raise ValueError(f'age {value} must less than 100') @attrs class 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))
這樣就會將所有的 Validator 都執行一遍,必須每個 Validator 都滿足才可以。這里 age 傳入了 500,那么不符合第二個 Validator,直接拋錯:
ValueError: age 500 must less than 100
轉換器
其實很多時候我們會不小心傳入一些形式不太標準的結果,比如本來是 int 類型的 100,我們傳入了字符串類型的 100,那這時候直接拋錯應該不好吧,所以我們可以設置一些轉換器來增強容錯機制,比如將字符串自動轉為數字等等,看一個實例:
from attr import attrs, attrib def to_int(value): try: return int(value) except: return None @attrs class Point(object): x = attrib(converter=to_int) y = attrib() if __name__ == '__main__': print(Point('100', 3))
看這里,我們定義了一個方法,可以將值轉化為數字類型,如果不能轉,那么就返回 None,這樣保證了任何可以被轉數字的值都被轉為數字,否則就留空,容錯性非常高。
運行結果如下:
Point(x=100, y=3)
類型
為什么把這個放到最后來講呢,因為 Python 中的類型是非常復雜的,有原生類型,有 typing 類型,有自定義類的類型。
首先我們來看看原生類型是怎樣的,這個很容易理解了,就是普通的 int、float、str 等類型,其定義如下:
from attr import attrs, attrib @attrs class Point(object): x = attrib(type=int) y = attrib() if __name__ == '__main__': print(Point(100, 3)) print(Point('100', 3))
這里我們將 x 屬性定義為 int 類型了,初始化的時候傳入了數值型 100 和字符串型 100,結果如下:
Point(x=100, y=3) Point(x='100', y=3)
但我們發現,雖然定義了,但是不會被自動轉類型的。
另外我們還可以自定義 typing 里面的類型,比如 List,另外 attrs 里面也提供了類型的定義:
from attr import attrs, attrib, Factory import typing @attrs class Point(object): x = attrib(type=int) y = attrib(type=typing.List[int]) z = attrib(type=Factory(list))
這里我們引入了 typing 這個包,定義了 y 為 int 數字組成的列表,z 使用了 attrs 里面定義的 Factory 定義了同樣為列表類型。
另外我們也可以進行類型的嵌套,比如像這樣子:
from attr import attrs, attrib, Factory import typing @attrs class Point(object): x = attrib(type=int, default=0) y = attrib(type=int, default=0) @attrs class 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 類代表離散點,隨后定義了線,其擁有 points 屬性是 Point 組成的列表。在初始化的時候我們聲明了五個點,然后用這五個點組成的列表聲明了一條線,邏輯沒什么問題。
運行結果:
[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)])
可以看到這里我們得到了一個嵌套類型的 Line 對象,其值是 Point 類型組成的列表。
以上便是一些屬性的定義,把握好這些屬性的定義,我們就可以非常方便地定義一個類了。
序列轉換
在很多情況下,我們經常會遇到 JSON 等字符串序列和對象互相轉換的需求,尤其是在寫 REST API、數據庫交互的時候。
attrs 庫的存在讓我們可以非常方便地定義 Python 類,但是它對于序列字符串的轉換功能還是比較薄弱的,cattrs 這個庫就是用來彌補這個缺陷的,下面我們再來看看 cattrs 這個庫。
cattrs 導入的時候名字也不太一樣,叫做 cattr,它里面提供了兩個主要的方法,叫做 structure 和 unstructure,兩個方法是相反的,對于類的序列化和反序列化支持非常好。
基本轉換
首先我們來看看基本的轉換方法的用法,看一個基本的轉換實例:
from attr import attrs, attrib from cattr import unstructure, structure @attrs class 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)
在這里我們定義了一個 Point 對象,然后調用 unstructure 方法即可直接轉換為 JSON 字符串。如果我們再想把它轉回來,那就需要調用 structure 方法,這樣就成功轉回了一個 Point 對象。
看下運行結果:
json: {'x': 1, 'y': 2} obj: Point(x=1, y=2)
當然這種基本的來回轉用的多了就輕車熟路了。
多類型轉換
另外 structure 也支持一些其他的類型轉換,看下實例:
>>> 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 這個模塊,后面我會寫內容詳細介紹這個庫的用法。
不過總的來說,大部分情況下,JSON 和對象的互轉是用的最多的。
屬性處理
上面的例子都是理想情況下使用的,但在實際情況下,很容易遇到 JSON 和對象不對應的情況,比如 JSON 多個字段,或者對象多個字段。
我們先看看下面的例子:
from attr import attrs, attrib from cattr import structure @attrs class 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 多了一個字段 z,而 Point 類只有 x、y 兩個字段,那么直接執行 structure 會出現什么情況呢?
TypeError: __init__() got an unexpected keyword argument 'z'
不出所料,報錯了。意思是多了一個參數,這個參數并沒有被定義。
這時候一般的解決方法的直接忽略這個參數,可以重寫一下 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)
這里定義了一個 drop_nonattrs 方法,用于從 JSON 里面刪除對象里面不存在的屬性,然后調用新的 structure 方法即可,寫法如下:
from typing import Set from attr import attrs, attrib import cattr @attrs class 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 字段冗余導致的轉換問題了。
另外還有一個常見的問題,那就是數據對象轉換,比如對于時間來說,在對象里面聲明我們一般會聲明為 datetime 類型,但在序列化的時候卻需要序列化為字符串。
所以,對于一些特殊類型的屬性,我們往往需要進行特殊處理,這時候就需要我們針對某種特定的類型定義特定的 hook 處理方法,這里就需要用到 register_unstructure_hook 和 register_structure_hook 方法了。
下面這個例子是時間 datetime 轉換的時候進行的處理:
import datetime from attr import attrs, attrib import cattr TIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ' @attrs class 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 這個類型注冊了兩個 hook,當序列化的時候,就調用 strftime 方法轉回字符串,當反序列化的時候,就調用 strptime 將其轉回 datetime 類型。
看下運行結果:
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))
這樣對于一些特殊類型的屬性處理也得心應手了。
嵌套處理
最后我們再來看看嵌套類型的處理,比如類里面有個屬性是另一個類的類型,如果遇到這種嵌套類的話,怎樣類轉轉換呢?我們用一個實例感受下:
from attr import attrs, attrib from typing import List from cattr import structure, unstructure @attrs class Point(object): x = attrib(type=int, default=0) y = attrib(type=int, default=0) @attrs class Color(object): r = attrib(default=0) g = attrib(default=0) b = attrib(default=0) @attrs class 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)
這里我們定義了兩個 Class,一個是 Point,一個是 Color,然后定義了 Line 對象,其屬性類型一個是 Color 類型,一個是 Point 類型組成的列表,下面我們進行序列化和反序列化操作,轉成 JSON 然后再由 JSON 轉回來,運行結果如下:
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)])
可以看到,我們非常方便地將對象轉化為了 JSON 對象,然后也非常方便地轉回了對象。
這樣我們就成功實現了嵌套對象的序列化和反序列化,所有問題成功解決!
以上是Python是如何實現面向對象編程的的所有內容,感謝各位的閱讀!希望分享的內容對大家有幫助,更多相關知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。