简单谈谈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中的实现
May 04 Python
Python 中pandas.read_excel详细介绍
Jun 23 Python
基于pandas数据样本行列选取的方法
Apr 20 Python
python 对dataframe下面的值进行大规模赋值方法
Jun 09 Python
基于Python中求和函数sum的用法详解
Jun 28 Python
python 实现返回一个列表中出现次数最多的元素方法
Jun 11 Python
django项目登录中使用图片验证码的实现方法
Aug 15 Python
基于pygame实现童年掌机打砖块游戏
Feb 25 Python
python读取excel进行遍历/xlrd模块操作
Jul 12 Python
基于Python正确读取资源文件
Sep 14 Python
http通过StreamingHttpResponse完成连续的数据传输长链接方式
Feb 12 Python
OpenCV项目实践之停车场车位实时检测
Apr 11 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编实现程动态图像的创建代码
2008/09/28 PHP
PHP更新购物车数量(表单部分/PHP处理部分)
2013/05/03 PHP
浅谈php自定义错误日志
2015/02/13 PHP
PHP会话操作之cookie用法分析
2016/09/28 PHP
PHP实现的回溯算法示例
2017/08/15 PHP
thinkPHP3.2.3结合Laypage实现的分页功能示例
2018/05/28 PHP
PHP 模拟登陆功能实例详解
2019/09/10 PHP
基于jQuery捕获超链接事件进行局部刷新代码
2012/05/10 Javascript
原生javaScript做得动态表格(注释写的很清楚)
2013/12/29 Javascript
javaScript中的this示例学习详解及工作原理
2014/01/13 Javascript
纯JS实现动态时间显示代码
2014/02/08 Javascript
JavaScript通过this变量快速找出用户选中radio按钮的方法
2015/03/23 Javascript
js实现仿Windows风格选项卡和按钮效果实例
2015/05/13 Javascript
基于jQuery实现拖拽图标到回收站并删除功能
2015/11/25 Javascript
简单的分页代码js实现
2016/05/17 Javascript
JS选取DOM元素的简单方法
2016/07/08 Javascript
JS组件系列之使用HTML标签的data属性初始化JS组件
2016/09/14 Javascript
Javascript基础回顾之(三) js面向对象
2017/01/31 Javascript
swiper插件自定义切换箭头按钮
2017/12/28 Javascript
详解Angular5 服务端渲染实战
2018/01/04 Javascript
vue-cli3脚手架的配置及使用教程
2018/08/28 Javascript
jquery分页插件pagination使用教程
2018/10/23 jQuery
使用 JavaScript 创建并下载文件(模拟点击)
2019/10/25 Javascript
教你使用Canvas处理图片的方法
2017/11/28 HTML / CSS
AmazeUI在模态框中嵌入表单形成模态输入框
2020/08/20 HTML / CSS
瑞典网上购买现代和复古家具:Reforma
2019/10/21 全球购物
工商学院毕业生自荐信
2013/11/12 职场文书
微笑服务演讲稿
2014/05/13 职场文书
英语分层教学实施方案
2014/06/15 职场文书
2014年党小组工作总结
2014/12/20 职场文书
银行先进个人总结
2015/02/15 职场文书
英语通知范文
2015/04/22 职场文书
利用 SQL Server 过滤索引提高查询语句的性能分析
2021/07/15 SQL Server
SQL Server数据库基本概念、组成、常用对象与约束
2022/03/20 SQL Server
Java十分钟精通进阶适配器模式
2022/04/06 Java/Android
nginx location 带斜杠【 / 】与不带的区别
2022/04/13 Servers