Python中的類(下)


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

類構造和初始化

在前面的文章中,經常使用初始化函數"__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__"方法是不會被調用的。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM