1. 程式人生 > >python基礎語法(11)類下

python基礎語法(11)類下

本文將介紹一下類的建構函式和初始化函式,以及如何通過"魔術方法"定製一個類。

類構造和初始化

在前面的文章中,經常使用初始化函式"__init__",下面看看"__init__"和"__new__"的聯絡和差別。

下面先通過一段程式碼看看這兩個方法的呼叫順序:

複製程式碼
class A(object):
    def __init__(self,*args, **kwargs):
        print "init %s" %self.__class__
    def __new__(cls,*args, **kwargs):
        print "new %s" %cls
        
return object.__new__(cls, *args, **kwargs) a = A()
複製程式碼

從程式碼的輸出可以看到,當通過類例項化一個物件的時候,"__new__"方法首先被呼叫,然後是"__init__"方法。

一般來說,"__init__"和"__new__"函式都會有下面的形式:

def __init__(self, *args, **kwargs):
    # func_suite
    
def __new__(cls, *args, **kwargs):
    # func_suite
return obj

對於"__new__"和"__init__"可以概括為:

  • "__new__"方法在Python中是真正的構造方法(建立並返回例項),通過這個方法可以產生一個"cls"對應的例項物件,所以說"__new__"方法一定要有返回
  • 對於"__init__"方法,是一個初始化的方法,"self"代表由類產生出來的例項物件,"__init__"將對這個物件進行相應的初始化操作

前面文章中已經介紹過了"__init__"的一些行為,包括繼承情況中"__init__"的表現。下面就重點看看"__new__"方法。

__new__特性

"__new__"是在新式類中新出現的方法,它有以下行為特性:

  • "__new__" 方法是在類例項化物件時第一個呼叫的方法,將返回例項物件
  • "__new__" 方法始終都是類的靜態方法(即第一個引數為cls),即使沒有被加上靜態方法裝飾器
  • 第一個引數cls是當前正在例項化的類,如果要得到當前類的例項,應當在當前類中的 "__new__" 方法語句中呼叫當前類的父類的" __new__" 方法

對於上面的第三點,如果當前類是直接繼承自 object,那當前類的 "__new__" 方法返回的物件應該為:

def __new__(cls, *args, **kwargs):
    # func_suite
return object.__new__(cls, *args, **kwargs)

重寫__new__

如果(新式)類中沒有重寫"__new__"方法,Python預設是呼叫該類的直接父類的"__new__"方法來構造該類的例項,如果該類的父類也沒有重寫"__new__",那麼將一直按照同樣的規則追溯至object的"__new__"方法,因為object是所有新式類的基類。

而如果新式類中重寫了"__new__"方法,那麼可以選擇任意一個其他的新式類(必須是新式類,只有新式類有"__new__",因為所有新式類都是從object派生)的"__new__"方法來建立例項,包括這個新式類的所有前代類和後代類,只要它們不會造成遞迴死迴圈。

看一段例子程式碼:

複製程式碼
class Foo(object):
    def __new__(cls, *args, **kwargs):
        obj = object.__new__(cls, *args, **kwargs)   
        # 這裡的object.__new__(cls, *args, **kwargs)   等價於
        # super(Foo, cls).__new__(cls, *args, **kwargs)   
        # object.__new__(Foo, *args, **kwargs)
        # Bar.__new__(cls, *args, **kwargs)
        # Student.__new__(cls, *args, **kwargs),即使Student跟Foo沒有關係,也是允許的,因為Student是從object派生的新式類
        
        # 在任何新式類,不能呼叫自身的“__new__”來建立例項,因為這會造成死迴圈
        # 所以要避免return Foo.__new__(cls, *args, **kwargs)或return cls.__new__(cls, *args, **kwargs)
        print "Call __new__ for %s" %obj.__class__
        return obj    
        
class Bar(Foo):
    def __new__(cls, *args, **kwargs):
        obj = object.__new__(cls, *args, **kwargs)   
        print "Call __new__ for %s" %obj.__class__
        return obj   

class Student(object):
    # Student沒有“__new__”方法,那麼會自動呼叫其父類的“__new__”方法來建立例項,即會自動呼叫 object.__new__(cls)
    pass

class Car(object):
    def __new__(cls, *args, **kwargs):
        # 可以選擇用Bar來建立例項
        obj = object.__new__(Bar, *args, **kwargs)   
        print "Call __new__ for %s" %obj.__class__
        return obj
    
foo = Foo()
bar = Bar()
car = Car()
複製程式碼

程式碼的輸出為:

__init__的呼叫

"__new__"決定是否要使用該類的"__init__"方法,因為"__new__" 可以呼叫其他類的構造方法或者直接返回別的類建立的物件來作為本類的例項。

通常來說,新式類開始例項化時,"__new__"方法會返回cls(cls指代當前類)的例項,然後呼叫該類的"__init__"方法作為初始化方法,該方法接收這個例項(即self)作為自己的第一個引數,然後依次傳入"__new__"方法中接收的位置引數和命名引數。

但是,如果"__new__"沒有返回cls(即當前類)的例項,那麼當前類的"__init__"方法是不會被呼叫的。看下面的例子:

複製程式碼
class A(object):
    def __init__(self, *args, **kwargs):
        print "Call __init__ from %s" %self.__class__
    
    def __new__(cls, *args, **kwargs):
        obj = object.__new__(cls, *args, **kwargs)
        print "Call __new__ for %s" %obj.__class__
        return obj   

class B(object):
    def __init__(self, *args, **kwargs):
        print "Call __init__ from %s" %self.__class__
    
    def __new__(cls, *args, **kwargs):
        obj = object.__new__(A, *args, **kwargs)
        print "Call __new__ for %s" %obj.__class__
        return obj      


b = B()
print type(b)
複製程式碼

程式碼中,在B的"__new__"方法中,通過"obj = object.__new__(A, *args, **kwargs)"建立了一個A的例項,在這種情況下,B的"__init__"函式就不會被呼叫到。

派生不可變型別

關於"__new__"方法還有一個重要的用途就是用來派生不可變型別。

例如,Python中float是不可變型別,如果想要從float中派生一個子類,就要實現"__new__"方法:

複製程式碼
class Round2Float(float):
    def __new__(cls, num):
        num = round(num, 2)
        #return super(Round2Float, cls).__new__(cls, num)
        return float.__new__(Round2Float, num)
        
f = Round2Float(4.14159)
print f
複製程式碼

程式碼中從float派生出了一個Round2Float類,該類的例項就是保留小數點後兩位的浮點數。

定製一個類

在Python中,我們可以通過"魔術方法"使自定義的class變得強大、易用。

例如,前面的文章中介紹過Python迭代器,當我們想定義一個可迭代的類物件的時候,就可以去實現"__iter__(self)"這個魔術方法;

又例如,前面文章介紹的上下文管理器,當需要建立一個上下文管理器類物件的時候,就可以去實現"__enter__(self)"和"__exit__(self)"方法。

所以,建議參考 "魔術方法" 的文件,通過魔術方法來定製自定義的類。

呼叫魔術方法

一些魔術方法直接和內建函式相對應的,在這種情況下,呼叫他們的方法很簡單,下面給出了一些對應表。

魔術方法

呼叫方式

解釋

__new__(cls [,...])

instance = MyClass(arg1, arg2)

__new__ 在建立例項的時候被呼叫

__init__(self [,...])

instance = MyClass(arg1, arg2)

__init__ 在建立例項的時候被呼叫

__cmp__(self, other)

self == other, self > other, 等。

在比較的時候呼叫

__pos__(self)

+self

一元加運算子

__neg__(self)

-self

一元減運算子

__invert__(self)

~self

取反運算子

__index__(self)

x[self]

物件被作為索引使用的時候

__nonzero__(self)

bool(self)

物件的布林值

__getattr__(self, name)

self.name # name 不存在

訪問一個不存在的屬性時

__setattr__(self, name, val)

self.name = val

對一個屬性賦值時

__delattr__(self, name)

del self.name

刪除一個屬性時

__getattribute(self, name)

self.name

訪問任何屬性時

__getitem__(self, key)

self[key]

使用索引訪問元素時

__setitem__(self, key, val)

self[key] = val

對某個索引值賦值時

__delitem__(self, key)

del self[key]

刪除某個索引值時

__iter__(self)

for x in self

迭代時

__contains__(self, value)

value in self, value not in self

使用 in 操作測試關係時

__concat__(self, value)

self + other

連線兩個物件時

__call__(self [,...])

self(args)

"呼叫"物件時

__enter__(self)

with self as x:

with 語句環境管理

__exit__(self, exc, val, trace)

with self as x:

with 語句環境管理

__getstate__(self)

pickle.dump(pkl_file, self)

序列化

__setstate__(self)

data = pickle.load(pkl_file)

序列化

總結

文中介紹了類的構造和初始化方法:"__new__"和"__init__"。

"__new__"方法是新式類特有的方法,通常情況下,__new__方法會建立返回cls(cls指代當前類)的例項,然後呼叫該類的"__init__"方法作為初始化方法,該方法接收這個例項(即self)作為自己的第一個引數,然後依次傳入"__new__"方法中接收的位置引數和命名引數;但是,如果"__new__"沒有返回cls(即當前類)的例項,那麼當前類的"__init__"方法是不會被呼叫的。

通過"魔術方法",可以對自定義類進行定製、擴充套件,使得自定義類變得強大、易用。