Python中類建立和例項化過程

python学习者0發表於2024-05-27

一、 type()

1、建立類的兩種方式

方式一

class MyClass(object):
    def func(self,name):
        print(name)

myc = MyClass()

print(MyClass, type(MyClass))
print(myc, type(myc))

我們建立了一個名為MyClass的類,並例項化了這個類,得到其物件myc

上面程式碼列印的結果為:

<class '__main__.MyClass'>    <class 'type'>
<__main__.MyClass object at 0x0288F8F0>   <class '__main__.MyClass'>

type()函式可以檢視一個型別或變數的型別,MyClass是一個class,它的型別就是type,而myc是一個例項,它的型別就是class MyClass。

我們說class的定義是執行時動態建立的,而建立class的方法就是使用type()函式。

type()函式既可以返回一個物件的型別,又可以建立出新的型別,比如,我們可以透過type()函式建立出MyClass類,而無需透過Class MyClass(object)...的定義:

方式二

動態建立類
type(類名, 父類的元組(針對繼承的情況,可以為空),包含屬性的字典(名稱和值))

def fn(self, name='world'): # 先定義函式
    print('Hello, %s.' % name)

MyClass = type('MyClass', (object,), {'func':fn}) # 建立MyClass類,得到一個type的類物件
# MyClass = type('MyClass', (object,), {'func':lambda self,name:name}) # 建立MyClass類

myc=MyClass()

print(MyClass, type(MyClass))
print(myc, type(myc))

列印結果:

<class '__main__.MyClass'>   <class 'type'>
<__main__.MyClass object at 0x0364B830>   <class '__main__.MyClass'>

要建立一個class物件,type()函式依次傳入3個引數:

  • class的名稱;
  • 繼承的父類集合,注意Python支援多重繼承,如果只有一個父類,別忘了tuple的單元素寫法;
  • class的方法名稱與函式繫結,這裡我們把函式fn繫結到方法名func上。

透過type()函式建立的類和直接寫class是完全一樣的,因為Python直譯器遇到class定義時,僅僅是掃描一下class定義的語法,然後呼叫type()函式建立出class。

type就是建立類物件的類。你可以透過檢查__class__屬性來看到這一點。Python中所有的東西,注意,我是指所有的東西——都是物件。這包括整數、字串、函式以及類。它們全部都是物件,而且它們都是從一個類(元類,預設為type,也可以自定製)建立而來。type也是由type建立。。

二、元類(metaclass)

除了使用type()動態建立類以外,要控制類的建立行為,還可以使用metaclass。

metaclass,直譯為元類,簡單的解釋就是:

當我們定義了類以後,就可以根據這個類建立出例項,所以:先定義類,然後建立例項。

但是如果我們想建立出類呢?那就必須根據metaclass建立出類,所以:先定義元類(不自定義時,預設用type),然後建立類。

連線起來就是:先定義metaclass,就可以建立類,最後建立例項。

所以,metaclass允許你建立類或者修改類。換句話說,你可以把類看成是元類建立出來的“例項”。

預設情況下,類是使用type()構造的。類主體在一個新的名稱空間中執行,類名在本地繫結到型別的結果(名稱、基、名稱空間)。

可以透過在類定義行中傳遞元類關鍵字引數來定製類建立過程,或者從包含此類引數的現有類繼承。在下面的示例中,MyClass和MySubclass都是Meta的例項:

class Meta(type):
    pass

class MyClass(metaclass=Meta):
    pass

class MySubclass(MyClass):
    pass

使用metaclass的兩種方式

class MyType(type):  # 自定義一個type的派生類
    def __init__(self,*args,**kwargs):
    print('xx')
       super(MyType,self).__init__(*args,**kwargs)

    def __call__(cls, *args, **kwargs):
        obj = cls.__new__(cls,*args, **kwargs)
        cls.__init__(obj,*args, **kwargs)
        return obj

def with_metaclass(base):
    return MyType("MyType2",(base,),{})

# 方式一
class Foo(metaclass=MyType):  # metaclass=MyType,即指定了由MyType建立Foo類,當程式執行,用到class Foo時,即呼叫MyType的__init__方法,建立Foo類
    def __init__(self,name):
        self.name = name


#方式二    在Flask的wtform的原始碼中用到過
# class Foo(with_metaclass(object)):
#     def __init__(self,name):
#         self.name = name


a=Foo('name')

方式一:即用類的形式

執行程式碼後,當遇到class Foo時即宣告要建立一個Foo類,就會呼叫type的__init__方法建立類,由於此處(metaclass=MyType),即指定了Foo類的建立方式,所以會執行type的派生類MyType的__init__方法,建立Foo類,列印一次'xx'

  • 一般情況下, 如果你要用類來實現metaclass的話,該類需要繼承於type,而且通常會重寫type的__new__方法來控制建立過程。

  • 在metaclass裡面定義的方法會成為類的方法,可以直接透過類名來呼叫

方式二:用函式的形式

構建一個函式,返回一個type的派生類物件,例如叫type的派生類, 需要3個引數:name, bases, attrs

  • name: 類的名字
  • bases: 基類,通常是tuple型別
  • attrs: dict型別,就是類的屬性或者函式

metaclass 原理

1.基礎

metaclass的原理其實是這樣的:當定義好類之後,建立類的時候其實是呼叫了type的__new__方法為這個類分配記憶體空間,建立好了之後再呼叫type的__init__方法初始化(做一些賦值等)。所以metaclass的所有magic其實就在於這個__new__方法裡面了。

說說這個方法:__new__(cls, name, bases, attrs)

  • cls: 將要建立的類,類似與self,但是self指向的是instance,而這裡cls指向的是class

  • name: 類的名字,也就是我們通常用類名.__name__獲取的。

  • bases: 基類

  • attrs: 屬性的dict。dict的內容可以是變數(類屬性),也可以是函式(類方法)。

所以在建立類的過程,我們可以在這個函式里面修改name,bases,attrs的值來自由的達到我們的功能。這裡常用的配合方法是

getattr和setattr(just an advice)

2.查詢順序

元類是由以下優先規則決定的:

如果“元類”存在,它就被使用了。

否則,如果至少有一個基類,則使用它的元類(這首先查詢類屬性,如果沒有找到,則使用它的型別)。

否則,如果一個名為元類的全域性變數存在,就會使用它。

三、 __init____new____call__三個特殊方法

  • __new__: 物件的建立,是一個靜態方法,第一個引數是cls。(想想也是,不可能是self,物件還沒建立,哪來的self),其必須要有返回值,返回例項化出來的例項,需要注意的是,可以return父類__new__()出來的例項,也可以直接將object的__new__()出來的例項返回。

  • __init__ : 物件的初始化, 是一個例項方法,第一個引數是self,該self引數就是__new__()返回的例項,__init__()__new__()的基礎上可以完成一些其它初始化的動作,__init__()不需要返回值。

  • __call__ : 物件可call,注意不是類,是物件。

1.對於__new__

class Bar(object):
    pass

class Foo(object):
    def __new__(cls, *args, **kwargs):
        return Bar()

print(Foo()) 

列印結果為:

<__main__.Bar object at 0x0090F930>

可以看到,輸出來是一個Bar物件。

__new__方法在類定義中不是必須寫的,如果沒定義,預設會呼叫object.__new__去建立一個物件。如果定義了,就是會覆蓋,使用自定義的,這樣就可以自定製建立物件的行為。

2.對於__init__

class Person(object):

  def __init__(self, name, age):
    self.name = name
    self.age = age
    print('執行__init__')

  def __new__(cls, *args, **kwargs):
      obj = object.__new__(cls) # 建立物件
      print('執行__new__方法')
      return obj

p1 = Person('hc', 24)
print(p1)

列印結果:

執行__new__方法
執行__init__
<__main__.Person object at 0x028EB830>

__init__ 方法通常用在初始化一個類例項的時候,但__init__其實不是例項化一個類的時候第一個被呼叫 的方法。當使用 Persion(name, age) 這樣的表示式來例項化一個類時,最先被呼叫的方法 其實是 __new__ 方法。從列印結果就可以看出來

__new__()沒有正確返回當前類cls的例項,那__init__()將不會被呼叫,即使是父類的例項也不行。

3.對於__call__

  物件透過提供__call__(slef, *args ,**kwargs)方法可以模擬函式的行為,如果一個物件x提供了該方法,就可以像函式一樣使用它,也就是說x(arg1, arg2...) 等同於呼叫x.__call__(self, arg1, arg2)

class Foo(object): 
  def __call__(self): 
    pass 
#學習中遇到問題沒人解答?小編建立了一個Python學習交流群:153708845 
f = Foo()    #類(),即執行元類的__call__
f()    #物件(),即執行Foo的__call__ 

4、例項化物件的完整過程

class Foo(Bar):
    pass

當我們寫如這段程式碼時,Python做了如下的操作:

Foo中有metaclass這個屬性嗎?如果是,Python會在記憶體中透過metaclass建立一個名字為Foo的類物件(我說的是類物件,請緊跟我的思路)。如果Python沒有找到metaclass,它會繼續在Bar(父類)中尋找metaclass屬性,並嘗試做和前面同樣的操作。如果Python在任何父類中都找不到metaclass,它就會在模組層次中去尋找metaclass,並嘗試做同樣的操作。如果還是找不到metaclass,Python就會用內建的type來建立這個類物件。

把上面這段話反覆讀幾次,現在的問題就是,你可以在metaclass中放置些什麼程式碼呢?

答案就是:可以建立一個類的東西。

那麼什麼可以用來建立一個類呢?

type,或者任何使用到type或者子類化type的東東都可以。

以上面的程式碼為例,我們例項化一個物件obj=Foo()時,會先執行Foo類的__new__方法,沒寫時,用父類的__new__方法,建立一個物件,並返回,然後執行__init__方法(自己有就用自己的,沒有就用父類的),對建立的物件進行初始化。

obj()會執行Foo類的__call__方法,沒有則用父類的

我們現在已經知道,類也是物件,是元類的物件,即我們例項化一個類時,呼叫其元類的__call__方法。

元類處理過程:定義一個類時,使用宣告或者預設的元類對該類進行建立,對元類求type運算,得到父元類(該類宣告的元類的父元類),呼叫父元類的__call__函式,在父元類的__call__函式中, 呼叫該類宣告的元類的__new__函式來建立物件(該函式需要返回一個物件(指類)例項),然後再呼叫該元類的__init__初始化該物件(此處物件是指類,因為是元類建立的物件),最終返回該類

1.物件是類建立,建立物件時候類的__init__方法自動執行,物件()執行類的__call__方法
2.類是type建立,建立類時候type的__init__方法自動執行,類() 執行type的 __call__方法(類的__new__方法,類的__init__方法)

原始type的__call__應該是引數結構應該是:

metaname, clsname, baseclasses, attrs

原始type的__new__

metaname, clsname, baseclasses, attrs

原始type的__init__

class_obj, clsname, baseclasses, attrs

元類的__new____init__影響的是建立類物件的行為,父元類的__call__控制對子元類的 __new____init__的呼叫,就是說控制類物件的建立和初始化。父元類的__new____init__由更上層的控制,

一般來說,原始type是最初的父元類,其__new____init__是具有普遍意義的,即應該是分配記憶體、初始化相關資訊等

元類__call__影響的是建立類的例項物件的行為,此時如果類自定義了__new____init__就可以控制類的物件例項的建立和初始化

__new____init__ 影響的是建立物件的行為,當這些函式在元類中時,影響建立的是類;同理,當這倆個函式在普通類中時,影響建立的是普通的物件例項。

__call__ 影響()呼叫行為, __call__是在建立類的時候呼叫,即: class Test(object): __metaclass__=type, 定義類時就是建立類,此時會呼叫元類的__call__,如果元類有繼承,子元類定義時執行的是父元類的__call__
如果是普通類例項化物件,呼叫的是普通類的__call__

相關文章