通常,我们操作对象的属性或者方法时,是通过点“.”操作符进行的。例如下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
class Person: type = "mammal" def __init__( self , name): self .name = name def say_hi( self ): print ( 'Hello, my name is' , self .name) @staticmethod def feed(): print ( "Three times per day." ) @classmethod def sleep( cls ): print ( "8 hours!" ) p = Person( 'Chunming' ) p.say_hi() print (p.name) |
上面代码的输出是
Hello, my name is Nikhil
Nikhil
反射是另外一种操作对象属性和方法的手段,例如:
1
2
3
|
func = getattr (p, 'say_hi' ) func() print ( getattr (p, "name" )) |
上面这段代码的输出是:
Hello, my name is Nikhil
Nikhil
可见与通过点操作符的结果一致。
1. 反射的四个函数
getattr是获取对象属性或方法的函数,Python的官方文档是这样描述其用法的:
getattr(object, name, value)
返回对象命名属性的值。name必须是字符串。如果该字符串是对象的属性之一,则返回该属性的值。例如, getattr(x, ‘foobar')等同于 x.foobar。如果指定的属性不存在,且提供了 default值,则返回它,否则触发 AttributeError。
根据文档理解上述代码,getattr(p, ‘say_hi') 获取了p对象的say_hi属性值并赋值给func变量,因为say_hi属性在Person类中是一个方法,要想调用这个方法, 需要执行func(),getattr(p, “name”) 则是获取p对象的name属性。
除了获取对象属性和方法的getattr函数,python还内置了判断、设置、删除对象属性和方法的函数,来看看Python官方文档对这三个函数的说明:
setattr(object, name, value)
此函数与 getattr() 两相对应。其参数为一个对象、一个字符串和一个任意值。字符串指定一个现有属性或者新增属性。函数会将值赋给该属性,只要对象允许这种操作。例如,setattr(x, ‘foobar', 123) 等价于 x.foobar = 123。
hasattr(object, name)
该实参是一个对象和一个字符串。如果字符串是对象的属性之一的名称,则返回 True,否则返回 False。(此功能是通过调用 getattr(object, name) 看是否有 AttributeError 异常来实现的。)
delattr(object, name)
setattr() 相关的函数。实参是一个对象和一个字符串。该字符串必须是对象的某个属性。如果对象允许,该函数将删除指定的属性。例如 delattr(x, ‘foobar') 等价于 del x.foobar 。
Python中通过getattr、setattr、hasattr和delattr四个函数操作属性的机制就是反射。是通过字符串的形式操作对象属性和方法的机制。下面对p属性应用setattr、hasattr和delattr这三个函数看看效果:
判断p对象是否有say_bye属性和say_hi属性:
1
2
|
print ( hasattr (p, 'say_bye' )) # 输出False print ( hasattr (p, 'say_hi' )) # 输出True |
给p对象增加say_bye的方法和age属性:
1
2
|
setattr (p, 'say_bye' , say_bye) setattr (p, 'age' , 18 ) |
现在可以访问这两个属性了,通过反射访问:
1
2
3
|
bye = getattr (p, 'say_bye' ) bye() print ( getattr (p, 'age' )) |
或者通过点操作符访问:
1
2
|
p.say_bye() print (p.age) |
删除age属性:
1
2
|
delattr (p, 'age' ) print ( hasattr (p, 'age' )) # 输出False |
2. 类的反射操作
除了对象的反射操作,还有类的反射操作,当前模块的反射操作,还有其他模块的反射操作,其他包的反射操作。
类的反射操作,指的是对类属性、类方法或者静态方法执行反射操作。
获取类属性:
1
2
3
4
5
6
|
t = getattr (Person, 'type' ) print (t) # 输出mammal f = getattr (Person, 'feed' ) f() # 输出Three times per day. s = getattr (Person, 'sleep' ) s() # 8 hours! |
判断类属性是否存在:
1
2
3
4
5
|
print ( hasattr (Person, 'type' )) # 输出True print ( hasattr (Person, 'name' )) # 输出False print ( hasattr (Person, 'say_hi' )) # 输出True print ( hasattr (Person, 'sleep' )) # 输出True print ( hasattr (Person, 'feed' )) # 输出True |
此外,还可以对类添加和删除属性和方法。
1
2
3
4
|
print ( delattr (Person, 'feed' )) print ( hasattr (Person, 'feed' )) setattr (Person, 'feed' , lambda x: print ( "Three times per day." )) print ( hasattr (Person, 'feed' )) |
3. 当前模块的反射操作
当前模块也就是当前代码所在的py文件,反射也可以对当前模块中的变量和函数进行操作。例如某个模块包含一个al函数,用来判断迭代对象中每个元素是否都是True,内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
import sys def al(iterable): for element in iterable: if not element: return False return True this_module = sys.modules[__name__] if hasattr (this_module, 'al' ): all_true = getattr (this_module, 'al' ) result = all_true([ 1 , 2 , 3 , 4 , True , 0 ]) print (result) |
通过sys.modules[name]方法获取当前模块的名称。getattr第一个参数是模块名称,第二个参数是想要从模块中获取的属性。
4. 其他模块反射操作
对import进来的其他模块中的函数、属性、变量进行反射操作。例如,我们导入Python的heapq模块,这块模块提供了堆队列算法的实现,也称为优先队列算法。下面的代码是一个实现堆排序的函数。
1
2
3
4
5
6
7
8
9
10
|
import heapq h = [( 5 , 'write code' ), ( 7 , 'release product' ), ( 1 , 'write spec' ), ( 3 , 'create tests' )] if hasattr (heapq, 'heapify' ): heapi = getattr (heapq, 'heapify' ) # 获取heapify属性 heapi(h) # 建堆 if hasattr (heapq, 'heappop' ): heapp = getattr (heapq, 'heappop' ) # 获取heappop属性 print ([heapp(h) for _ in range ( len (h))]) # 弹出并从返回堆中最小的项 |
这里,我们并没有通过heapq.heapify和heapq.heappop方式调用heapq模块中的函数。而是通过反射达到的同样的效果。
5. 反射应用场景之一
了解了反射中四个函数的基本用法。那么反射到底有什么用呢?它的应用场景是什么呢?答案是,当不确定所需要的属性和函数是否存在时,可以使用反射。另外一个重要作用是,可以提高代码的扩展性和可维护性。
假如我们把所有的加密算法都放到一个叫做encryption的模块中维护 ,并且允许使用这个模块的用户添加更多的加密算法到这个模块中。encryption的模块内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
|
import hashlib import os import sys def md5(content = None ): """生成字符串的SHA256值""" if content is None : return '' md5_gen = hashlib.md5() md5_gen.update(content.encode( 'utf-8' )) md5code = md5_gen.hexdigest() return md5code def sha256(content = None ): """生成字符串的SHA256值""" if content is None : return '' sha256_gen = hashlib.sha256() sha256_gen.update(content.encode( 'utf-8' )) sha256code = sha256_gen.hexdigest() return sha256code def sha256_file(filename): """生成文件的SHA256值""" if not os.path.isfile(filename): return "" sha256gen = hashlib.sha256() size = os.path.getsize(filename) # 获取文件大小,单位是Byte with open (filename, 'rb' ) as fd: # 以二进制方式读取文件 while size > = 1024 * 1024 : # 当文件大于1MB时分块读取文件内容 sha256gen.update(fd.read( 1024 * 1024 )) size - = 1024 * 1024 sha256gen.update(fd.read()) sha256code = sha256gen.hexdigest() return sha256code def md5_file(filename): """生成文件的MD5值""" if not os.path.isfile(filename): return "" md5gen = hashlib.md5() size = os.path.getsize(filename) # 获取文件大小,单位是Byte with open (filename, 'rb' ) as fd: while size > = 1024 * 1024 : # 当文件大于1MB时分块读取文件内容 md5gen.update(fd.read( 1024 * 1024 )) size - = 1024 * 1024 md5gen.update(fd.read()) md5code = md5gen.hexdigest() return md5code def encrypt_something(something, algorithm): """ 通用加密算法 :param something: 待加密的内容,字符串或者文件 :param algorithm: 加密算法 :return: 加密后的内容 """ result = "" if algorithm = = "md5" : result = md5(something) elif algorithm = = "sh256" : result = sha256(something) elif algorithm = = "sh256_file" : result = sha256_file(something) elif algorithm = = "md5_file" : result = md5_file(something) return result |
其中,encrypt_something函数提供了通用加密算法,需要调用者传入待加密的内容和加密算法,这样当调用者使用encryption.py模块时,只需导入encrypt_something函数即可。就像这样:
1
2
3
|
import encryption print (encryption.encrypt_something( "learn_python_by_coding" , "sh256" )) print (encryption.encrypt_something( "learn_python_by_coding" , "md5" )) |
通过分析encrypt_something函数发现,当我们在encryption.py模块添加更多的加密算法后,就要修改encrypt_something函数,在其中增加新的if分支,随着加密算法的增加,encrypt_something函数的分支会越来越多。
学了反射之后,encrypt_something代码部分就可以这样写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
def encrypt_something(something, algorithm): """ 通用加密算法 :param something: 待加密的内容,字符串或者文件 :param algorithm: 加密算法 :return: 加密后的内容 """ this_module = sys.modules[__name__] if hasattr (this_module, algorithm): algorithm = getattr (this_module, algorithm) result = algorithm(something) else : raise ValueError( "Not support {} algorithm" . format (algorithm)) return result |
相比前面的采用if分支语句方式,反射更加简洁明了,可维护性更强,要想增加新的加密方法,只需要在encryption.py模块添加更多的加密算法即可,encrypt_something代码不需要任何变更。
6. 反射应用场景之二
再看一个基于Pytest测试框架的测试脚本中应用反射的例子,比如conftest文件内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
# content of conftest.py import pytest import smtplib @pytest .fixture(scope = "module" ) def smtp_connection(request): server = getattr (request.module, "smtpserver" , "smtp.gmail.com" ) smtp_connection = smtplib.SMTP(server, 587 , timeout = 5 ) yield smtp_connection print ( "finalizing {} ({})" . format (smtp_connection, server)) smtp_connection.close() |
request.module 是所有测试脚本,就是那些以_test结尾或者test_开头的py文件。
1
|
server = getattr(request.module, "smtpserver", "smtp.gmail.com") |
含义就是从测试脚本文件中找smtpserver属性,如果找不到,默认使用smtp.gmail.com作为smtpserver的值。如果测试脚本文件有这个属性,则使用测试脚本中的值,例如下面这个测试脚本,smtpserver则会使用mail.python.org这个值:
1
2
3
4
5
6
7
|
# content of test_anothersmtp.py smtpserver = "mail.python.org" # will be read by smtp fixture def test_showhelo(smtp_connection): assert 0, smtp_connection.helo() |
7. 总结
在很多开源框架中普遍采用,是提高可维护性和扩展性的利器。如果工作中也涉及到框架开发,反射一定会助一臂之力,,希望大家以后多多支持服务器之家!
原文链接:https://blog.csdn.net/AI_Green/article/details/119893960