python 類篇

金字塔下的蜗牛發表於2024-10-10

目錄
    • 2. 類
      • 例項屬性和類屬性
      • 建構函式和self引數
      • 類方法
      • 靜態方法
      • 共有私有[待補充]
    • 3.繼承
      • 單繼承多繼承
      • super函式
      • MRO列表
      • super的本質
      • 經典類和新式類
    • 4. 多型
      • C++ 多型的定義
      • python多型
      • 鴨子型別
    • 5. 虛擬函式
  • null

2. 類

物件導向的三個特點,封裝,繼承,多型。其中封裝指的是將函式和資料封在一起形成基本單位,以該基本單位為操作原子實現功能的一種程式設計方法。

封裝的集中體現就是類。繼承和多型是建立在類的基礎之上,是在類之上衍生出來的概念。

類對應物件導向程式設計中的封裝。類中通常包含有屬性和方法。類的屬性和方法根據其本身具有的特點可以劃分為不同的粒度或者層次。大致劃分為三個層次

  • 例項粒度:屬性和方法 屬於例項層面
  • 類粒度:屬性和方法 屬於類層面,為類的所有例項所共享
  • 其他粒度:不屬於類層面,也不屬於例項層面的函式,屬於類空間。如靜態方法

例項屬性和類屬性

例項屬性:屬於例項粒度的屬性,透過建構函式賦值

類屬性:屬於類本身的屬性,被所有類的例項共享。

class Car:
  color = 'red'      # 類屬性

  def __init__(self):
    self.name='car'  # 例項屬性
    
if __name__=='__main__':
  c = Car()
  print(Car.color)   # 訪問類屬性
  print(c.name)      # 訪問例項屬性

建構函式和self引數

類似於C++的建構函式。在類例項化的時候,呼叫建構函式。建構函式在類例項化的時候進行一些初始化的操作。如變數賦值等

self引數是類的當前例項的引用。類似於C++的This指標。

__init__或者其他類的普通方法的首個引數一般都是self,當然命名不一定是self,也可以是其他,但是它必須是類中普通方法的首個引數

class Base:
  def __init__(self):
    self.color = 'red'

python2有新式類和老式類:(新式類繼承自object)

  • 對於新式類,即使類沒有顯示定義建構函式,class也會隱式構造一個__init__函式
  • 老式類則不會顯示構建

如下:

class A:        # 舊式類
  def print_func(self):
    print("This is A") 

class D(object): # 新式類
  def print_func(self):
    print("This is D") 

if __name__=='__main__':
  a = A() 
  print(dir(a))   # 不含__init__方法
  d = D() 
  print(dir(d))   # 含有___init方法

dir可以檢視一個類的內部屬性和方法。

類方法

類方法是操作類屬性的方法。

類方法相對於類中的例項方法,有如下區別:

  • 使用@classmethod裝飾器進行定義
  • 第一個引數通常是cls(class的縮寫),代表類本身

類方法的作用:

  • 修改類屬性,其不可以呼叫類的普通屬性(例項屬性)

類方法的呼叫:

  • 使用類名可以呼叫
  • 使用類的物件可以呼叫
class Car:
  color = 'red'       # 類屬性
  
  def __init__(self): # 例項方法
    self.name='car'   # 例項屬性

  @classmethod
  def printx(cls):    # 類方法
    print("The color of car is %s" % cls.color)
    # print("The name of car is " % self.name) # 報錯無法呼叫例項屬性 

  def run(self):
    print("The name of car is %s " % self.name)

if __name__=='__main__':
  Car.printx()   # 使用類名呼叫類方法
  c = Car()
  c.printx()     # 呼叫類方法
  c.run()

靜態方法

靜態方法是python中定義在類中的一種特殊的方法,它的特殊之處在於它不與類繫結,也不與例項繫結。

靜態方法的定義和類方法,例項方法的區別如下:

  • 使用@staticmethod 裝飾器來宣告
  • 不需要傳遞類物件cls或者例項物件self作為第一個引數

靜態方法與類以及例項無關,屬於類的名稱空間中的獨立函式。

靜態方法可以透過類名和例項名來呼叫。

案例:靜態方法

class Car:
  color = 'red' 
  def __init__(self):
    self.name='car'

  @staticmethod
  def printx():
    print("hello world")
    print("The color of car is %s" % Car.color)  # 可以使用 “類名.類變數“的方式 訪問類的方式
    
  @staticmethod
  def printy(cls):   # 完全獨立的引數,可以傳入任何物件,和所屬的類有關或者無關都可以
    print("The color of car is %s" % cls.color)
    
  def run(self):
    print("The name of car is %s " % self.name)

if __name__=='__main__':
  Car.printx()
  c = Car()
  c.printx()
  Car.printy(Car)    # 類名呼叫,傳入引數
  c.printy(c)        # 例項名呼叫,傳入引數

缺少一個靜態方法應用的好的案例

靜態方法和類方法的異同點:

  • 訪問層面:兩者都可以透過類或者物件例項進行訪問;靜態方法和類方法都無法訪問例項變數,但是都可以透過類名訪問類屬性
  • 靜態方法和類方法 可以減少創造例項時所創造出的記憶體空間,加快執行速度

共有私有[待補充]

根據類內部變數和方法是否可以直接訪問,可以將類內的變數或方法分為私有、共有,受保護的。

不像C++有確定的關鍵字privateprotectedpublic 來表明屬性和方法的是否可以直接訪問,python

3.繼承

單繼承多繼承

python支援單繼承和多繼承

單繼承

class Derived(Base):  # Base為基類,Derived為派生類
   pass

多繼承

class D(A,B,C): # A,B,C為基類,D為整合類  
  pass

兩個常用函式:

  • 使用issubclass(子類,父類) 來確認一個類是否是一個父類的子類。

  • 使用isinstance(子類物件,類名)來確認一個例項是否是一個由某一個類例項化得到的。

super函式

python引入super函式是為了方便子類呼叫父類的函式。

一般意義上,當類的繼承比較簡單的時候,在子類中,可以使用父類名字.函式名(引數) 呼叫父類函式,但是當呼叫關係複雜的時候,會出現問題。

案例1: 菱形繼承

class Base():
  def __init__(self):
    print('This is Base')

class A(Base):
  def __init__(self):
    Base.__init__(self) # 使用"父類函式名.子類函式(self) 呼叫父類的建構函式
    print("This is A") 
class B(Base):
  def __init__(self):
    Base.__init__(self)
    print("This is B") 

class D(A,B):
  def __init__(self):
    A.__init__(self)
    B.__init__(self)
    print("This is D") 

if __name__ == '__main__':
  d = D() 

輸出:

This is Base
This is A
This is Base
This is B
This is D

上述是一個典型的菱形繼承問題。其繼承關係如下:

我們可以很明確的看出 Base類的__init__函式被呼叫了兩次。super函式可以解決上述問題,其呼叫形式如下:

super(子類函式名,self).父類函式名(引數列表)
super(Classname, self).methodname() 或 super(Classname, cls).methodname() 呼叫"下一個"父類中的方法

用super函式修改如上案例,如下:

class Base(object):  # python2中如果不新增該object,則 TypeError: super() argument 1 must be type, not classobj
  def __init__(self):
    print('This is Base')

class A(Base):
  def __init__(self):
    super(A,self).__init__()  # 可以寫成 super().__init__()
    print("This is A") 
class B(Base):
  def __init__(self):
    super(B,self).__init__()  # 可以寫成 super().__init__()
    print("This is B") 

class D(A,B):
  def __init__(self):
    super(D, self).__init__() # 可以寫成 super().__init__()
    print("This is D") 

if __name__ == '__main__':
  d = D() 

執行結果如下:

This is Base
This is B
This is A
This is D

思考:為什麼輸出結果是上述的順序?

MRO列表

當定義一個派生類的時候,python會計算出一個方法解析順序列表(MRO列表),它代表了類的繼承順序。可以用過類名.mro()來檢視一個子類的MRO列表

print(D.mro())

列印結果如下

[<class '__main__.D'>, <class '__main__.A'>, <class '__main__.B'>, <class '__main__.Base'>, <type 'object'>]

上述列印結果表示繼承順序:D-->A-->B-->Base-->object

MRO全稱method resolution order (方法解釋順序),主要用於多繼承時,判斷一個屬性和方法來自於那個基類。一個類的MRO順序綜合其父類MRO列表的結果。python使用C3演算法生成MRO

如果類繼承了一個基類,如下class B(A),此時B的mro序列為[B,A]

當存在多繼承的時候,如class B(A1,A2,A3...),如何合併各個子類的MRO就是C3演算法的核心。其步驟如下:[存在疑問]

  • 根據類的宣告順序,生成拓撲排序的列表(DAG)

  • 在拓撲排序的列表中,檢查每個節點的父類列表,並將其父類所在的位置移動到它自身的後面行形成新的節點。這樣,可以保證子類在父類之前

  • 對於多個父類同時出現在同一個節點之後的情況,需要按照它們在子類列表中的順序保持不變

案例1

class A(O);
class B(O);
class C(A,B);

mro(C) = C + merge( mro(A), mro(B)) 
       = C + merge( [A,O], [B,O])
       = [C,A,B,O]

案例2: 存在交叉繼承, python2出錯誤計算不出來

class A;
class B;
class C(A,B);
class D(B,A);
class E(C,D)

mro(E)如何計算? python2

mro(E) = E + merge( mro(C), mro(D)) 
       = E + merge( [C,A,B], [D,B,A])

整體程式碼如下:

class A(object):
  def __init__(self):
    print("A")

class B(object):
  def __init__(self):
    print("B")

class C(A,B):
  def __init__(self):
    super().__init__()
    print("B")

class D(B,A):
  def __init__(self):
    super().__init__()
    print("D")
'''   #交叉繼承會報錯
class E(C,D):
  def __init__(self):
    super().__init__()
    print("E")
'''
if __name__ == '__main__':
  print(D.mro())
  print(C.mro())

super的本質

super是一個函式,其定義如下:

def super(cls, inst):
    mro = inst.__class__.mro()
    return mro[mro.index(cls) + 1]

其中,cls是類,inst是類的例項, 上述函式的含義是:

  • 獲取例項inst的mro列表
  • 查詢mro列表中類cls的下表,返回下一個類即 mro[index+1]

即:super(cls, inst) 獲得的是clsinstMRO列表中的下一個類。

由上可知:super函式並非呼叫父類的函式,和呼叫父類函式並沒有實質性的練習。super函式僅僅是傳入什麼入參,進行什麼呼叫。特殊的輸入讓其表現出了像是在呼叫父類函式一樣,但是這不是本質。

案例解析:上述super函式中的 D類

class D(A,B):
  def __init__(self):
    super(D, self).__init__() # 可以寫成 super().__init__()
    print("This is D") 
 print(D.mro())  
 #[<class '__main__.D'>, <class '__main__.A'>, <class '__main__.B'>, <class '__main__.Base'>, <type 'object'>]

呼叫d=D(),結果為

This is Base
This is C
This is A
This is D

確認super(D, self).__init__() 函式呼叫:

  • super(D, self).__init__() 呼叫super函式,cls和inst都是D,根據D的mro,返回 類A。所以接下來是呼叫類A的建構函式__init__
  • super(A, self).__init__()的入參要特別注意,發生了變化。此時呼叫super函式,cls是類A,inst是類D。所以返回撥用 類B,此時呼叫類B的建構函式
  • super(B, self).__init__()的如參,cls是B,inst是D,此時呼叫Base函式,所以此時輸出 "This is Base"
    • 接下來,執行 "This is B"
    • 接下來回到 類A的建構函式,輸出This is A
    • 接下來回到類D的建構函式,輸出This is A

上述過程,針對__init__方法,沿著mro依次執行。因為mro中的類,除了基類,依次使用了super(類名,self).__init__() 這個函式,導致其會不斷的沿著mro順序進行執行。

只要相應類在mro中只出現一次,那麼就能保證一個類的相應方法只呼叫一次。

案例:當mro基類中沒有使用super。

class A(object):
  def do_something(self):
    print("This is A") 

class B(object):
  def do_something(self):
    print("This is B") 

class C(A,B):
  def do_something(self):
    super(C,self).do_something()
    print("This is C") 

if __name__=='__main__':
  print(C.mro())
  c = C() 
  c.do_something()

輸出:

[<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <type 'object'>]
This is A
This is C

上面B也是作為C的基類,因為mro中,C接下來是A,所以只執行了A的do_somethings函式,執行完畢之後,直接返回C的do_something執行後面的語句。

經典類和新式類

由於python版本的變化,python的類有了經典類和新興類的區別。

python2中,如果定義類的方式是class Base 則為經典類;如果定義類的方式是class Base(object) 則該類為新興類。

python3中上述兩種方式定義出來的類都是新式類,無須顯示繼承object

super函式只能使用新式類。

4. 多型

C++ 多型的定義

多型,即多種形態。即用不同物件去實現一個相同的動作的時候,由於物件的不同會表現出多種不同的形態。簡稱“一個介面,多種實現"

C++有靜態多型和動態多型。

  • 靜態多型: 在編譯的時候,能確定程式的行為。比如過載函式
  • 動態多型: 在執行的時候,才能確定程式的行為。 用父類的指標指向子類的函式

我們常說的多型就專指動態多型

python多型

python多型的含義和C++沒有區別,但是在表現形式存在不同。

python多型只關心函式名稱是否相同,不關心類之間是否有繼承關係。

常見的多型形式如下:定義父類,子類重寫父類的函式。

class A:
  def print_func(self):
      print("This is A") 

class B(A):
  def print_func(self):
      print("This is B") 

class C(A):
  def print_func(self):
      print("This is C") 
     
def func(obj):
    obj.print_func()  

往函式func中傳入不同的物件,其會呼叫不同物件中名稱引數匹配的函式。如下:

if __name__ == '__main__':
   a = A() 
   b = B() 
   c = C() 
   for obj in [a,b,c]:
       func(obj)

輸出:

This is A
This is B
This is C

似乎與C++多型相同,但是python的特別之處在於:python中一切皆物件, 往func可以傳入任何物件,只要該物件有print_func函式,那麼不管物件之間是否有繼承關係,相應函式都可以成功呼叫物件中相應的功能。這稱之為鴨子型別。

如下:

class D(A):
  def print_func(self):
      print("This is D") 

呼叫

d = D()
func(d.print_func())  # 輸出 This is D

鴨子型別

鴨子型別的概念來自一首詩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有很多使用方法都是可以看作是基於鴨子型別,比如 如果一個物件實現了 __getitem__方法,那麼在該物件上你就可以使用切片功能;如果一個物件實現了__iter__和next方法,python就認為它是一個迭代器,可以透過迴圈來獲取各個子項。

5. 虛擬函式

python虛擬函式需要藉助abc模組。該模組有如下功能:

  • 提供了一個 元類ABCmeta可以用來定義抽象類(元類metaclass);
  • 提供了一個裝飾起abstractmothod 定義純虛擬函式

python中虛擬函式和純虛擬函式貌似沒有什麼區別;純虛擬函式只有定義在抽象類中才有意義,因為普通的父類中普通函式可以實現虛擬函式的功能

  • 貌似沒有純虛擬函式,一旦一個python類定義為抽線基類,則不能初始化,虛擬函式必須需要要在子類中重寫

案例1: 定義虛擬函式

from abc import ABCMeta,abstractmethod

class Base():
  @abstractmethod
  def print_func(self):
    print 'This is Base'
    pass

class C(Base):
  def print_func(self):
    print "This is C"

class D(Base):
    pass

if __name__ == '__main__':
  b = Base()      # 可以例項化 
  b.print_func()  # 輸出: This is Base
  c = C()     
  c.print_func()  # 輸出:This is C
  d = D()
  d.print_func()  # 輸出: This is Base

案例2:定義抽象類

from abc import ABCMeta,abstractmethod

class Base():
  __metaclass__ = ABCMeta    # 轉變為抽象類
  @abstractmethod
  def print_func(self):
    print 'This is Base'
    pass

class C(Base):
  def print_func(self):
    print "This is C"

class D(Base):
    pass

如上,使用ABCmeta定義抽象類,此時類Base轉變為抽象類,當對Base類進行例項化,會報如下錯誤:

b = Base()  # Error:Can't instantiate abstract class Base with abstract methods print_func

上述case,D類沒有對 Base類中的虛擬函式print_func進行例項化,所以也會報錯

d = D() #Error:Can't instantiate abstract class D with abstract methods print_func

Python2中透過修改__metaclass__類變數指定元類,而Python3中直接繼承ABC類,正規化如下:

## python2
from abc import ABCMeta
class Base(ABC):  # Base此時為抽象類 
   __metaclass__ = ABCMeta

## python3
from abc import ABC, abstractmethod
class Base(ABC):  # Base此時為抽象類 
   pass

相關文章