Python面向對象:雜七雜八的知識點

爲什麼有這篇"雜項"文章
實在是因爲python中對象方面的內容太多、太亂、太雜,在寫相關文章時比我所學過的幾種語言都更讓人"糟心",很多內容似獨立內容、又似相關內容,放這也可、放那也可、放這也不好、放那也不好。

所以,用一篇單獨的文章來收集那些在我其它文章中不好歸類的知識點,而且會隨時更新。

class、type、object的關係

在python 3.x中,類就是類型,類型就是類,它們變得完全等價。

要理解class、type、object的關係,只需幾句話:

  • object是所有類的祖先類,包括type類也繼承自object
  • 所有class自身也是對象,所有類/類型都是type的實例對象,包括object和type自身都是type的實例對象
    論證略,網上一大堆。

鴨子模型(duck typing)

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."。

意思是:如果我看到一隻鳥走路像一隻鴨子,游泳像一隻鴨子,叫起來像一隻鴨子,那麼我就認爲這隻鳥是一隻鴨子。

在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類的實例對象去調用m1、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種類型的方法:普通的實例方法、類方法、靜態方法。

  • 普通實例方法:通過self參數傳遞實例對象自身
  • 類方法:傳遞的是類名而非對象
  • 靜態方法:不通過self傳遞.

從這些方法的簡單定義上看,很容易知曉實例方法可以操作類屬性、對象屬性,而類方法和靜態方法只能操作類屬性,不能操作對象屬性。

所以,要實現類方法、靜態方法需要合理地定義、傳遞參數。例如:

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']

MethodType:添加外部函數作爲方法

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)動態地鏈接到實例對象或類上,使其臨時作爲對象或類的方法屬性,只有在被調用的時候纔會進行屬性的添加。

需要注意的是,當外部函數鏈接到實例對象上時,這個鏈接只對這個實例對象有效,其它對象是不具備這個屬性的。如果鏈接到類上,那麼所有對象都可以訪問這個鏈接的方法。

call

正常情況下定義了一個類,調用這個類表示創建一個對象。

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種方式:

使用內置函數callable(X),X可調用則返回True,否則False
注:返回False一定表示不可調用,但返回True不代表一定可調用
判斷是否定義了call__方法。使用hasattr(obj,'__call')即可判斷

>>> callable(c)
True

>>> hasattr(c,'__call__')
True
__slots__

python是一門動態語言,而且是極其開放的動態語言。在面向對象上,它允許我們隨意地、任意時間地添加屬性。例如:

# 小編創建了一個Python學習交流QQ羣:857662006 
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__的屬性限定功能失效。

# 小編創建了一個Python學習交流QQ羣:857662006 
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'])

多重繼承和__ mro__和super()

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

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類,有幾個編碼規範需要遵守:

  • 類名使用Mixin結尾,例如ListMixin、AbcMixin
  • 多重繼承時Mixin類放在主類的前面,或者說主類放在最後面,避免主類有和Mixin類中重名函數而使得Mixin類失效
  • 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的時候纔會拋異常,在實例化對象的時候或者沒有調用到這兩個方法的時候不會報錯。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章