Python反射机制详解:四个核心函数与实战应用
本文深入解析Python反射机制,详细讲解getattr、setattr、hasattr和delattr四个核心函数的用法。通过对象属性操作、类方法反射、模块级反射以及加密算法和Pytest测试框架的实际案例,展示如何利用字符串形式动态操作对象,从而提升代码的扩展性与可维护性。
在Python中,访问对象的属性或调用方法通常使用点操作符“.”。例如:
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
反射提供了一种通过字符串名称来操作对象属性和方法的替代方式,示例如下:
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值,则返回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属性:
print(hasattr(p, 'say_bye')) # 输出False
print(hasattr(p, 'say_hi')) # 输出True
为p对象动态添加say_bye方法和age属性:
setattr(p, 'say_bye', say_bye)
setattr(p, 'age', 18)
此时可通过反射访问这两个新增属性:
bye = getattr(p, 'say_bye')
bye()
print(getattr(p, 'age'))
或者继续使用点操作符访问:
p.say_bye()
print(p.age)
删除age属性:
delattr(p, 'age')
print(hasattr(p, 'age')) # 输出False
2. 类的反射操作
除了对实例对象进行反射操作外,还可以对类本身、当前模块、其他模块乃至其他包进行反射操作。
类的反射操作,指的是对类属性、类方法或静态方法执行反射操作。
获取类属性示例:
t = getattr(Person, 'type')
print(t) # 输出mammal
f = getattr(Person, 'feed')
f() # 输出Three times per day.
s = getattr(Person, 'sleep')
s() # 8 hours!
判断类属性是否存在示例:
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
此外,还可以对类动态添加和删除属性和方法。
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,代码如下:
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模块,该模块提供了堆队列算法(即优先队列算法)的实现。以下代码展示了堆排序函数的实现:
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模块的内容如下:
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函数即可,用法如下:
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的代码可重构为:
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测试框架中的应用
再看一个基于Pytest测试框架的测试脚本中应用反射的例子,例如conftest文件内容如下:
# 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文件。
server = getattr(request.module, "smtpserver", "smtp.gmail.com")
其含义是从测试脚本文件中查找smtpserver属性,若找不到,则默认使用smtp.gmail.com作为smtpserver的值。如果测试脚本文件中定义了该属性,则使用脚本中的值。例如以下测试脚本中,smtpserver将使用mail.python.org这个值:
# 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. 总结
反射在许多开源框架中普遍采用,是提高代码可维护性和扩展性的有力工具。若工作中涉及框架开发,反射技术必将助您一臂之力,希望大家多多支持本站!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













