浅谈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__有不同的表现。
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对象的内建机制,其前瞻性远远超过了自省和反射机制。
到此这篇关于浅谈Python的自省Introspection和反射机制Reflection的文章就介绍到这了,更多相关Python的自省和反射内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!