1. 程式人生 > >Python面試進階問題,__init__和__new__的區別是什麼?

Python面試進階問題,__init__和__new__的區別是什麼?

本文始發於個人公眾號:**TechFlow**,原創不易,求個關注

今天這篇是Python專題的第17篇文章,我們來聊聊Python當中一個新的預設函式__new__。

上一篇當中我們講了如何使用type函式來動態建立Python當中的類,除了type可以完成這一點之外,還有另外一種用法叫做metaclass。原本這一篇應該是繼續元類的內容,講解metaclass的使用。但是metaclass當中用到了一個新的預設函式__new__,關於這個函式大家可能會比較陌生,所以在我們研究metaclass之前,我們先來看看__new__這個函式的用法。

真假建構函式

如果你去面試Python工程師的崗位,面試官問你,請問Python當中的類的建構函式是什麼?

你不假思索,當然是__init__啦!如果你這麼回答,很有可能你就和offer無緣了。因為在Python當中__init__並不是建構函式,__new__才是。是不是有點蒙,多西得(日語:為什麼)?我們不是一直將__init__方法當做建構函式來用的嗎?怎麼又冒出來一個__new__,如果__new__才是建構函式,那麼為什麼我們建立類的時候從來不用它呢?

彆著急,我們慢慢來看。首先我們回顧一下__init__的用法,我們隨便寫一段程式碼:

class Student:
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender

我們一直都是這麼用的,對不對,毫無問題。但是我們換一個問題,我們在Python當中怎麼實現單例(Singleton)的設計模式呢?怎麼樣實現工廠呢?

從這個問題出發,你會發現只使用__init__函式是不可能完成的,因為__init__並不是建構函式,它只是初始化方法。也就是說在呼叫__init__之前,我們的例項就已經被建立好了,__init__只是為這個例項賦上了一些值。如果我們把建立例項的過程比喻成做一個蛋糕,__init__方法並不是烘焙蛋糕的,只是點綴蛋糕的。那麼顯然,在點綴之前必須先烘焙出一個蛋糕來才行,那麼這個烘焙蛋糕的函式就是__new__。

__new__函式

我們來看下__new__這個函式的定義,我們在使用Python面向物件的時候,一般都不會重構這個函式,而是使用Python提供的預設建構函式,Python預設建構函式的邏輯大概是這樣的:

def __new__(cls, *args, **kwargs):
    return super().__new__(cls, *args, **kwargs)

從程式碼可以看得出來,函式當中基本上什麼也沒做,就原封不動地呼叫了父類的建構函式。這裡隱藏著Python當中類的建立邏輯,是根據繼承關係一級一級建立的。根據邏輯關係,我們可以知道,當我們建立一個例項的時候,實際上是先呼叫的__new__函式建立例項,然後再呼叫__init__對例項進行的初始化。我們可以簡單做個實驗:

class Test:
    def __new__(cls):
        print('__new__')
        return object().__new__(cls)
    def __init__(self):
        print('__init__')

當我們建立Test這個類的時候,通過輸出的順序就可以知道Python內部的呼叫順序。

從結果上來看,和我們的推測完全一樣。

單例模式

那麼我們過載__new__函式可以做什麼呢?一般都是用來完成__init__無法完成的事情,比如前面說的單例模式,通過__new__函式就可以實現。我們來簡單實現一下:

class SingletonObject:
    def __new__(cls, *args, **kwargs):
        if not hasattr(SingletonObject, "_instance"):
            SingletonObject._instance = object.__new__(cls)
        return SingletonObject._instance
    
    def __init__(self):
        pass

當然,如果是在併發場景當中使用,還需要加上執行緒鎖防止併發問題,但邏輯是一樣的。

除了可以實現一些功能之外,還可以控制例項的建立。因為Python當中是先呼叫的__new__再呼叫的__init__,所以如果當呼叫__new__的時候返回了None,那麼最後得到的結果也是None。通過這個特性,我們可以控制類的建立。比如設定條件,只有在滿足條件的時候才能正確建立例項,否則會返回一個None。

比如我們想要建立一個類,它是一個int,但是不能為0值,我們就可以利用__new__的這個特性來實現:

class NonZero(int):
    def __new__(cls, value):
        return super().__new__(cls, value) if value != 0 else None

那麼當我們用0值來建立它的時候就會得到一個None,而不是一個例項。

工廠模式

理解了__new__函式的特性之後,我們就可以靈活運用了。我們可以用它來實現許多其他的設計模式,比如大名鼎鼎經常使用的工廠模式。

所謂的工廠模式是指通過一個介面,根據引數的取值來建立不同的例項。建立過程的邏輯對外封閉,使用者不必關係實現的邏輯。就好比一個工廠可以生產多種零件,使用者並不關心生產的過程,只需要告知需要零件的種類。也因此稱為工廠模式。

比如說我們來建立一系列遊戲的類:

class Last_of_us:
    def play(self):
        print('the Last Of Us is really funny')
        
        
class Uncharted:
    def play(self):
        print('the Uncharted is really funny')
        

class PSGame:
    def play(self):
        print('PS has many games')

然後這個時候我們希望可以通過一個介面根據引數的不同返回不同的遊戲,如果不通過__new__,這段邏輯就只能寫成函式而不能通過面向物件來實現。通過過載__new__我們就可以很方便地用引數來獲取不同類的例項:

class GameFactory:
    games = {'last_of_us': Last_Of_us, 'uncharted': Uncharted}
    def __new__(cls, name):
        if name in cls.games:
            return cls.games[name]()
        else:
            return PSGame()
        

uncharted = GameFactory('uncharted')
last_of_us = GameFactory('last_of_us')

總結

相信看到這裡,關於__new__這個函式的用法應該都能理解了。一般情況下我們是用不到這個函式的,只會在一些特殊的場景下使用。雖然如此,我們學會它並不只是用來實現設計模式,更重要的是可以加深我們對於Python面向物件的理解。

除此之外,另一個經常使用__new__場景是元類。所以今天的這篇文章其實也是為了後面介紹元類的其他用法打基礎。

如果喜歡本文,可以的話,請點個關注,給我一點鼓勵,也方便獲取更多文章。

![](https://user-gold-cdn.xitu.io/2020/6/11/172a119798571372?w=258&h=258&f=png&