Python世界裏的魔術方法(一)

序言

傳說中,Python對象天生具有一些神奇的方法,它們具有一些魔力,因此它們總被雙下劃線所包圍着。這些方法統稱爲魔術方法。在特定的操作下,這些魔術方法會被自動調用,並且表現出許多神奇的現象。

它們是Python面向對象下智慧的結晶。作爲Python使用者,瞭解它們是我們的職責,在某些情況下,我們甚至能改變它們的魔力。

本文主要介紹下這些魔術方法中主要的部分,並且說明它們每個的作用和神奇之處。

構造和析構方法

__new__

魔力:實例的構造方法。一個對象實例化時,調用的第一個方法。

參數:第一個參數是這個類,其他參數則傳遞給__init__方法

__new__方法決定是否要使用該__init__方法,因爲它可以調用其他類的構造方法或者直接返回別的實例對象作爲本類的實例。

如果__new__沒有返回實例對象,則__init__不會被調用。

class Foo:
    pass

class Bar(Foo):
    def __new__(cls):
        print("__new__方法被執行")
        return super().__new__(cls) # 返回值就是所謂的self

    def __init__(self): # self被傳遞
        print("__init__方法被執行")
        
# 輸出
__new__方法被執行
__init__方法被執行

__new__方法傳入類(cls),而__init__傳入類的實例化對象(self)。

__new__返回的值是一個實例化對象,如果返回None時則不執行__init__,並且返回值只能調用父類中的__new__方法,而不能調用毫無關係的類的__new__方法。

class CapStr():
    def __new__(*string):
        cls,*args = string
        print(cls,*args) # <class '__main__.CapStr'> I love China!
        self_in_init = super(CapStr,cls).__new__(cls)
        print(id(self_in_init))
        return self_in_init

    def __init__(self, string):
        print(id(self))


a = CapStr("I love China!")
print(id(a))

上例可以看到,一個對象實例化後,它的參數會被當做new方法第二個參數接收,並作爲init方法的第二個參數接收。
並且,new方法必須調用super()方法,返回其實例。

創世之初,宇宙一片混
沌。盤古(Python類)開天闢地,使用__new__的魔力,創造了世界(返回了Python實例化對象)。此時新世界誕生,佔據無垠宇宙中的一席之地(內存地址)。於是,天地萬物從這裏開始。

宇宙之外,造物者以這種方式無時無刻在創造着新世界。

__init__

構造方法。

魔力:構造器。在構建實例時,自動執行此方法。如果不定義,則實例化後隱式繼承調用。

參數:__new__的返回值self,其他可選參數

注意:__init__()不能有返回值,即它只能返回None。

類實例化後,會獲得一個實例對象。

class World:
    def __init__(self, mountain, river, human, light):
        self.mountain = mountain
        self.river = river
        self.human = human
        self.light = light

    def begin(self):
        print('{} is born'.format(self.human))
        
    def create_light(self):
        print('要有{}'.format(self.light))

world = World('mountain','river','human','光')
world.begin()  # 輸出  human is born
world.create_light()  # 輸出  要有光

實例對象self會綁定方法,即每個方法都需要和此實例self綁定。Python會把方法的調用者,作爲第一個參數self的實參傳入。上例中,world即self。

世界的模子被創建出來之後,上帝開始在模子上構建世界的初貌。Ta施展了__init__方法的魔力,說:要有光(傳遞參數)。於是,世界有了光。

注意:

world = World('mountain','river','human','光')
earth = World('mountain','river','human','光')

print(id(world.begin)) # 2140756327624
print(id(earth.begin)) # 2140756327624

儘管創造了不同的實例對象,但由於它們同源,因此它們的方法本質上是一致的。儘管,方法是綁定在各自的實例對象上。

小結:__new____init__相配合,是Python世界中的構造器。

__del__

析構方法。

使用場景:析構方法。銷燬類的實例時可調用,以釋放佔用的資源。其中就放些清理資源的代碼,比如釋放連接。

注意:此方法不能引起對象的真正銷燬,只是對象銷燬時會自動調用它。

使用del語句刪除實例時,引用計數會減1。當引用計數爲0時,會自動調用__del__方法。

由於Python實現了垃圾回收機制,無法確定對象何時執行垃圾回收。

class World:

    def __init__(self, name):
        self.name = name

    def __del__(self):
        print('delete {}'.format(self.name))

def delete():
    world = World('earth')
    world.__del__()
    world.__del__()

delete()
# 打印三次delete earth,因爲最後一次對象銷燬,引用計數爲0

由於垃圾回收對象銷燬時,纔會真正清理對象。因此還會在之前自動調用__del__方法。一般不手動調用。關於Python的垃圾回收機制,在這裏暫不作討論。

類和對象屬性

__class__

對象屬性。

魔力:存儲類名

world = World('mountain', 'river', 'human', '光')
print(world.__class__) # <class '__main__.World'>

類似的有類的魔術方法:

屬性 含義
__name__ 類、函數、方法等的名字
__module__ 類定義所在的模塊名
__qualname__ 對象或類所屬的類
print(World.__qualname__) # World
print(World.__name__) # World

__dict__

類屬性和對象屬性

魔力:存儲了屬性字典,分爲對象屬性字典和類屬性字典。

print(world.__dict__)  # 實例屬性
# 輸出 {'mountain': 'mountain', 'river': 'river', 'human': 'human', 'light': '光'}

print(World.__dict__)  # 類屬性
# 輸出 {'__module__': '__main__', '__init__': <function World.__init__ at 0x000001B81AEE8620>, 'begin': <function World.begin at 0x000001B81AEE86A8>, 'create_light': <function World.create_light at 0x000001B81AEE8730>, '__dict__': <attribute '__dict__' of 'World' objects>, '__weakref__': <attribute '__weakref__' of 'World' objects>, '__doc__': None}

實例字典,只保存實例的屬性。實例屬性和實例self綁定,爲實例獨佔。

類字典,保存了類的屬性,包括了__init__,實例方法,類變量等等。所有實例可共享此類屬性,但不包括各個實例屬性。

當訪問一個實例的屬性時,實例屬性的查找順序:

先查找自己的屬性字典,如果沒有則通過屬性__class__找到自己的類,再到類的屬性字典中找。

注意:如果實例使用__dict__[變量名]訪問變量,則不會按照上述的訪問順序查找變量。這是指明使用字典的key查找,而不是屬性查找。

一般而言,類變量使用全大寫命名。類變量相當於類的常量,爲實例準備的常量。

__bases__

類屬性

使用場景:類的基類的元組,順序爲它們在基類的列表中出現的順序。

class World:

    def __init__(self):
        self.name = 'world'


class Human(World):

    def __init__(self):
        self.name = 'earth';

class Person(Human,World):

    def __init__(self):
        self.name = 'jaywin'

print(Person.__bases__) 
# (<class '__main__.Human'>, <class '__main__.World'>)	

__doc__

類屬性。

作用:類、函數的文檔字符串。如果沒有定義則爲None

__mro__

類屬性。

作用:Method Resolution Order。類的mroclass.mro()返回的結果保存在__mro__中。它

class World:

    def __init__(self):
        self.name = 'world'


class Human(World):

    def __init__(self):
        self.name = 'earth';

class Person(Human,World):

    def __init__(self):
        self.name = 'jaywin'

print(Person.mro()) 
# [<class '__main__.Person'>, <class '__main__.Human'>, <class '__main__.World'>, <class 'object'>]

關於Python的繼承,這裏暫不討論。

收集屬性

__dir__

方法。

作用:返回類或者對象的所有成員名稱列表。如果需要查看類或對象的屬性,則可使用dir()

dir()函數就是調用__dir__

如果提供此方法,則返回屬性的列表。否則會盡量從此屬性中收集信息。

  • 如果對象是模塊對象,返回的列表包含模塊的屬性名
  • 如果對象是類型或者類對象,返回的列表包含類的屬性名,以及它的基類的屬性名。
  • 否則,返回列表包含對象的屬性名,它的類的屬性名和類的基類的屬性名。

哈希和等值方法

__hash__

作用:內建函數hash()調用時的返回值,返回一個整數。如果定義此方法,該類的實例就可以可hash。

__eq__

作用:對應==操作符,判斷2個對象是否相等,返回bool值。

這兩個魔術方法可以一起來理解,看以下例子。

class Foo:
    def __init__(self, name='Tom', age=18):
        self.name = name
        self.age = age

    def __hash__(self):
        return 1

    def __repr__(self):
        return "<Foo name={},age={}>".format(self.name,self.age)


print(hash(Foo('tom'))) # 輸出 1

print(Foo('tom'),Foo('tom'))
# 輸出: <Foo name=tom,age=18> <Foo name=tom,age=18>

print([Foo('tom'),Foo('tom')])
# 列表: [<Foo name=tom,age=18>, <Foo name=tom,age=18>]

print({Foo('tom'),Foo('tom')})
# 集合: {<Foo name=tom,age=18>, <Foo name=tom,age=18>}
# 集合中竟然有兩個相同的元素?

在Python中,對於集合對象裏元素的要求,必須是可hash的。也就是說,會對每個元素進行hash()。

在這裏,對hash(Foo(‘tom’))的結果是1,因此,最後一個的輸出結果中,不應該出現重複的元素。

那這裏爲什麼無法去重了?不是說集合裏不允許相同的元素出現嗎?

此時,需要了解__eq__魔術方法。

在集合或者字典中(字典中的key也可看做集合的元素,支持集合的與或非等操作。不過它還映射一個值罷了),元素的第一步是hash。如果hash後的結果相同,則第二步是檢查是否相等,相等則去重。

第一步的hash中,__hash__方法只返回一個hash值,作爲key。

第二步判斷兩個對象是否相等,則通過__eq__方法。

因此,上例可解釋爲,元素的hash值相等,hash衝突。但這兩個對象並不是同一個對象。

而一般要求是,只要hash相同, 那麼它們應該相等。

因此,是相等的步驟上出現了問題:

    def __eq__(self,other):
        return self.age == other.age # other爲第二個對象
        
 print({Foo('tom'),Foo('tom')})
 # 輸出: {<Foo name=tom,age=18>}

一般地,提供了__hash__方法,是爲了作爲set或者dict的key,爲了去重,還需要提供 eq方法。

即使是不同的實例,如果__eq__的結果返回True,則表示二者相等。

不可hash對象:isinstance(p1, collections.Hashable)一定爲False。如此判斷對象是否可hash。

list類實例爲什麼不可hash?

源碼中,有一句:__hash__= None。所有類都繼承object,而object具有此方法 的。如果希望一個類不可hash,則將此方法設爲None即可。

注意:

  • __hash__僅僅return整型,而hash()可接受任意類型的可哈希的值
  • 如果定義了__hash__,就應該定義__eq__

布爾值方法

__bool__

作用:內建函數bool(),或者對象放在邏輯表達式的位置,調用這個函數返回布爾值。沒有定義__bool__,就找__len__返回長度,非0爲真。

如果__ len__也沒有定義,則所有實例都返回真。

class A: pass


print(bool(A())) # True
if A():
    print('Read A')

class B:
    def __bool__(self):
        return False

print(bool(B)) # True
print(bool(B())) # False

class C:
    def __len__(self):
        return 0

print(bool(C())) # False

此時,我們知道了爲什麼空列表,空元組,空集合,空字典的bool()都爲False了。

可視化

__repr__

__str__

__bytes__

方法 意義
_repr_ 內建函數repr()對一個對象獲取字符串表達。調用__repr__方法返回字符串表達。如果它沒有定義,就直接返回object的定義,即顯示內存地址信息。
_str_ str()函數,內建函數format()和print()函數調用,需要返回對象的字符串表達。如果沒有定義,就去調用_repr_ 方法返回字符串表達。如果__repr__沒有定義,則直接返回對象的內存地址信息。
__bytes__ bytes()函數調用,返回一個對象的bytes表達,返回bytes對象。
class A:
    def __init__(self, name, age=18):
        self.name = name
        self.age = age

    def __repr__(self):
        return 'repr: {},{}'.format(self.name,self.age)

    def __str__(self):
        return 'str: {},{}'.format(self.name,self.age)

    def __bytes__(self):
        import json
        return json.dumps(self.__dict__).encode() # 返回bytes

a = A('tom')
print(a) # 調用__str__
print([a]) # []使用__str__, 但[]內部使用__repr__
print([str(a)]) # []使用__str__,內部也使用__str__
print(bytes(a)) # 輸出 b'{"name": "tom", "age": 18}'

運算符重載

operator模塊提供以下的特殊方法,可以將類的實例,使用下面的操作符來操作。

運算符 特殊方法 含義
<,<=,==,>,>=,!= __lt__, __le__,__eq__,__gt__,__ge__,__ne__ 比較運算符
+,-,*,/,%,//,**,divmod __add__,__sub__,__mul__,__truediv__,__mod__,__floordiv__,__pow__,__divmod__ 算數運算符,移位運算符,位運算符
+=,-=,*=,/=,%=,//=,**= iadd,isub,imul,iteruediv,imod,ifloordiv,ipow
class A:
    def __init__(self,name,age=18):
        self.name = name
        self.age = age

    def __sub__(self,other): # 減法
        return self.age - other.age

    def __isub__(self,other): # 減等
        return A(self.name, self-other)

tom = A('tom')
jerry = A('jerry', 16)
print(tom - jerry) # 18 - 16 = 2,輸出2
print(jerry - tom, jerry.__sub__(tom)) #  輸出 -2 -2 
#誰調用,self就是誰

print(id(tom))  # 1710930572792
tom -= jerry  # 返回新對象,也能就地修改實例
print(tom.age,id(tom)) # 2 1710930572904

#  就地修改實例
    def __isub__(self,other):
        self.age = self.age - other.age
        return self

運算符重載應用場景

往往是用面向對象實現的類,需要做大量的運算。而運算符的這種運算是在數學上最常見的表達方式,並且可以重新定義+的表達。

提供運算符重載,比直接提供加法方法要更加適合該領域內使用者的習慣。比如Path類,/變成了路徑分隔符,而不是除法。因此這個符號,運算符進行了重載了。

因此,根據業務的要求,可以應用到運算符重載,比如座標+座標。

int類,幾乎實現了所有操作符,可以作爲參考。

@functools.total_ordering 裝飾器

__lt__, __le__,__eq__,__gt__,__ge__,__ne__ 是比較大小時必須實現的方法,但是要全部寫完太麻煩了。使用此裝飾器可大大簡化代碼。

但是,要求__eq__必須實現,而其他方法實現其一,即可支持全部的比較大小的方法。

from functools import total_ordering

@total_ordering
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __eq__(self,other):
        return self.age == other.age

    def __gt__(self,other):
        return self.age > other.age

tom = Person('tom',20)
jerry = Person('jerry',16)
print(tom > jerry) # True
print(tom >= jerry) # True

上例大大簡化代碼,但是一般而言比較實現等於或者小於方法就夠,其他可以不實現。因此此裝飾器可能會帶來性能問題,建議需要什麼方法自己創建,少用這個裝飾器。

容器相關方法

關於Python的容器類型,可參考這篇文章

https://blog.csdn.net/qchl2015/article/details/105073207

__len__

作用:內建函數len(),返回對象的長度。一個對象必須定義__len__方法才能使用len()函數去獲取其長度。

如果把對象當做容器類型看,就如同list或者dict。

bool()函數調用時,如果沒有__bool__方法,則看此方法是否存在,存在返回非0爲真。

如果是Python內置的類型,比如列表,字符串,字節序列等。那麼CPython會抄個近路,__len__實際上會直接返回PyVarObject裏的ob_size屬性。PyVarObject是表示內存中長度可變的內置對象的C語言結構體。直接讀取這個屬性比調用一個方法要快得多。

迭代和成員關係判斷

__iter__

作用: 迭代容器時,調用。返回一個新的迭代器對象。定義了此方法的對象,爲可迭代對象。

__contains__

作用:in,not in 成員關係判斷運算符,使用此運算符時會調用此方法。沒有實現時就調用__iter__方法遍歷。

class Foo:
    def __init__(self,value):
        self.data = value

    def __contains__(self, item):
        print('contains: ',end='')
        return item in self.data


f = Foo([1,2,3,4])
print(1 in f) # contains: True
print(10 in f) # contains: False

__getitem__

作用:使用self[key]訪問時,調用此方法。

如果類把某個屬性定義爲序列時,使用此方法。比如說,實現一個類似列表或者字典的類。

key接受整數爲索引,或者切片。對於set和dict,key必須hashable。key不存在時引發KeyError異常。

class Bar:
    def __init__(self, my_list ):
        self.list = my_list

    def __getitem__(self, item):
        return self.list[item]

b = Bar([0,1,2,3,4,5,6,7,8,9])
print(b[4]) # 4
print(b[4:]) # 切片 [4, 5, 6, 7, 8, 9]

c = Bar({'name':'tom','age':18,'gender':'female'})
print(c['name']) # tom

總結:

class Bar:
    def __init__(self, my_list ):
        self.list = my_list

    def __contains__(self, item):
        print('call contains')
        return item in self.list

    def __iter__(self):
        print('call iter')
        for i in self.list:
            yield i


    def __getitem__(self, item):
        print('call getitem')
        return self.list[item]


c = Bar({'name':'tom','age':18,'gender':'female'})
print('name' in c)
# call contains
# True

print(c['name'])
# call getitem
# tom

for i in c:
    print(i)
# call iter
# name
# age
# gender
  • 迭代對象時的方法查找優先級:__iter__ > __getitem__

  • 成員關係判斷的方法查找優先級: __contains__ > __iter__ > __getitem__

__setitem__

__getitem__ 的訪問類似,是設置值的方法。

__missing__

字典或其子類使用__getitem__ ()調用時,key不存在執行此方法。

class A(dict):
    def __missing__(self,key):
        print('Missing key:',key)
        return 0

a = A()
print(a['k'])

# 輸出
Missing key: k
0

可調用對象

__call__

作用:如果實現了此方法的對象,則爲可調用對象。類中定義一個該方法,實例就可以像函數一樣調用

Python中一切皆對象,函數也不例外。函數之所以能夠被調用,是因爲內部實現了__call__魔術方法。

def foo():
    print(foo.__module__,foo.__name__)


foo() # 輸出  __main__ foo
foo.__call__() # 輸出 __main__ foo
print(dir(foo)) # 收集屬性,包含'__call__'
class Point:
    def __init__(self,x,y):
        self.x = x
        self.y = y

    def __call__(self,*args,**kwargs):
        return '<Point {}:{}>'.format(self.x,self.y)

p = Point(4,5)
print(p()) # <Point 4:5>

class Adder:
    def __call__(self,*args):
        ret = 0
        for x in args:
            ret += x
        self.ret = ret
        return ret

adder = Adder()
print(adder(4,5,6)) # 15
print(adder.ret) # 15
# 

類實例調用和函數調用相比,實例調用的優點:

實例就算定義完成,調用完畢,結果也會記錄在屬性中。下次可以繼續累加。

而函數調用完畢,值就會丟失了。除非形成閉包,由外層函數記錄。

因此,根據此特性,斐波那契數列的類的形式爲:

class Fib():

    def __init__(self):
        self.items = [0,1,1]

    def __len__(self):
        return len(self.items)

    def __call__(self,num):
        l = len(self.items)
        if num < 0:
            raise IndexError()
        elif num < l:  # 如果第n項小於3,則無需計算,輸出初始值
            return self.items[num]

        for i in range(l,num+1):
            x = self.items[i-1] + self.items[i-2]
            self.items.append(x)
            return x

    def __iter__(self):
        return iter(self.items)

    def __getitem__(self, index):
        return self.items[index]


fib = Fib()
print(fib(2))
print(fib.items)
for i,v in enumerate(fib):
    print(i,v)

其他

__slots__

字典爲了提升查詢效率,必須用空間換時間。

一般而言一個對象,屬性多一點,都存儲在字典中便於查詢,問題不大。但是如果數百個對象,那麼字典佔據的空間就有點大了。

是否,可以把屬性字典__dict__省了?

Python提供了__slots__

class A:
    X = 1
    __slots__ = ('y','z')

    def __init__(self):
        self.y = 5
        self.z = 6

    def show(self):
        print(self.X,self.y)

a = A()
a.show()  # 1 5
print(A.__dict__) # {'__module__': '__main__', 'X': 1, '__slots__': ('y', 'z'), '__init__': <function A.__init__ at 0x0000029C0C3B87B8>, 'show': <function A.show at 0x0000029C0C3B8B70>, 'y': <member 'y' of 'A' objects>, 'z': <member 'z' of 'A' objects>, '__doc__': None}

print(a.__dict__) # 異常 AttributeError: 'A' object has no attribute '__dict__'
print(a.__slots__) # ('y', 'z')
print(a.X) # 1 

__slots__告訴解釋器,實例的屬性都叫什麼。一般而言,爲了節約內存,因此使用元組比較合適。

一旦類提供了此屬性,就阻止實例產生__dict__來保存實例的屬性。

如果爲實例動態增加屬性,會異常。而且實例定義的屬性,必須在__slots__中定義。

如果爲類動態增加屬性,是可以的。

注意:__slots__不影響子類實例,不會繼承下去。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章