从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对象的内建机制,其前瞻性远远超过了自省和反射机制。

相关文章
相关标签/搜索