<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Python進(jìn)階——如何正確使用魔法方法?(上)

          共 17283字,需瀏覽 35分鐘

           ·

          2021-03-04 00:06

          閱讀本文大約需要 10 分鐘。

          在做 Python 開發(fā)時(shí),我們經(jīng)常會(huì)遇到以雙下劃線開頭和結(jié)尾的方法,例如 __init__、__new____getattr__、__setitem__ 等等,這些方法我們通常稱之為「魔法方法」,而使用這些「魔法方法」,我們可以非常方便地給類添加特殊的功能。

          這篇文章,我們就來(lái)分析一下,Python 中的魔法方法都有哪些?使用這些魔法方法,我們可以實(shí)現(xiàn)哪些實(shí)用的功能?

          魔法方法概覽

          首先,我們先對(duì) Python 中的魔法方法進(jìn)行歸類,常見的魔法方法大致可分為以下幾類:

          • 構(gòu)造與初始化
          • 類的表示
          • 訪問控制
          • 比較操作
          • 容器類操作
          • 可調(diào)用對(duì)象
          • 序列化

          由于魔法方法分類較多,這篇文章我們先來(lái)看前幾個(gè):構(gòu)造與初始化、類的表示、訪問控制。剩下的魔法方法,我們會(huì)在下一篇文章進(jìn)行分析講解。

          構(gòu)造與初始化

          首先,我們來(lái)看關(guān)于構(gòu)造與初始化相關(guān)的魔法方法,主要包括以下幾種:

          • __init__
          • __new__
          • __del__

          __init__

          關(guān)于構(gòu)造與初始化的魔法方法,我們使用最頻繁的一個(gè)就是 __init__ 了。

          我們?cè)诙x類的時(shí)候,通常都會(huì)去定義構(gòu)造方法,它的作用就是在初始化一個(gè)對(duì)象時(shí),定義這個(gè)對(duì)象的初始值。

          # coding: utf8

          class Person(object):

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

          p1 = Person('張三'25)
          p2 = Person('李四'30)

          __new__

          在初始化一個(gè)類的屬性時(shí),除了使用 __init__ 之外,還可以使用 __new__ 這個(gè)方法。

          我們?cè)谄綍r(shí)開發(fā)中使用的雖然不多,但是經(jīng)常能夠在開源框架中看到它的身影。實(shí)際上,這才是「真正的構(gòu)造方法」。

          # coding: utf8

          class Person(object):

              def __new__(cls, *args, **kwargs):
                  print "call __new__"
                  return object.__new__(cls, *args, **kwargs)

              def __init__(self, name, age):
                  print "call __init__"
                  self.name = name
                  self.age = age

          p = Person("張三"20)

          # Output:
          # call __new__
          # call __init__

          從例子我們可以看到,__new__ 會(huì)在對(duì)象實(shí)例化時(shí)第一個(gè)被調(diào)用,然后才會(huì)調(diào)用 __init__,它們的區(qū)別如下:

          • __new__ 的第一個(gè)參數(shù)是 cls,而 __init__ 的第一個(gè)參數(shù)是 self
          • __new__ 返回值是一個(gè)實(shí)例對(duì)象,而 __init__ 沒有任何返回值,只做初始化操作
          • __new__ 由于返回的是一個(gè)實(shí)例對(duì)象,所以它可以給所有實(shí)例進(jìn)行統(tǒng)一的初始化操作

          了解了它們之間的區(qū)別,我們來(lái)看 __new__ 在什么場(chǎng)景下使用?

          由于 __new__ 優(yōu)先于 __init__ 調(diào)用,而且它返回的是一個(gè)實(shí)例,所以我們可以利用這個(gè)特性,在 __new__ 方法中,每次返回同一個(gè)實(shí)例來(lái)實(shí)現(xiàn)一個(gè)單例類:

          # coding: utf8

          class Singleton(object):
              """單例"""
              _instance = None
              def __new__(cls, *args, **kwargs):
                  if not cls._instance:
                      cls._instance = super(Singleton, cls).__new__(cls, *args, **kwargs)
                  return cls._instance

          class MySingleton(Singleton):
              pass

          a = MySingleton()
          b = MySingleton()

          assert a is b # True

          另外一個(gè)使用場(chǎng)景是,當(dāng)我們需要繼承內(nèi)置類時(shí),例如想要繼承 intstr、tuple,就無(wú)法使用 __init__ 來(lái)初始化了,只能通過 __new__ 來(lái)初始化數(shù)據(jù):

          # coding: utf8

          class g(float):
              """千克轉(zhuǎn)克"""
              def __new__(cls, kg):
                  return float.__new__(cls, kg * 2)

          a = g(50# 50千克轉(zhuǎn)為克
          print a   # 100
          print a + 100 # 200 由于繼承了float,所以可以直接運(yùn)算,非常方便!

          在這個(gè)例子中,我們實(shí)現(xiàn)了一個(gè)類,這個(gè)類繼承了 float,之后,我們就可以對(duì)這個(gè)類的實(shí)例進(jìn)行計(jì)算了,是不是很神奇?

          除此之外,__new__ 比較多的應(yīng)用場(chǎng)景是配合「元類」使用,關(guān)于「元類」的原理,我會(huì)在后面的文章中講到。

          __del__

          __del__ 這個(gè)方法就是我們經(jīng)常說(shuō)的「析構(gòu)方法」,也就是在對(duì)象被垃圾回收時(shí)被調(diào)用。

          但是請(qǐng)注意,當(dāng)我們執(zhí)行 del obj 時(shí),這個(gè)方法不一定會(huì)執(zhí)行。

          由于 Python 是通過引用計(jì)數(shù)來(lái)進(jìn)行垃圾回收的,如果這個(gè)實(shí)例在執(zhí)行 del 時(shí),還被其他對(duì)象引用,那么就不會(huì)觸發(fā)執(zhí)行 __del__ 方法。

          我們來(lái)看一個(gè)例子:

          class Person(object):
              def __del__(self):
                  print '__del__'

          我們定義了一個(gè)帶有 __del__ 方法的類,此時(shí)我們直接執(zhí)行:

          a = Person()
          print 'exit'

          # Output:
          # exit
          # __del__

          由于我們沒有對(duì)實(shí)例進(jìn)行任何引用操作時(shí),所以 __del__ 在程序退出時(shí)被調(diào)用。

          如果我們顯示執(zhí)行 del obj,如下:

          a = Person()
          del a    # 手動(dòng)銷毀對(duì)象
          print 'exit'

          # Output:
          # __del__
          # exit

          同樣地,由于實(shí)例沒有被其他對(duì)象所引用,當(dāng)我們手動(dòng)銷毀這個(gè)實(shí)例時(shí),__del__ 被調(diào)用后程序正常退出。

          如果這個(gè)對(duì)象被其他對(duì)象所引用:

          a = Person()
          b = a   # b引用a
          del a   # 手動(dòng)銷毀 不觸發(fā)__del__
          print 'exit'

          # Output:
          # exit
          # __del__

          可以看到,如果這個(gè)實(shí)例有被其他對(duì)象引用,盡管我們手動(dòng)銷毀這個(gè)實(shí)例,但不會(huì)觸發(fā) __del__ 方法,而是在程序正常退出時(shí)被調(diào)用執(zhí)行。

          通常來(lái)說(shuō),__del__ 這個(gè)方法我們很少會(huì)使用到,除非需要在顯示執(zhí)行 del 執(zhí)行特殊清理邏輯的場(chǎng)景中才會(huì)使用到。

          但另一方面,也給我們一個(gè)提醒,當(dāng)我們?cè)趯?duì)文件、Socket 進(jìn)行操作時(shí),如果要想安全地關(guān)閉和銷毀這些對(duì)象,最好是在 try 異常塊后的 finally 中進(jìn)行關(guān)閉和釋放操作,從而避免資源的泄露。

          類的表示

          接下來(lái),我們來(lái)看關(guān)于類的表示相關(guān)的魔法方法,主要包括以下幾種:

          • __str__ / __repr__
          • __unicode__
          • __hash__ / __eq__
          • __nozero__

          __str__/__repr__

          關(guān)于 __str__ 和 __repr__ 這 2 個(gè)魔法方法,非常類似,很多人區(qū)分不出它們有什么不同,我們來(lái)看幾個(gè)例子,就能理解這 2 個(gè)方法的效果:

          >>> a = 'hello'
          >>> str(a)
          'hello'
          >>> '%s' % a # 調(diào)用__str__
          'hello'

          >>> repr(a)  # 對(duì)象a的標(biāo)準(zhǔn)表示 也就是a是如何創(chuàng)建的
          "'hello'"
          >>> '%r' % a # 調(diào)用__repr__
          "'hello'"

          >>> import datetime
          >>> b = datetime.datetime.now()
          >>> str(b)
          '2017-02-22 12:28:40.923379'
          >>> print b  # 等同于print str(b)
          2017-02-22 12:28:40.923379

          >>> repr(b)  # 展示對(duì)象b的標(biāo)準(zhǔn)創(chuàng)建方式(如何創(chuàng)建的)
          'datetime.datetime(2017, 2, 22, 12, 28, 40, 923379)'
          >>> b       # 等同于print repr(b)
          datetime.datetime(2017222122840923379)

          >>> c = eval(repr(b)) # repr(b)目標(biāo)針對(duì)于機(jī)器 所以可執(zhí)行
          >>> c
          datetime.datetime(2017222122840923379)

          從上述例子中我們可以看出這 2 個(gè)方法的區(qū)別:

          • __str__ 強(qiáng)調(diào)可讀性,而 __repr__ 強(qiáng)調(diào)準(zhǔn)確性 / 標(biāo)準(zhǔn)性
          • __str__ 的目標(biāo)人群是用戶,而 __repr__ 的目標(biāo)人群是機(jī)器,__repr__ 返回的結(jié)果是可執(zhí)行的,通過 eval(repr(obj)) 可以正確運(yùn)行
          • 占位符 %s 調(diào)用的是 __str__,而 %r 調(diào)用的是 __repr__ 方法

          所以,我們?cè)趯?shí)際中開發(fā)中定義類時(shí),一般這樣使用:

          # coding: utf8

          class Person(object):

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

              def __str__(self):
                  # 格式化 友好對(duì)用戶展示
                  return 'name: %s, age: %s' % (self.name, self.age)

              def __repr__(self):
                  # 標(biāo)準(zhǔn)化展示
                  return "Person('%s', %s)" % (self.name, self.age)

          person = Person('zhangsan'20)

          # 強(qiáng)調(diào)對(duì)用戶友好
          print str(person)       # name: zhangsan, age: 20 
          print '%s' % person     # name: zhangsan, age: 20

          # 強(qiáng)調(diào)對(duì)機(jī)器友好 結(jié)果 eval 可執(zhí)行
          print repr(person)  # Person('zhangsan', 20)
          print '%r' % person     # Person('zhangsan', 20)

          明白了它們之間的區(qū)別,我們?cè)偎伎家幌?,如果只定義了 __str__ 或 __repr__ 其中一個(gè),那會(huì)是什么結(jié)果?

          只定義 __str__,但沒有定義 __repr__

          # coding: utf8

          class Person(object):

              def __init__(self, name, age):
                  self.name = name
                  self.age = age
                  
              def __str__(self):
                  return 'name: %s, age: %s' % (self.name, self.age)

          person = Person('zhangsan'20)

          print str(person)       # name: zhangsan, age: 20 
          print '%s' % person     # name: zhangsan, age: 20

          print repr(person)  # <__main__.Person object at 0x10bee9390>
          print '%r' % person     # <__main__.Person object at 0x10bee9390>

          只定義 __repr__,但沒有定義 __str__

          # coding: utf8

          class Person(object):

              def __init__(self, name, age):
                  self.name = name
                  self.age = age
                  
              def __repr__(self):
                  return "Person('%s', %s)" % (self.name, self.age)

          person = Person('zhangsan'20)

          print str(person)       # Person('zhangsan', 20)
          print '%s' % person     # Person('zhangsan', 20)

          print repr(person)  # Person('zhangsan', 20)
          print '%r' % person     # Person('zhangsan', 20)

          從例子中我們可以看到結(jié)果:

          • 如果只定義了 _str__,那么 repr(person) 輸出 <__main__.Person object at 0x10bee9390>
          • 如果只定義了 __repr__,那么 str(person) 與 repr(person) 結(jié)果是相同的

          也就是說(shuō),__repr__ 在表示類時(shí),是一級(jí)的,如果只定義它,那么 __str__ = __repr__。

          而 __str__ 展示類時(shí)是次級(jí)的,如果沒有定義 __repr__,那么 repr(person) 將會(huì)展示缺省的定義。

          __unicode__

          如果一個(gè)類定義了 __unicode__ 方法,那么在調(diào)用 unicode(obj) 時(shí),此方法將被調(diào)用,但是其返回值類型是 unicode

          # coding: utf8

          class Person(object):

              def __unicode__(self):
                  # 這里不是u'hello'
                  return 'hello'
              
          person = Person()
          print unicode(person)           # helllo
          print type(unicode(person))     # <type 'unicode'>

          從例子中我們可以看到, 雖然我們定義的 __unicode__ 返回值不是 unicode 類型,但在輸出時(shí),程序會(huì)自動(dòng)轉(zhuǎn)換成 unicode 類型。

          這個(gè)方法在開發(fā)中一般很少使用,通常我們只需要定義 __str__ 即可。

          __hash__/__eq__

          __hash__ 方法返回一個(gè)整數(shù),用來(lái)表示實(shí)例對(duì)象的唯一標(biāo)識(shí),配合 __eq__ 方法,可以判斷兩個(gè)對(duì)象是否相等:

          # coding: utf8

          class Person(object):
              def __init__(self, uid):
                  self.uid = uid
                  
           def __repr__(self):
                  return 'Person(%s)' % self.uid
                  
              def __hash__(self):
                  return self.uid
              
              def __eq__(self, other):
                  return self.uid == other.uid
              
          p1 = Person(1)
          p2 = Person(1)
          p1 == p2    # True

          p3 = Person(2)
          print set([p1, p2, p3]) # 根據(jù)唯一標(biāo)識(shí)去重輸出 set([Person(1), Person(2)])

          如果我們需要判斷兩個(gè)對(duì)象是否相等,只需要我們重寫 __hash__ 和 __eq__ 方法就可以了。

          此外,當(dāng)我們使用 set 時(shí),在 set 中存放這些對(duì)象,也會(huì)根據(jù)這兩個(gè)方法進(jìn)行去重操作。

          __nonzero__

          當(dāng)調(diào)用 bool(obj) 時(shí),會(huì)調(diào)用 __nonzero__ 方法,返回 True 或 False

          # coding: utf8

          class Person(object):
              def __init__(self, uid):
                  self.uid = uid

              def __nonzero__(self):
                  return self.uid > 10
              
          p1 = Person(1)
          p2 = Person(15)
          print bool(p1)  # False
          print bool(p2)  # True

          在 Python3 中,__nonzero__ 被重命名為 __bool__

          訪問控制

          接下來(lái),我們來(lái)看關(guān)于訪問控制的魔法方法,主要包括以下幾種:

          • __setattr__:通過「.」設(shè)置屬性或 setattr(key, value) 設(shè)置屬性時(shí)調(diào)用
          • __getattr__:訪問不存在的屬性時(shí)調(diào)用
          • __delattr__:刪除某個(gè)屬性時(shí)調(diào)用
          • __getattribute__:訪問任意屬性或方法時(shí)調(diào)用

          我們來(lái)看使用這些方法的完整例子:

          # coding: utf8

          class Person(object):

              def __setattr__(self, key, value):
                  """屬性賦值"""
                  if key not in ('name''age'):
                      return
                  if key == 'age' and value < 0:
                      raise ValueError()
                  super(Person, self).__setattr__(key, value)

              def __getattr__(self, key):
                  """訪問某個(gè)不存在的屬性"""
                  return 'unknown'

              def __delattr__(self, key):
                  """刪除某個(gè)屬性"""
                  if key == 'name':
                      raise AttributeError()
                  super(Person, self).__delattr__(key)

              def __getattribute__(self, key):
                  """所有屬性/方法調(diào)用都經(jīng)過這里"""
                  if key == 'money':
                      return 100
                  if key == 'hello':
                      return self.say
                  return super(Person, self).__getattribute__(key)

              def say(self):
                  return 'hello'
              
          p1 = Person()
          p1.name = 'zhangsan' # 調(diào)用__setattr__
          p1.age = 20          # 調(diào)用__setattr__
          print p1.name        # zhangsan
          print p1.age        # 20

          setattr(p1, 'name''lisi'# 調(diào)用__setattr__
          setattr(p1, 'age'30)   # 調(diào)用__setattr__
          print p1.name              # lisi
          print p1.age              # 30

          p1.gender = 'male'  # __setattr__中忽略對(duì)gender賦值
          print p1.gender     # gender不存在 所以會(huì)調(diào)用__getattr__返回unknown

          print p1.money      # money不存在 在__getattribute__中返回100

          print p1.say()      # hello
          print p1.hello()    # hello 調(diào)用__getattribute__ 間接調(diào)用say方法

          del p1.name      # __delattr__中引發(fā)AttributeError

          p2 = Person()
          p2.age = -1      # __setattr__中引發(fā)ValueError

          我們仔細(xì)看一下這個(gè)例子,我已經(jīng)添加好了詳細(xì)的注釋。

          __setattr__

          先來(lái)說(shuō) __setattr__,當(dāng)我們?cè)诮o一個(gè)對(duì)象進(jìn)行屬性賦值時(shí),都會(huì)經(jīng)過這個(gè)方法,在這個(gè)例子中,我們只允許對(duì) name 和 age 這 2 個(gè)屬性進(jìn)行賦值,忽略了 gender 屬性,除此之外,我們還對(duì) age 賦值進(jìn)行了校驗(yàn)。

          通過 __setattr__ 方法,我們可以非常方便地對(duì)屬性賦值進(jìn)行控制。

          __getattr__

          再來(lái)看 __getattr__,由于我們?cè)?nbsp;__setattr__ 中忽略了對(duì) gender 屬性的賦值,所以當(dāng)訪問這個(gè)不存在的屬性時(shí),會(huì)調(diào)用 __getattr__ 方法,在這個(gè)方法中返回了默認(rèn)值 unknown。

          很多同學(xué)以為這個(gè)方法與 __setattr__ 方法對(duì)等的,一個(gè)是賦值,一個(gè)是獲取。其實(shí)不然,__getattr__ 只有在訪問「不存在的屬性」時(shí)才會(huì)被調(diào)用,這里我們需要注意。

          __getattribute__

          了解了 __getattr__ 后,還有一個(gè)和它非常類似的方法:__getattribute__。

          很多人經(jīng)常把這個(gè)方法和 __getattr__ 混淆,通過例子我們可以看出,它與前者的區(qū)別在于:

          • __getattr__ 只有在訪問不存在的屬性時(shí)被調(diào)用,而 __getattribute__ 在訪問任意屬性時(shí)都會(huì)被調(diào)用
          • __getattr__ 只針對(duì)屬性訪問,而__getattribute__ 不僅針對(duì)所有屬性訪問,還包括方法調(diào)用

          在上面的例子,雖然我們沒有定義 money 屬性和 hello 方法,但是在 __getattribute__ 里攔截到了這個(gè)屬性和方法,就可以對(duì)其執(zhí)行不同的邏輯。

          __delattr__

          最后,我們來(lái)看 __delattr__,它比較簡(jiǎn)單,當(dāng)刪除對(duì)象的某個(gè)屬性時(shí),這個(gè)方法會(huì)被調(diào)用,所以它一般會(huì)用在刪除屬性前的校驗(yàn)場(chǎng)景中使用。

          總結(jié)

          這篇文章,我們主要介紹了 Python 中常見的魔法方法,主要有構(gòu)造與初始化、類的表示、訪問控制這 3 個(gè)模塊。

          構(gòu)造與初始化的魔法方法,常常用在類的初始化過程中,其中 __init__一般用于實(shí)例初始化, 而 __new__ 可以改變初始化實(shí)例的行為,通過它我們可以實(shí)現(xiàn)一個(gè)單例或者繼承一個(gè)內(nèi)置類。

          關(guān)于類的表示的魔法方法,比較常用的,當(dāng)我們想表示一個(gè)類時(shí),可以使用 __str__ 或 __repr__ 方法,當(dāng)需要判斷兩個(gè)對(duì)象是否相等時(shí),可以使用 __hash__ 和 __eq__ 方法。

          關(guān)于訪問控制的魔法方法,它可以控制實(shí)例的屬性賦值、屬性訪問、方法訪問、屬性刪除等操作,這對(duì)于我們實(shí)現(xiàn)一個(gè)復(fù)雜功能的類有很大幫助。

          在下一篇文章,我們會(huì)繼續(xù)分析剩下的魔法方法,主要包括關(guān)于比較操作、容器類操作、可調(diào)用對(duì)象、序列化相關(guān)的魔法方法。


          更多閱讀



          2020 年最佳流行 Python 庫(kù) Top 10


          2020 Python中文社區(qū)熱門文章 Top 10


          5分鐘快速掌握 Python 定時(shí)任務(wù)框架

          特別推薦





          點(diǎn)擊下方閱讀原文加入社區(qū)會(huì)員

          瀏覽 38
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  怡春院国产 | 国产乱人激情h在线观看 | 日韩国产欧美成人 | 北条麻妃av在线播放 | 天天操人人摸视频ⅩⅩⅩA√ |