简单谈谈python的反射机制


Posted in Python onJune 28, 2016

对编程语言比较熟悉的朋友,应该知道“反射”这个机制。Python作为一门动态语言,当然不会缺少这一重要功能。然而,在网络上却很少见到有详细或者深刻的剖析论文。下面结合一个web路由的实例来阐述python的反射机制的使用场景和核心本质。

一、前言

def f1():
  print("f1是这个函数的名字!")
 
s = "f1"
print("%s是个字符串" % s)

在上面的代码中,我们必须区分两个概念,f1和“f1"。前者是函数f1的函数名,后者只是一个叫”f1“的字符串,两者是不同的事物。我们可以用f1()的方式调用函数f1,但我们不能用"f1"()的方式调用函数。说白了就是,不能通过字符串来调用名字看起来相同的函数!

二、web实例

考虑有这么一个场景,根据用户输入的url的不同,调用不同的函数,实现不同的操作,也就是一个url路由器的功能,这在web框架里是核心部件之一。下面有一个精简版的示例:

首先,有一个commons模块,它里面有几个函数,分别用于展示不同的页面,代码如下:

def login():
  print("这是一个登陆页面!")

def logout():
  print("这是一个退出页面!")

def home():
  print("这是网站主页面!")

其次,有一个visit模块,作为程序入口,接受用户输入,展示相应的页面,代码如下:(这段代码是比较初级的写法)

import commons

def run():
  inp = input("请输入您想访问页面的url: ").strip()
  if inp == "login":
    commons.login()
  elif inp == "logout":
    commons.logout()
  elif inp == "home":
    commons.home()
  else:
    print("404")

if __name__ == '__main__':
  run()

我们运行visit.py,输入:home,页面结果如下:

请输入您想访问页面的url: home
这是网站主页面!

这就实现了一个简单的WEB路由功能,根据不同的url,执行不同的函数,获得不同的页面。

然而,让我们考虑一个问题,如果commons模块里有成百上千个函数呢(这非常正常)?。难道你在visit模块里写上成百上千个elif?显然这是不可能的!那么怎么破?

三、反射机制

仔细观察visit中的代码,我们会发现用户输入的url字符串和相应调用的函数名好像!如果能用这个字符串直接调用函数就好了!但是,前面我们已经说了字符串是不能用来调用函数的。为了解决这个问题,python为我们提供一个强大的内置函数:getattr!我们将前面的visit修改一下,代码如下:

import commons

def run():
  inp = input("请输入您想访问页面的url: ").strip()
  func = getattr(commons,inp)
  func()
 
if __name__ == '__main__':
  run()

首先说明一下getattr函数的使用方法:它接收2个参数,前面的是一个对象或者模块,后面的是一个字符串,注意了!是个字符串!

例子中,用户输入储存在inp中,这个inp就是个字符串,getattr函数让程序去commons这个模块里,寻找一个叫inp的成员(是叫,不是等于),这个过程就相当于我们把一个字符串变成一个函数名的过程。然后,把获得的结果赋值给func这个变量,实际上func就指向了commons里的某个函数。最后通过调用func函数,实现对commons里函数的调用。这完全就是一个动态访问的过程,一切都不写死,全部根据用户输入来变化。

执行上面的代码,结果和最开始的是一样的。

这就是python的反射,它的核心本质其实就是利用字符串的形式去对象(模块)中操作(查找/获取/删除/添加)成员,一种基于字符串的事件驱动!

这段话,不一定准确,但大概就是这么个意思。

四、进一步完善

上面的代码还有个小瑕疵,那就是如果用户输入一个非法的url,比如jpg,由于在commons里没有同名的函数,肯定会产生运行错误,具体如下:

请输入您想访问页面的url: jpg
Traceback (most recent call last):
 File "F:/Python/pycharm/s13/reflect/visit.py", line 16, in <module>
  run()
 File "F:/Python/pycharm/s13/reflect/visit.py", line 11, in run
  func = getattr(commons,inp)
AttributeError: module 'commons' has no attribute 'jpg'

那怎么办呢?其实,python考虑的很全面了,它同样提供了一个叫hasattr的内置函数,用于判断commons中是否具有某个成员。我们将代码修改一下:

import commons
 
def run():
  inp = input("请输入您想访问页面的url: ").strip()
  if hasattr(commons,inp):
    func = getattr(commons,inp)
    func()
  else:
    print("404")
 
if __name__ == '__main__':
  run()

通过hasattr的判断,可以防止非法输入错误,并将其统一定位到错误页面。

其实,研究过python内置函数的朋友,应该注意到还有delattrsetattr两个内置函数。从字面上已经很好理解他们的作用了。

python的四个重要内置函数:getattrhasattrdelattrsetattr较为全面的实现了基于字符串的反射机制。他们都是对内存内的模块进行操作,并不会对源文件进行修改。

五、动态导入模块

上面的例子是在某个特定的目录结构下才能正常实现的,也就是commons和visit模块在同一目录下,并且所有的页面处理函数都在commons模块内。如下图:

但在现实使用环境中,页面处理函数往往被分类放置在不同目录的不同模块中,也就是如下图:

难道我们要在visit模块里写上一大堆的import 语句逐个导入account、manage、commons模块吗?要是有1000个这种模块呢?

刚才我们分析完了基于字符串的反射,实现了动态的函数调用功能,我们不禁会想那么能不能动态导入模块呢?这完全是可以的!

python提供了一个特殊的方法:__import__(字符串参数)。通过它,我们就可以实现类似的反射功能。__import__()方法会根据参数,动态的导入同名的模块。

我们再修改一下上面的visit模块的代码。

def run():
  inp = input("请输入您想访问页面的url: ").strip()
  modules, func = inp.split("/")
  obj = __import__(modules)
  if hasattr(obj, func):
    func = getattr(obj, func)
    func()
  else:
    print("404")
 
if __name__ == '__main__':
  run()

运行一下:

请输入您想访问页面的url: commons/home
这是网站主页面!
请输入您想访问页面的url: account/find
这是一个查找功能页面!

我们来分析一下上面的代码:

首先,我们并没有定义任何一行import语句;

其次,用户的输入inp被要求为类似“commons/home”这种格式,其实也就是模拟web框架里的url地址,斜杠左边指向模块名,右边指向模块中的成员名。

然后,modules,func = inp.split("/")处理了用户输入,使我们获得的2个字符串,并分别保存在modules和func变量里。

接下来,最关键的是obj = __import__(modules)这一行,它让程序去导入了modules这个变量保存的字符串同名的模块,并将它赋值给obj变量。

最后的调用中,getattr去modules模块中调用func成员的含义和以前是一样的。

总结:通过__import__函数,我们实现了基于字符串的动态的模块导入。

同样的,这里也有个小瑕疵!

如果我们的目录结构是这样的:

 

那么在visit的模块调用语句中,必须进行修改,我们想当然地会这么做:

def run():
  inp = input("请输入您想访问页面的url: ").strip()
  modules, func = inp.split("/")
  obj = __import__("lib." + modules)   #注意字符串的拼接
  if hasattr(obj, func):
    func = getattr(obj, func)
    func()
  else:
    print("404")
 
if __name__ == '__main__':
  run()

改了这么一个地方:obj = __import__("lib." + modules),看起来似乎没什么问题,和import lib.commons的传统方法类似,但实际上运行的时候会有错误。

请输入您想访问页面的url: commons/home
404
请输入您想访问页面的url: account/find
404

为什么呢?因为对于lib.xxx.xxx.xxx这一类的模块导入路径,__import__默认只会导入最开头的圆点左边的目录,也就是“lib”。我们可以做个测试,在visit同级目录内新建一个文件,代码如下:

obj = __import__("lib.commons")
print(obj)

执行结果:

<module 'lib' (namespace)>

这个问题怎么解决呢?加上fromlist = True参数即可!

def run():
  inp = input("请输入您想访问页面的url: ").strip()
  modules, func = inp.split("/")
  obj = __import__("lib." + modules, fromlist=True) # 注意fromlist参数
  if hasattr(obj, func):
    func = getattr(obj, func)
    func()
  else:
    print("404")
 
if __name__ == '__main__':
  run()

至此,动态导入模块的问题基本都解决了,只剩下最后一个,那就是万一用户输入错误的模块名呢?比如用户输入了somemodules/find,由于实际上不存在somemodules这个模块,必然会报错!那有没有类似上面hasattr内置函数这么个功能呢?答案是没有!碰到这种,你只能通过异常处理来解决。

六、最后的思考

可能有人会问python不是有两个内置函数execeval吗?他们同样能够执行字符串。比如:

exec("print('haha')")

结果:

haha

那么直接使用它们不行吗?非要那么费劲地使用getattr__import__干嘛?

其实,在上面的例子中,围绕的核心主题是如何利用字符串驱动不同的事件,比如导入模块、调用函数等等,这些都是python的反射机制,是一种编程方法、设计模式的体现,凝聚了高内聚、松耦合的编程思想,不能简单的用执行字符串来代替。当然,exec和eval也有它的舞台,在web框架里也经常被使用。

Python 相关文章推荐
python模拟登陆阿里妈妈生成商品推广链接
Apr 03 Python
举例讲解Python面相对象编程中对象的属性与类的方法
Jan 19 Python
学习python 之编写简单乘法运算题
Feb 27 Python
Python文件的读写和异常代码示例
Oct 31 Python
python ansible服务及剧本编写
Dec 29 Python
Python使用selenium实现网页用户名 密码 验证码自动登录功能
May 16 Python
Python根据指定日期计算后n天,前n天是哪一天的方法
May 29 Python
Django继承自带user表并重写的例子
Nov 18 Python
Python +Selenium解决图片验证码登录或注册问题(推荐)
Feb 09 Python
Python中pyecharts安装及安装失败的解决方法
Feb 18 Python
浅谈python中频繁的print到底能浪费多长时间
Feb 21 Python
Python基于unittest实现测试用例执行
Nov 25 Python
Python实现带百分比的进度条
Jun 28 #Python
Python中的字符串替换操作示例
Jun 27 #Python
Python的string模块中的Template类字符串模板用法
Jun 27 #Python
Python的Flask框架及Nginx实现静态文件访问限制功能
Jun 27 #Python
总结网络IO模型与select模型的Python实例讲解
Jun 27 #Python
结合Python的SimpleHTTPServer源码来解析socket通信
Jun 27 #Python
Python的Tornado框架的异步任务与AsyncHTTPClient
Jun 27 #Python
You might like
php版微信公众账号第三方管理工具开发简明教程
2016/09/23 PHP
php关联数组与索引数组及其显示方法
2018/03/12 PHP
php日志函数error_log用法实例分析
2019/09/23 PHP
JavaScript高级程序设计(第3版)学习笔记2 js基础语法
2012/10/11 Javascript
JS获取随机数函数可自定义最小值最大值
2014/05/08 Javascript
jQuery 实现侧边浮动导航菜单效果
2014/12/26 Javascript
jquery中ready()函数执行的时机和window的load事件比较
2015/06/22 Javascript
把Node.js程序加入服务实现随机启动
2015/06/25 Javascript
js ajaxfileupload.js上传报错的解决方法
2016/05/05 Javascript
jQuery根据表单name获取值的方法
2016/05/24 Javascript
老生常谈javascript变量的命名规范和注释
2016/09/29 Javascript
JS实现异步上传压缩图片
2017/04/22 Javascript
深入理解JavaScript创建对象的多种方式以及优缺点
2017/06/01 Javascript
vue.js中v-on:textInput无法执行事件问题的解决过程
2017/07/12 Javascript
vue删除html内容的标签样式实例
2018/09/13 Javascript
微信公众平台 发送模板消息(Java接口开发)
2019/04/17 Javascript
Vue 中文本内容超出规定行数后展开收起的处理的实现方法
2019/04/28 Javascript
js实现图片上传到服务器和回显
2020/01/19 Javascript
浅谈vue项目利用Hbuilder打包成APP流程,以及遇到的坑
2020/09/12 Javascript
Tensorflow实现卷积神经网络用于人脸关键点识别
2018/03/05 Python
在Python中分别打印列表中的每一个元素方法
2018/11/07 Python
Python 取numpy数组的某几行某几列方法
2019/10/24 Python
关于box-sizing的全面理解
2016/07/28 HTML / CSS
html2canvas截图空白问题的解决
2020/03/24 HTML / CSS
巴西24小时在线药房:Droga Raia
2020/05/12 全球购物
前厅部经理岗位职责范文
2014/02/04 职场文书
更夫岗位责任制
2014/02/11 职场文书
岗位职责怎么写
2014/03/14 职场文书
彩妆大赛策划方案
2014/05/13 职场文书
离婚协议书范本
2015/01/26 职场文书
2015年技术工作总结范文
2015/04/20 职场文书
企业战略合作意向书
2015/05/08 职场文书
优秀班主任工作总结2015
2015/05/25 职场文书
特种设备安全管理制度
2015/08/06 职场文书
感恩主题班会教案
2015/08/12 职场文书
PHP对接阿里云虚拟号的实现(号码隐私保护)
2021/04/06 PHP