從Python對象的內建屬性和方法談Python的自省(Introspection)和反射機制(Reflection)

1. 從dir()函數提及

對於dir()這個Python的內置函數,Python進階羣裏的小夥伴們必定不陌生。我不止一次地介紹過這個函數。每當想要了解一個類或類實例包含了什麼屬性和方法時,我都會求助於這個函數。python

>>> a = [3,4,5]
>>> type(a) # 返回a的類型,結果是list類
<class 'list'>
>>> dir(a) # 返回list類實例對象a包含的屬性和方法
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
>>> dir(list) # 返回list類a包含的屬性和方法
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']

對於模塊、內置函數,以及自定義的類,dir()一視同仁,照樣可用。程序員

>>> import math
>>> dir(math) # 返回math模塊包含的子項(子模塊、類、函數、常量等)
['__doc__', '__loader__', '__name__', '__package__', '__spec__', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'ceil', 'copysign', 'cos', 'cosh', 'degrees', 'e', 'erf', 'erfc', 'exp', 'expm1', 'fabs', 'factorial', 'floor', 'fmod', 'frexp', 'fsum', 'gamma', 'gcd', 'hypot', 'inf', 'isclose', 'isfinite', 'isinf', 'isnan', 'ldexp', 'lgamma', 'log', 'log10', 'log1p', 'log2', 'modf', 'nan', 'pi', 'pow', 'radians', 'remainder', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'tau', 'trunc']
>>> dir(max) # 返回內置函數的內建子項
['__call__', '__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__self__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__text_signature__']

讀到這裏,必定會有不少小夥伴會說,個人PyCharm(也多是VSCode或者其餘什麼)也會告訴我,當前的對象有什麼屬性和方法,仍是自動顯示的,不須要我動手。沒錯,IDE的確爲咱們提供了不少便利,可是,你有沒有想過IDE是如何實現這些功能的呢?假如你的任務就是設計一款相似的IDE,你真的不要深刻理解Python內在的機制嗎?shell

2. 內建屬性和方法

下面的代碼中,類Player定義了兩個屬性和一個方法,p是Player的一個實例。調用dir()顯示實例p的屬性和方法,就會發現,除了代碼中定義name,rating和say_hello()外,其餘都是以雙下劃線開頭、以雙下劃線結尾,這些就是傳說中的Python對象的內建屬性和方法。編程

>>> class Player:
		"""玩家類"""
		def __init__(self, name, rating=1800):
			self.name = name
			self.rating = rating
		def say_hello(self):
			"""自報姓名和等級分"""
			print('你們好!我是棋手%s,目前等級分%d分。'%(self.name, self.rating))
		
>>> p = Player('天元浪子')
>>>> p.say_hello()
你們好!我是棋手天元浪子,目前等級分1800分。
>>> for item in dir(p):
		print(item)
	
__class__
__delattr__
__dict__
__dir__
__doc__
__eq__
__format__
__ge__
__getattribute__
__gt__
__hash__
__init__
__init_subclass__
__le__
__lt__
__module__
__ne__
__new__
__reduce__
__reduce_ex__
__repr__
__setattr__
__sizeof__
__str__
__subclasshook__
__weakref__
name
rating
say_hello

這些內建屬性和方法中,彷佛只有__init__和__new__看起來有點面熟,其餘那些都有什麼用途呢?下面,我選其中的幾個演示一下。app

2.1 _ doc _

__doc__是最經常使用的內建屬性,有不少小夥伴並無意識到這一點。一個規範的代碼文件,除了代碼自己,還會提供不少必要信息,好比類、函數的說明,這些說明,咱們稱其爲文檔字符串(DocString)。__doc__就是對象的文檔字符串。ssh

>>> Player.__doc__
'玩家類'
>>> p.__doc__
'玩家類'
>>> p.say_hello.__doc__
'自報姓名和等級分'

這裏顯示的文檔字符串,就是我在定義Player時寫在特定位置的註釋(沒有注意到這一點的小夥伴,請返回查看前面的Player類定義代碼)。編程語言

2.2 _ module _

很容易猜到,內建屬性__mudule__表示對象所屬的模塊。這裏,Player類及其實例,都是當前__main__模塊。如咱們引入一個模塊,更容易說明__module__的含義。函數

>>> Player.__module__
'__main__'
>>> p.__module__
'__main__'
>>> p.say_hello.__module__
'__main__'
>>> import math
>>> math.sin.__module__
'math'

2.3 _ dict _

內建屬性__dict__,是一個由對象的屬性鍵值對構成的字典。類的__dict__和類實例的__dict__有不一樣的表現。學習

>>> p.__dict__
{'name': '天元浪子', 'rating': 1800}
>>> Player.__dict__
mappingproxy({'__module__': '__main__', '__doc__': '玩家類', '__init__': <function Player.__init__ at 0x000002578CF399D8>, 'say_hello': <function Player.say_hello at 0x000002578CF39A68>, '__dict__': <attribute '__dict__' of 'Player' objects>, '__weakref__': <attribute '__weakref__' of 'Player' objects>})

2.4 _ class _

經過類的實例化,能夠獲得一個類實例。那麼如何從一個類實例,逆向獲得類呢?實際上,類實例的內建屬性__class__就是類。咱們徹底能夠用一個實例的__class__去初始化另外一個實例。編碼

>>> pp = p.__class__('零下八段', 2100)
>>> pp.say_hello()
你們好!我是棋手零下八段,目前等級分2100分。

2.5 _ dir _

dir()函數是Python的內置函數,內建方法__dir__相似於dir()函數。。

>>> p.__dir__()
['name', 'rating', '__module__', '__doc__', '__init__', 'say_hello', '__dict__', '__weakref__', '__repr__', '__hash__', '__str__', '__getattribute__', '__setattr__', '__delattr__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__new__', '__reduce_ex__', '__reduce__', '__subclasshook__', '__init_subclass__', '__format__', '__sizeof__', '__dir__', '__class__']

2.6 _ getattribute _

顧名思義,__getattribute__返回對象的屬性——其實是屬性或方法。這是一個內建方法,使用的時候其後必須有圓括號,參數是指定的屬性或方法的名字。

>>> p.__getattribute__('name')
'天元浪子'
>>> p.__getattribute__('rating')
1800
>>> p.__getattribute__('say_hello')
<bound method Player.say_hello of <__main__.Player object at 0x000002578CF2CA88>>
>>> p.__getattribute__('say_hello')()
你們好!我是棋手天元浪子,目前等級分1800分。

3. 動態加載及調用

學習任何一門編程語言的初級階段,咱們幾乎都會遇到一個共同的問題:動態建立一個變量或對象。在這裏,「動態」只是強調變量或對象名稱不是由程序員決定,而是由另外的參與方(好比交互程序中的操做者,C/S或B/S程序中的客戶端)決定。也許不是一個準確的說法,但我想不出一個更好的詞彙來表述此種應用需求。

以Python爲例:從鍵盤上讀入一個字符串,以該字符串爲名建立一個整型對象,令其值等於3。一般,這樣的問題咱們使用exec()函數就能夠解決。爲何不是eval()函數呢?eval()函數僅是計算一個字符串形式的表達式,沒法完成賦值操做。

>>> var_name = input('請輸入整型對象名:')
請輸入整型對象名:x
>>> exec('%s=3'%var_name)
>>> x
3

理解了「動態」的概念,咱們來看看如何動態加載模塊、如何動態調用對象等

3.1 動態加載模塊

按照Python編碼規範,腳本文件通常會在編碼格式聲明和文檔說明以後統一導入模塊。有些狀況下,代碼須要根據程序運行時的具體狀況,臨時導入相應的模塊——一般,這種狀況下,導入的模塊命是由一個字符串指定的。下面的代碼給出了動態加載模塊的實例。

>>> os.getcwd() # 此時沒有導入os模塊,因此拋出異常
Traceback (most recent call last):
  File "<pyshell#158>", line 1, in <module>
    os.getcwd()
NameError: name 'os' is not defined
>>> os = __import__('os') # 動態導入'os'模塊
>>> os.getcwd()
'C:\\Users\\xufive\\AppData\\Local\\Programs\\Python\\Python37'

3.2 經過對象名取得對象

這個需求聽起來有點奇怪,但也有不少人會遇到。Player類實例p爲例,若是咱們只有字符串’p’,怎樣才能獲得p實例呢?咱們知道內置函數globals()返回全局的對象字典,locals()返回所處層次的對象字典,這兩個字典的鍵就是對象名的字符串。有了這個思路,就很容易經過對象名取得對象了。

>>> obj = globals().get('p', None)
>>> obj
<__main__.Player object at 0x000002578CF2CA88>
>>> obj.say_hello()
你們好!我是棋手天元浪子,目前等級分1800分。

3.3 動態調用對象

動態調用對象最典型的應用是服務接口的實現。假如客戶端經過發送服務的名字字符串來調用服務端的一個服務,名字字符串和服務有者一一對應的關係。若是沒有動態調用,代碼恐怕就得寫成下面這個樣子。

if cmd == 'service_1':
	serv.service_1()
elif cmd == 'service_2':
	serv.service_2()
elif cmd == 'service_3':
	serv.service_3()
... ...

下面的代碼,演示了服務端如何根據接收到的命令動態調用對應的服務。

>>> class ServiceDemo:
	def service_1(self):
		print('Run service_1...')
	def service_2(self):
		print('Run service_2...')
	def onconnect(self, cmd):
		if hasattr(self, cmd):
			getattr(self, cmd)()
		else:
			print('命令錯誤')

			
>>> serv = ServiceDemo()
>>> serv.onconnect('service_1')
Run service_1...
>>> serv.onconnect('service_2')
Run service_2...
>>> serv.onconnect('hello')
命令錯誤

4. 自省和反射機制

是時候說說自省和反射了。可是,截止到這裏,我已經把自省和反射所有講完了,只是沒有使用自省和反射這兩個詞罷了。僅從這一點,就能夠說明,自省和反射是徹底多餘的概念。若是有小夥伴搞不清楚這兩個概念,那也徹底沒有關係,一點兒都不會影響你對編程的理解。

所謂的自省,就是對象自身提供能夠查看自身屬性、方法、類型的手段。內建方法__dir__不正是對象的自省嗎?另外,內置函數dir()、type()、isinstance()均可以提供相似自省的部分或所有功能。

反射機制是Java和PHP等語言提供的一個特性,準確描述起來有些費勁,簡而言之,就是在運行態能夠獲取對象的屬性和方法,並隨時調用他們,最典型的應用就是經過字符串形式的對象名獲取對象。這不就是我說的「動態加載和調用」嗎?

寫道這裏,不禁地再次致敬龜叔當年的遠見卓識:早在Java誕生前好多年,龜叔就已經全面地規劃了Python對象的內建機制,其前瞻性遠遠超過了自省和反射機制。

相關文章
相關標籤/搜索