从Python对象的内建属性和方法谈Python的自省(Introspection)和反射机制(Reflection)

1. 从dir()函数说起

对于dir()这个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内在的机制吗?

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__看起来有点面熟,其他那些都有什么用途呢?下面,我选其中的几个演示一下。

2.1 doc

__doc__是最常用的内建属性,有很多小伙伴并没有意识到这一点。一个规范的代码文件,除了代码本身,还会提供很多必要信息,比如类、函数的说明,这些说明,我们称其为文档字符串(DocString)。__doc__就是对象的文档字符串。

>>> 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__有不同的表现。

扫描二维码关注公众号,回复: 11456231 查看本文章
>>> 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对象的内建机制,其前瞻性远远超过了自省和反射机制。

猜你喜欢

转载自blog.csdn.net/xufive/article/details/107607562