實在是由於python中對象方面的內容太多、太亂、太雜,在寫相關文章時比我所學過的幾種語言都更讓人"糟心",不少內容似獨立內容、又似相關內容,放這也可、放那也可、放這也很差、放那也很差。python
因此,用一篇單獨的文章來收集那些在我其它文章中很差歸類的知識點,並且會隨時更新。redis
在python 3.x中,類就是類型,類型就是類,它們變得徹底等價。算法
要理解class、type、object的關係,只需幾句話:緩存
論證略,網上一大堆。安全
Duck typing的概念來源於的詩句"When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck."。app
意思是:若是我看到一隻鳥走路像一隻鴨子,游泳像一隻鴨子,叫起來像一隻鴨子,那麼我就認爲這隻鳥是一隻鴨子。ide
在python中,鴨子模型很是容易理解。下面是典型的鴨子模型示例:函數
class Duck(): def walk(self): print("duck walk") def swim(self): print("duck swim") def quacks(self): print("duck quacks") class Bird(): def walk(self): print("bird walk") def swim(self): print("bird swim") def quacks(self): print("bird quacks")
對於Python來講,鴨子模型的意思是:只要某個地方須要調用Duck的walk、swim、quacks方法,就可讓Bird也做爲Duck,由於它也實現了這3個方法。編碼
Python並不強制檢查類型,只要對象實現了某個所須要的方法,就認爲這是能夠接受的對象。線程
它還傳達一種思想,A類對象能放在一個地方,若是想讓B類對象也能夠放在這個地方,只須要讓B實現這個地方所須要的方法就能夠。
鴨子模型貫穿了python中的運算符重載行爲,也貫穿了整個Python的類設計理念。例如print()執行的時候須要調用__str__
方法,因此只要實現了__str__
方法的類,均可以被print()調用。
綁定之意,在於方法是否與實例對象(或類名)進行了綁定。
當經過實例對象去調用方法時,或者說會自動傳遞self的方法是綁定方法,其它經過類名調用、手動傳遞self的方法調用是非綁定方法,在3.x中沒有非綁定方法的概念,它直接被看成是普通函數。
例如:
class cls(): def m1(self): print("m1: ", self) def m2(arg1): print("m2: ", arg1)
當經過cls類的實例對象去調用m一、m2的時候,是綁定方法:
>>> c = cls() >>> c.m1 <bound method cls.m1 of <__main__.cls object at 0x000001EE2DA75860>> >>> c.m1() m1: <__main__.cls object at 0x000001EE2DA75860> >>> c.m2 <bound method cls.m2 of <__main__.cls object at 0x000001EE2DA75860>> >>> c.m2() m2: <__main__.cls object at 0x000001EE2DA75860>
也就是說,綁定方法中是綁定了實例對象的,無需手動去傳遞實例對象。例如:
>>> cc = c.m1 >>> cc() m1: <__main__.cls object at 0x000001EE2DA75860>
當經過類名去訪問的時候,是普通函數(非綁定方法):
>>> cls.m1 <function cls.m1 at 0x000001EE2DA78620> >>> cls.m2 <function cls.m2 at 0x000001EE2DA786A8> >>> cls.m1(c) m1: <__main__.cls object at 0x000001EE2DA75860> >>> cls.m2(c) m2: <__main__.cls object at 0x000001EE2DA75860>
惟一須要在乎的是,並不是必定要經過實例對象去調用方法,經過類方法也能的調用,也能手動傳遞實例對象。此外,類中的方法並不是必定要求有self參數。
python的面向對象中有3種類型的方法:普通的實例方法、類方法、靜態方法。
從這些方法的簡單定義上看,很容易知曉實例方法能夠操做類屬性、對象屬性,而類方法和靜態方法只能操做類屬性,不能操做對象屬性。
因此,要實現類方法、靜態方法須要合理地定義、傳遞參數。例如:
class cls(): def m1(self, arg1): print("m1: ", self, arg1) def m2(arg1, arg2): print("m2: ", arg1)
顯然這裏m2()是靜態方法,m1根據調用方式能夠是類方法,也能夠是實例方法,甚至是靜態方法。例如:
# m1做爲實例方法 >>> c.m1("hello") m1: <__main__.cls object at 0x000001EE2DA75BA8> hello # m1做爲類方法,經過類名調用,並傳遞類名做爲self參數 >>> cls.m1(cls,"hello") m1: <class '__main__.cls'> hello # m1做爲靜態方法,經過類名調用,隨意處置self參數 >>> cls.m1("asdfas","hello") m1: asdfas hello
這樣的調用方式並無什麼問題,python是容許這樣作的,很自由,但很容易犯錯。好比想要經過對象名去調用上面的m2,arg1就必須看成self同樣解釋成對象自身,換句話說只能傳遞一個參數c.m2("arg2")
,這顯然有悖靜態方法的編碼方式。
在python中,要定義嚴格的類方法、靜態方法,須要使用內置的裝飾器函數classmethod()、staticmethod()來裝飾,裝飾後不管使用對象名去調用仍是使用類名去調用,均可以。
例如:
class cls(): def m1(self,arg1): print("m1: ", self, arg1) @classmethod def m2(self,arg1): print("m2: ", self, arg1) @staticmethod def m3(arg1, arg2): print("m3: ", arg1, arg2)
上面定義了普通方法、類方法和靜態方法。若是尚不瞭解裝飾器的用法,暫時只需知道上面的@xxx
將它下面的函數(方法)擴展成了類方法、靜態方法便可。
調用實例方法:
>>> c = cls() >>> c.m1("hello") m1: <__main__.cls object at 0x000001EE2DA840B8> hello
注意輸出的self是"...object...",和下面的類方法調用注意區分比較。
調用類方法。由於@classmethod
已經將m2包裝成了類方法,因此m2的第一個self參數將老是表明類名,而不管是使用對象去調用m2仍是使用類名去調用m2。
>>> c.m2("hello") m2: <class '__main__.cls'> hello >>> cls.m2("hello") m2: <class '__main__.cls'> hello
若是輸出m2方法,會發現它已是綁定方法,也就是說和類名進行了綁定(這裏不是和對象名進行綁定)。
>>> c.m2 <bound method cls.m2 of <class '__main__.cls'>> >>> cls.m2 <bound method cls.m2 of <class '__main__.cls'>>
調用靜態方法。
>>> c.m3("hello","world") m3: hello world >>> cls.m3("hello","world") m3: hello world
靜態方法都是未綁定的函數:
>>> c.m3 <function cls.m3 at 0x000001EE2DA789D8> >>> cls.m3 <function cls.m3 at 0x000001EE2DA789D8>
通常來講,類方法用於在類中操做/返回和類名有關的內容,靜態方法用於在類中作和類或對象徹底無關的操做。一個比較好理解的例子是,一個Employee類,要檢查員工的年齡範圍在16-35,若是年齡在這範圍內,就返回一個員工對象,能夠將這個邏輯定義爲類方法。若是隻是檢查年齡範圍來決定True或False這樣和類/對象無關的操做,則定義爲靜態方法。
class Employee: @staticmethod def age_ok(age): if 16<age<35: return True else: return False @classmethod def age_check(cls, age): if 16<age<35: return cls(...)
python沒有private關鍵字來修飾屬性使其變成私有屬性,可是帶上雙下劃線前綴的屬性且沒有後綴下劃線的屬性(__X
)能夠認爲是私有屬性。它僅僅只是約定性的私有屬性,不表明外界真的不能訪問。
實際上,使用__X
這樣的屬性,在類的內部訪問時會自動進行擴展爲_clsname__X
,也就是加個前綴下劃線,再加個類名。由於擴展時加上了類名,使得這個屬性在名稱上是獨屬於這個類的。
例如:
class cls(): __X = 12 def m1(self,y): self.__Y = y print(self.__X) print(self.__Y) >>> print(cls.__dict__.keys()) dict_keys([..., '_cls__X', 'm1', ....]) >>> c = cls() >>> c.m1(22) 12 22 >>> print(c.__dict__.keys()) dict_keys(['_cls__Y'])
由於已經擴展了屬性的名稱,因此沒法在類的外界經過直接的名稱__X
去訪問對應的屬性。
>>> c.__Y Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'cls' object has no attribute '__Y' >>> c.__X Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'cls' object has no attribute '__X' >>> cls.__X Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: type object 'cls' has no attribute '__X'
前面說了,這種加雙下劃線前綴的屬性僅僅只是一個約定形式,雖然在外界沒法直接經過名稱去訪問,可是仍有很多方法去訪問。例如經過擴展後的名稱、經過字典__dict__
:
>>> cls._cls__X 12 >>> c._cls__Y 22 >>> c.__dict__['_cls__Y'] 22
要想嚴格地聲明屬性的私有性,能夠編寫裝飾器類,在裝飾器類中完成屬性的判斷。
若是一個方法的參數給了默認參數,且這個默認參數是一個可變類型,那麼這裏有一個陷阱:使用這個默認參數的時候各對象會共享這個可變默認值。
例如:
class A: def __init__(self, arg=[]): self.data = arg def add(self, value): self.data.append(value) # 兩個不一樣對象,且都使用參數arg的默認值 a1 = A() a2 = A() # 向兩個對象中添加元素 a1.add("a1") a2.add("a2") print(a1.data) print(a2.data)
執行結果:
['a1', 'a2'] ['a1', 'a2']
發現a1和a2這兩個不一樣的對象中的data居然是相同的數據,若是輸出下它們的data屬性,會發現是同一個對象:
>>> a1.data is a2.data True
這是由於參數的默認值是在申請變量以前就先評估好的,也就是在賦值給參數變量arg以前,這個空列表就已經存在了。而後使用默認值來構造對象時,這些對象都使用同一個空列表,而這個空列表是可變的類型,因此不管誰修改這個列表都會影響其它對象。
若是不使用默認值,那麼每一個對象的列表就是獨佔的,不會被其它對象修改。
a3 = A([]) a3.add("a3") print(a3.data)
結果:
['a3']
python的types模塊中提供了一個MethodType(funcName, instance)
函數,它能夠將類外部定義的函數funcName連接到實例對象或類上。
例如鏈接到實例對象上:
# 注意外部函數上加了self參數 def func(self, age): print(age) class cls: pass >>> c = cls >>> import types >>> c.printage = types.MethodType(func, c) >>> c.printage(22) 22
type.MethodType()是將某個可調用對象(這裏的func)動態地連接到實例對象或類上,使其臨時做爲對象或類的方法屬性,只有在被調用的時候纔會進行屬性的添加。
須要注意的是,當外部函數連接到實例對象上時,這個連接只對這個實例對象有效,其它對象是不具有這個屬性的。若是連接到類上,那麼全部對象均可以訪問這個連接的方法。
正常狀況下定義了一個類,調用這個類表示建立一個對象。
class cls: pass c = cls()
可是,對象c再也不是可調用的對象,也就是說,它不能再被執行。
>>> callable(c) False
python對象的__call__
可讓實例對象也變成可調用類型,就像函數同樣。
class cls: def __call__(self, *args, **kwargs): print('__call__: ', args, kwargs) >>> c = cls() >>> c(1,2,3,x=4,y=5) __call__: (1, 2, 3) {'x': 4, 'y': 5} >>> callable(c) True
將類定義爲一個可調用對象是很是有用的,它能夠像函數同樣去修飾、擴展其它內容的功能,特別是編寫裝飾器類的時候。
例如,正常狀況下寫裝飾器總要返回一個新裝飾器函數,可是想要直接使用類做爲裝飾器,就須要在這個類中定義__call__
,將__call__
做爲函數裝飾器中的裝飾器函數wrapped()。下面是一個示例:
import types from functools import wraps class DecoratorClass(): def __init__(self, func): wraps(func)(self) self.callcount = 0 def __call__(self, *args, **kwargs): self.callcount += 1 return self.__wrapped__(*args, **kwargs) def __get__(self, instance, cls): if instance is None: return self else: return types.MethodType(self, instance)
上面是裝飾器類,能夠像函數裝飾器同樣去裝飾其它函數。
@DecoratorClass def add(x, y): return x + y >>> add(2,3) 5 >>> add(3,4) 7 >>> add.callcount 2
根據前面的說明可知,判斷一個對象是不是可調用的依據有2種方式:
__call__
方法。使用hasattr(obj,'__call__')
便可判斷>>> callable(c) True >>> hasattr(c,'__call__') True
python是一門動態語言,並且是極其開放的動態語言。在面向對象上,它容許咱們隨意地、任意時間地添加屬性。例如:
class cls(): attr1 = 111 # 在類中添加屬性 def __init__(self): self.attr2 = 222 # 添加實例對象的屬性 >>> c = cls() >>> c.attr3 = 333 # 在類的外部添加屬性 >>> c.__dict__.keys() dict_keys(['attr2', 'attr3'])
若是想要限定對象只能擁有某些屬性,可使用__slots__
來限定,__slots__
能夠指定爲一個元組、列表、集合等。
例如:
class cls(): __slots__ = ['a', 'b'] >>> c = cls() >>> c.a=13 >>> c.b=14 >>> c.cc=15 # 報錯 Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'cls' object has no attribute 'cc'
但注意:
__slots__
定義在類級別上,它僅僅只限定實例對象屬性,不會限制類屬性__slots__
不會被子類繼承__slots__
定義後,對象默認就沒有了__dict__
屬性
__dict__
放進__slots__
的範圍內來容許__dict__
例如:
class cls(): __slots__ = ['a', 'b'] x = 13 # 容許定義類屬性 c = cls() c.a = 14 c.b = 15 cls.y = 16 # 容許定義類屬性 print(c.x, c.a, c.b, c.y) print(cls.__dict__.keys()) # 類有__dict__屬性 print(c.__dict__.keys()) # 報錯,對象沒有__dict__屬性
能夠將__dict__
放進__slots__
中,使得對象能夠帶有屬性字典。但這會讓__slots__
的限定失效:實例對象能夠繼續添加任意屬性,那些不在__slots__
中的屬性會加入到__dict__
中。
class cls1(): __slots__ = ['a', 'b', '__dict__'] cc = cls1() cc.a = 14 cc.b = 15 cc.c = 16 cc.d = 17 print(cc.__slots__) print(cc.__dict__.keys())
輸出結果:
['a', 'b', '__dict__'] dict_keys(['c', 'd'])
由於子類不會繼承父類的__slots__
,因此若是父類中沒有定義__slots__
的話,由於子類能夠訪問父類的__dict__
,這會使得子類自身定義的__slots__
的屬性限定功能失效。
class cls1(): pass class cls2(cls1): __slots__ = ['a', 'b'] ccc = cls2() ccc.a=13 ccc.b=14 ccc.ddd=15 print(ccc.__slots__) print(ccc.__dict__.keys())
結果:
['a', 'b'] dict_keys(['ddd'])
python支持多重繼承,只需將須要繼承的父類放進子類定義的括號中便可。
class cls1(): ... class cls2(): ... class cls3(cls1,cls2): ...
上面cls3繼承了cls1和cls2,它的名稱空間將鏈接到兩個父類名稱空間,也就是說只要cls1或cls2擁有的屬性,cls3構造的對象就擁有(注意,cls3類是不擁有的,只有cls3類的對象才擁有)。
但多重繼承時,若是cls1和cls2都具備同一個屬性,好比cls1.x和cls2.x,那麼cls3的對象c3.x取哪個?會取cls1中的屬性x,由於規則是按照(括號中)從左向右的方式搜索父類。
再考慮一個問題,若是cls1中沒有屬性x,但它繼承自cls0,而cls0有x屬性,那麼,c3.x取哪一個屬性。
這在python 3.x中是一個比較複雜的問題,它根據MRO(Method Resolution Order)算法來決定多重繼承時的訪問順序,這個算法的規則能夠總結爲:先左後右,先深度再廣度,但必須遵循共同的超類最後搜索。
下面是一個訪問順序圖示:
每一個類都有一個__mro__
屬性,這個屬性是一個元組,從左向右的元素順序表明的是屬性搜索順序。
class D(): pass class C(D): pass class B(D): pass class A(B, C): pass >>> A.__mro__ (<class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.D'>, <class 'object'>) >>> D.__mro__ (<class '__main__.D'>, <class 'object'>)
不只多重繼承時是按照MRO順序進行屬性搜索的,super()引用的時候也同樣是按照mro算法來引用屬性的,因此super並不必定老是引用父類屬性。
例如:
class D(): def __init__(self): print("D") class C(D): def __init__(self): print("C") super().__init__() class B(D): def __init__(self): print("B") super().__init__() # 調用的不是父類D的構造方法 class A(B, C): def __init__(self): print("A") super().__init__() a = A()
輸出結果爲:
A B C D
面向對象中,通常不推薦使用多重繼承,由於很容易出現屬性引用混亂的問題,並且有些面向對象的語言根本就不支持多重繼承。但在Python中,使用多重繼承的狀況也很是多,若是真的要使用多重繼承,必定要設計好類。一種更好的方式是使用Mixin類,見下文。
Mixin的wiki頁:https://en.wikipedia.org/wiki/Mixin
對於那些想要從多個類中繼承的方法,若是想要避免多重繼承可能引發的屬性混亂,能夠將這些方法單獨編寫到一個類中,而這個功能/方法相對單一的類稱爲Mixin類。
Mixin類經過特殊的多重繼承方法來擴展主類的功能,卻又很安全,不會出現多重繼承時屬性混亂的問題。
例如:
class Mixin1(): def test1(self): print("test1 method provided by Mixin1") class Mixin2(): def test2(self): print("test2 method provided by Mixin1") class Base(): def mymethod(self): print("mymethod is the base method") class Myclass(Mixin1, Mixin2, Base): pass
上面的Mixin1和Mixin2是Mixin類,它們都只有一個方法,功能很是單一,它們能夠看做是Base類的功能擴充類,也能夠認爲Mixin類是主類Include的類。
例如wiki頁中給的一個例子,class TCPServer
中提供了UDP/TCP server的功能,這時每一個鏈接都經過一個相同的進程進行處理。可是能夠將class ThreadingMixIn
經過Mixin的方法對TCPServer進行擴充:
class ThreadingTCPServer(ThreadMixIn, TCPServer): pass
這至關於將ThreadingMixIn類中的方法添加到了TCPServer類中,使得每一個新鏈接都會建立一個新的線程,這個功能是ThreadMixIn提供的,但看上去做用在TCPServer上。
關於Mixin類,有幾個編碼規範須要遵照:
抽象類是指:這個類的子類必須重寫這個類中的方法,且這個類無法進行實例化產生對象。
先說明在Python中如何定義抽象類。Python中的abc模塊(Abstract Base Classes)專門用來實現抽象類、接口。
例如,在設計某個程序的緩存接口時,想要讓它將來既可使用普通的cache,也可使用redis緩存。那麼只須要定義一個抽象的類Cache,裏面實現兩個抽象方法get()和set(),之後不管使用普通的cache仍是redis緩存,都只需讓這兩種緩存類型實現且必須實現get()和set()便可。
import abc class Cache(metaclass=abc.ABCMeta): @abc.abstractmethod def get(self, key): pass @abc.abstractmethod def get(self, key, value): pass # 子類繼承時,必須實現這兩個方法 class CacheBase(Cache): def get(self, key): pass def set(self, key, value): pass class Redis(Cache): def get(self, key): pass def set(self, key, value): pass
若是子類沒有實現或者少實現了抽象類中的方法,在構造子類實例化對象的時候就會當即報錯。
在Python中大多數時候不建議直接定義抽象類,這可能會形成過分封裝/過分抽象的問題。若是想要讓子類必須實現父類的某個方法,能夠在父類方法中加上raise來拋出異常NotImplementedError,這時若是子類對象沒有實現該方法,就會查找到父類的這個方法,從而拋出異常。
class Cache(): def get(self, key): raise NotImplementedError("must define get method") def set(self, key): raise NotImplementedError("must define set method")
使用raise NotImplementedError的方式來模擬抽象類,它只有在調用到set/get的時候纔會拋異常,在實例化對象的時候或者沒有調用到這兩個方法的時候不會報錯。