反射
反射机制就是在运行时,动态的确定对象的类型,并可以通过字符串调用对象属性、方法、导入模块,是一种基于字符串的事件驱动。
解释型语言:程序不需要编译,程序在运行时才翻译成机器语言,每执行一次都要翻译一次。因此效率比较低。相对于编译型语言存在的,源代码不是直接翻译成机器语言,而是先翻译成中间代码,再由解释器对中间代码进行解释运行。比如Python/JavaScript / Perl /Shell等都是解释型语言。
python是一门解释型语言,因此对于反射机制支持很好。在python中支持反射机制的函数有getattr()、setattr()、delattr()、exec()、eval()、__import__,这些函数都可以执行字符串。
eval
计算指定表达式的值。它只能执行单个表达式,而不能是复杂的代码逻辑。而且不能是赋值表达式。
单个表达式
a = "12 + 43"
b = eval(a)
print(b)
复杂表达式
a = "print(12 + 43); print(1111)"
b = eval(a)
print(b)
# 输出:
Traceback (most recent call last):
File "xxxx.py", line 10, in
b = eval(a)
File "", line 1
print(12 + 43); print(1111)
^
SyntaxError: invalid syntax
赋值
a = 1
b = eval("a = 21")
print(b)
通常我们使用eval的时候,主要是使用它的返回值,获取表达式计算出的值
exec
执行复杂表达式,返回值永远都是None
b = exec("aa = 21")
print(b) # None,exec返回值为None
print(aa) # 21,exec执行了赋值语句,并定义了aa变量
执行复杂语句
a = '''ret = []
for i in range(10):
ret.append(i)'''
exec(a)
print(ret) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
导入模块
# 导入模块
exec("import config")
print(config.KEYWORD)
# 动态创建类
class Base:
def __init__(self):
print("Base")
a = "Base"
exec(a+"()")
导入模块这个功能就非常屌了,这样我们就可以动态的创建各种模块类。
eval()函数和exec()函数的区别:
eval()函数只能计算单个表达式的值,而exec()函数可以动态运行代码段。
eval()函数可以有返回值,而exec()函数返回值永远为None。
再看一下下面的例子:
class Base:
def __init__(self):
print("Base")
def test(self):
print("test")
return "Base::test"
如果我们想通过字符串来调用a对象的test方法,应该怎么做呢,如果要获取返回值,那么可以使用
b = eval("a.test()")
print(b)
输出:
test
Base::test
如果不需要获取返回值,那么可以使用exec,exec("a.test()")
,输出:test
虽然我们可以使用eval和exec来执行以上代码,但是这种方式有一个缺陷,假如这个属性是不存在的,那么这种调用就会报错。那么做好的方式是什么呢?先判断属性是否存在,如果存在就调用,不存在就不调用,python为我们提供了一套方法:hasattr、getattr、setattr、delattr
hasattr
def hasattr(*args, **kwargs): # real signature unknown
"""
Return whether the object has an attribute with the given name.
This is done by calling getattr(obj, name) and catching AttributeError.
"""
pass
通过源码注释我们知道,它返回对象是否具有指定名称的属性。而且它是通过调用getattr
并捕获AttributeError
异常来判断的。就像上面的属性调用,我们就可以使用hasattr(a, "test")
来判断,通过源码注释我们也可以思考一下,eval这种是不是也可以实现这种方法呢?
def has_attr(obj, name):
try:
eval("obj.%s()" % name)
return True
except AttributeError as e:
return False
a = Base()
if has_attr(a, "test"):
eval("a.test()")
# 输出:
Base
test
test
但是这种方式是有缺陷的,因为test输出了两次,因为我们调用了两次test(),这跟我们想要的效果不一样。如果用hasattr呢,这个函数就不会在判断的时候调用一次了。
getattr()
有了判断属性是否存在的函数,那么就得有获取属性的函数了
def getattr(object, name, default=None): # known special case of getattr
"""
getattr(object, name[, default]) -> value
Get a named attribute from an object; getattr(x, 'y') is equivalent to x.y.
When a default argument is given, it is returned when the attribute doesn't
exist; without it, an exception is raised in that case.
"""
pass
从源码注释我们就能知道获取object对象的名为name的属性,想到与object.name,如果提供了default参数,那么当属性不存在的时候,就会返回默认值。同样是上面的例子:
a = Base()
if hasattr(a, "test"):
func = getattr(a, "test")
func()
# 输出:
Base
test
从例子中我们可以看出,hasattr并没有调用test函数,而且getattr获取到的是函数对象,也没有调用它,通过我们主动执行func()才执行了a.test()函数,这样相比于exec和eval就灵活了许多。
setattr
判断和获取属性有了,那么设置属性也是需要的
def setattr(x, y, v): # real signature unknown; restored from __doc__
"""
Sets the named attribute on the given object to the specified value.
setattr(x, 'y', v) is equivalent to ``x.y = v''
"""
pass
将一个特殊值设置给object对象的name属性,相当于x.y = v
class Base:
def __init__(self):
self.name = "name"
a = Base()
setattr(a, "name", "zhangsan")
print(a.name) # 改变原有属性的值
setattr(a, "age", 32)
print(getattr(a, "age")) # 新增不存在的属性,并设置值
虽然setattr(a, "age", 32)
等于a.age=32,但是我们不要忘了,这是通过一个字符串来增加的属性。
判断、获取、增加都有了,当然还有删除delattr
,这个我们就不详述了,接下来我们要看一个比较重要的方法。
import
在学习exec的时候,我们有一个例子,导入配置文件exec("import config")
,针对这种方式python也为我们提供了更好的方法。
def __import__(name, globals=None, locals=None, fromlist=(), level=0): # real signature unknown; restored from __doc__
"""
__import__(name, globals=None, locals=None, fromlist=(), level=0) -> module
Import a module. Because this function is meant for use by the Python
interpreter and not for general use, it is better to use
importlib.import_module() to programmatically import a module.
The globals argument is only used to determine the context;
they are not modified. The locals argument is unused. The fromlist
should be a list of names to emulate ``from name import ...'', or an
empty list to emulate ``import name''.
When importing a module from a package, note that __import__('A.B', ...)
returns package A when fromlist is empty, but its submodule B when
fromlist is not empty. The level argument is used to determine whether to
perform absolute or relative imports: 0 is absolute, while a positive number
is the number of parent directories to search relative to the current module.
"""
pass
在这里我们最需要关注的是formlist参数,先看一个简单的例子:
a = __import__("config")
print(a.KEYWORD)
config是一个py脚本-config.py,内部有一个变量KEYWORD,我们要通过其他py模块来导入这个文件,使用__import__我们就可以把它导入为一个对象,然后使用对象的方式去调用,而不是一直用exec字符串的形式去调用。上面我们说了formlist这个参数需要关注,为什么呢?我们新增了一个模块:comm。模块内有一个脚本function.py
# function.py
def comm_function():
print("test_module")
我们现在想通过动态引入的方式调用comm_function函数,那么按照上面的方式来
a = __import__("comm.function")
a.comm_function()
结果输出:
Traceback (most recent call last):
File "xxx.py", line 10, in
print(a.comm_function())
AttributeError: module 'comm' has no attribute 'comm_function'
意思是comm模块没有comm_function这个属性,为什么是comm模块而不是function呢?我们可以打印一下模块的引入名称print(a.__name__)
,打印的结果是comm,就是说我们通过上面的方式只是引入comm,而不是function。其实通过源码注释我们就知道了,__import__(A.B)
,如果fromlist为空,返回的是A包,如果不为空,则返回其子包B。修改一下我们的代码:
a = __import__("comm.function", fromlist=True)
print(a.__name__)
a.comm_function()
# 输出:
comm.function
test_module
引入的模块和执行函数都正确了,符合了我们的预期要求。
总结
通过以上的函数学习,其中有常用的,也有不常用的,但是这些函数在我们进行框架设计时是必不可少的,尤其是__import__,接下来我们还会继续看框架设计中最重要的一个概念--元编程。学完了这些概念就可以设计框架了。开玩笑的,哪有那么简单。阅读源码是一种增长知识的最快捷方式,但是前提是基础一定要打好。否则看源码是一头雾水。我们整理完这些概念后,在找几个源码库看看,学习一下里面的设计理念。
如果你觉得我的文章还可以,可以关注我的微信公众号,查看更多实战文章:Python爬虫实战之路
也可以扫描下面二维码,添加我的微信公众号