Python元编程与模块管理全解析
1. 元编程基础
在大型面向对象程序里,把类的定义置于元类的掌控之下往往很有帮助。元类能够对类的定义进行监控,还能给程序员预警那些可能被忽视的问题,比如使用稍有不兼容的方法签名。虽说可以用分析工具或者IDE来捕捉这类错误,但要是你在创建供他人使用的框架或库,通常无法把控用户的开发严谨程度。所以,对于某些类型的应用程序,在元类里添加额外的检查是有意义的,前提是这种检查能给用户带来便利。
在元类中选择重写
__new__()
还是
__init__()
,取决于你对生成类的操作方式。
__new__()
在类创建之前被调用,若元类想改变类的定义(比如修改类字典的内容),通常会使用它。而
__init__()
在类创建之后被调用,如果你想编写处理完整类对象的代码,它就很有用。例如,在某些代码中使用
super()
来查找继承层次结构中更高层次的定义,这就要求类对象已经创建,并且底层的方法解析顺序已经确定。
下面是一个简单的流程图,展示了元类中
__new__()
和
__init__()
的调用顺序:
graph LR
A[类定义开始] --> B[调用元类__new__()]
B --> C[创建类对象]
C --> D[调用元类__init__()]
D --> E[类定义完成]
2. 程序式定义类
当你需要创建新的类对象时,除了把类的源代码以字符串形式传给
exec()
执行,还可以用
types.new_class()
函数。使用时,要给它提供类名、父类元组、命名参数和一个回调函数,这个回调函数会用元素填充类的字典。以下是示例代码:
# stock.py
# 示例:手动从部分创建类
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
def cost(self):
return self.shares * self.price
cls_dict = {
'__init__': __init__,
'cost': cost,
}
import types
Stock = types.new_class('Stock', (), {}, lambda ns: ns.update(cls_dict))
Stock.__module__ = __name__
使用
types.new_class()
创建类后,要注意设置
__module__
属性,因为它包含类定义所在的模块名,很多库和方法会用到这个属性。如果要创建使用元类的类,可以在
types.new_class()
的第三个参数中指定。例如:
import abc
Stock = types.new_class('Stock', (), {'metaclass': abc.ABCMeta}, lambda ns: ns.update(cls_dict))
Stock.__module__ = __name__
3. 类成员的初始化
在类定义时初始化部分内容,而不是在创建实例时初始化,这是元类的经典应用场景。元类会在类定义的时刻触发,让你可以执行额外的步骤。以下是一个创建类似
collections.namedtuple
类的示例:
import operator
class StructTupleMeta(type):
def __init__(cls, *args, **kwargs):
super().__init__(*args, **kwargs)
for n, name in enumerate(cls._fields):
setattr(cls, name, property(operator.itemgetter(n)))
class StructTuple(tuple, metaclass=StructTupleMeta):
_fields = []
def __new__(cls, *args):
if len(args) != len(cls._fields):
raise ValueError('{} arguments required'.format(len(cls._fields)))
return super().__new__(cls, args)
class Stock(StructTuple):
_fields = ['name', 'shares', 'price']
class Point(StructTuple):
_fields = ['x', 'y']
在这个例子中,
StructTupleMeta
类会将
_fields
中的属性名转换为属性方法,实现对元组特定槽位的访问。
StructTuple
类作为基类,其
__new__()
方法负责创建新实例,由于元组不可变,所以在实例创建前进行参数检查。
4. 多重调度的实现
利用函数参数注解实现多重调度(方法重载)是一个有趣的想法。可以通过元类和描述符的组合来实现。以下是实现代码:
# multiple.py
import inspect
import types
class MultiMethod:
def __init__(self, name):
self._methods = {}
self.__name__ = name
def register(self, meth):
sig = inspect.signature(meth)
types = []
for name, parm in sig.parameters.items():
if name == 'self':
continue
if parm.annotation is inspect.Parameter.empty:
raise TypeError('Argument {} must be annotated with a type'.format(name))
if not isinstance(parm.annotation, type):
raise TypeError('Argument {} annotation must be a type'.format(name))
if parm.default is not inspect.Parameter.empty:
self._methods[tuple(types)] = meth
types.append(parm.annotation)
self._methods[tuple(types)] = meth
def __call__(self, *args):
types = tuple(type(arg) for arg in args[1:])
meth = self._methods.get(types, None)
if meth:
return meth(*args)
else:
raise TypeError('No matching method for types {}'.format(types))
def __get__(self, instance, cls):
if instance is not None:
return types.MethodType(self, instance)
else:
return self
class MultiDict(dict):
def __setitem__(self, key, value):
if key in self:
current_value = self[key]
if isinstance(current_value, MultiMethod):
current_value.register(value)
else:
mvalue = MultiMethod(key)
mvalue.register(current_value)
mvalue.register(value)
super().__setitem__(key, mvalue)
else:
super().__setitem__(key, value)
class MultipleMeta(type):
def __new__(cls, clsname, bases, clsdict):
return type.__new__(cls, clsname, bases, dict(clsdict))
@classmethod
def __prepare__(cls, clsname, bases):
return MultiDict()
class Spam(metaclass=MultipleMeta):
def bar(self, x:int, y:int):
print('Bar 1:', x, y)
def bar(self, s:str, n:int = 0):
print('Bar 2:', s, n)
在这个实现中,
MultipleMeta
元类使用
__prepare__()
方法提供一个自定义的类字典
MultiDict
。
MultiMethod
类收集方法,通过函数的类型签名构建映射。不过,这种实现存在一些限制,比如不支持命名参数,对继承的支持也有限。
5. 避免重复的属性方法
在编写类时,经常会重复定义执行常见任务(如类型检查)的属性方法,为了简化代码、避免重复,可以创建一个函数来定义属性并返回它。示例如下:
def typed_property(name, expected_type):
storage_name = '_' + name
@property
def prop(self):
return getattr(self, storage_name)
@prop.setter
def prop(self, value):
if not isinstance(value, expected_type):
raise TypeError('{} must be a {}'.format(name, expected_type))
setattr(self, storage_name, value)
return prop
class Person:
name = typed_property('name', str)
age = typed_property('age', int)
def __init__(self, name, age):
self.name = name
self.age = age
还可以用
functools.partial()
进一步简化代码:
from functools import partial
String = partial(typed_property, expected_type=str)
Integer = partial(typed_property, expected_type=int)
class Person:
name = String('name')
age = Integer('age')
def __init__(self, name, age):
self.name = name
self.age = age
6. 轻松定义上下文管理器
使用
contextlib
模块中的
@contextmanager
装饰器可以轻松实现新的上下文管理器。以下是两个示例:
import time
from contextlib import contextmanager
@contextmanager
def timethis(label):
start = time.time()
try:
yield
finally:
end = time.time()
print('{}: {}'.format(label, end - start))
# 示例
with timethis('counting'):
n = 10000000
while n > 0:
n -= 1
@contextmanager
def list_transaction(orig_list):
working = list(orig_list)
yield working
orig_list[:] = working
在
timethis
函数中,
yield
之前的代码相当于上下文管理器的
__enter__()
方法,之后的代码相当于
__exit__()
方法。
list_transaction
函数实现了对列表的事务操作,只有在代码块执行过程中没有引发异常,才会将更改应用到原始列表。
7. 执行具有局部副作用的代码
使用
exec()
在调用者的作用域中执行代码时,可能看不到执行结果。可以用
locals()
函数获取局部变量字典,在
exec()
执行后从字典中提取更改的值。示例如下:
def test():
a = 13
loc = locals()
exec('b = a + 1')
b = loc['b']
print(b)
test()
不过,
exec()
的正确使用比较复杂,很多情况下可以用闭包、装饰器、元类等更优雅的方法替代。
8. 解析和分析Python源代码
可以用
ast
模块将Python源代码编译成抽象语法树(AST)进行分析。以下是示例代码:
import ast
ex = ast.parse('2 + 3*4 + x', mode='eval')
print(ast.dump(ex))
top = ast.parse('for i in range(10): print(i)', mode='exec')
print(ast.dump(top))
class CodeAnalyzer(ast.NodeVisitor):
def __init__(self):
self.loaded = set()
self.stored = set()
self.deleted = set()
def visit_Name(self, node):
if isinstance(node.ctx, ast.Load):
self.loaded.add(node.id)
elif isinstance(node.ctx, ast.Store):
self.stored.add(node.id)
elif isinstance(node.ctx, ast.Del):
self.deleted.add(node.id)
# 示例使用
code = '''
for i in range(10):
print(i)
del i
'''
top = ast.parse(code, mode='exec')
c = CodeAnalyzer()
c.visit(top)
print('Loaded:', c.loaded)
print('Stored:', c.stored)
print('Deleted:', c.deleted)
通过分析AST,可以编写各种工具进行代码分析、优化和验证。还可以修改AST来表示新的代码,例如将全局名称“降级”为函数内部的局部变量。
9. 反汇编Python字节码
使用
dis
模块可以对Python函数进行反汇编,查看其底层的字节码执行情况。示例如下:
def countdown(n):
while n > 0:
print('T-minus', n)
n -= 1
print('Blastoff!')
import dis
dis.dis(countdown)
虽然
dis
模块没有方便处理字节码的函数,但可以编写一个生成器将原始字节码转换为操作码和参数:
import opcode
def generate_opcodes(codebytes):
extended_arg = 0
i = 0
n = len(codebytes)
while i < n:
op = codebytes[i]
i += 1
if op >= opcode.HAVE_ARGUMENT:
oparg = codebytes[i] + codebytes[i+1]*256 + extended_arg
extended_arg = 0
i += 2
if op == opcode.EXTENDED_ARG:
extended_arg = oparg * 65536
continue
else:
oparg = None
yield (op, oparg)
c = countdown.__code__.co_code
for op, oparg in generate_opcodes(c):
print(op, opcode.opname[op], oparg)
需要注意的是,修改函数的原始字节码是非常危险的操作,可能会导致解释器崩溃,但在某些高级优化和元编程工具开发中可能会用到。
Python元编程与模块管理全解析
10. 模块与包的组织
在大型项目中,模块和包是核心组成部分。下面将介绍一些常见的模块和包的编程技巧,包括创建层次化的包、控制导入、使用相对名称导入子模块等。
10.1 创建层次化的包模块
要将代码组织成一个由层次化模块集合组成的包很简单,只需将代码组织到目录结构中,并确保每个目录都包含
__init__.py
文件。例如:
graphics/
__init__.py
primitive/
__init__.py
line.py
fill.py
text.py
formats/
__init__.py
png.py
jpg.py
之后就可以进行各种导入操作,如下表所示:
| 导入语句 | 说明 |
| ---- | ---- |
|
import graphics.primitive.line
| 导入
graphics
包下
primitive
子包中的
line
模块 |
|
from graphics.primitive import line
| 从
graphics.primitive
包中导入
line
模块 |
|
import graphics.formats.jpg as jpg
| 导入
graphics.formats
包中的
jpg
模块并起别名
jpg
|
__init__.py
文件可以包含可选的初始化代码,在访问包的不同层级时会执行。有时可以用它自动加载子模块,或者将不同文件的定义合并到一个逻辑命名空间中。
10.2 控制导入
当用户使用
from module import *
语句时,要完全控制从模块或包中导出的名称,可以在模块中定义
__all__
变量,明确列出要导出的名称。例如:
# somemodule.py
def spam():
pass
def grok():
pass
blah = 42
# 仅导出 'spam' 和 'grok'
__all__ = ['spam', 'grok']
如果不定义
__all__
,默认会导出所有不以下划线开头的名称;如果
__all__
为空列表,则不导出任何名称;如果
__all__
包含未定义的名称,会引发
AttributeError
异常。
10.3 使用相对名称导入包的子模块
在包内的模块中导入同一包的其他子模块时,可以使用相对导入。假设有如下包结构:
mypackage/
__init__.py
A/
__init__.py
spam.py
grok.py
B/
__init__.py
bar.py
在
mypackage.A.spam
模块中导入同一目录的
grok
模块,可使用:
# mypackage/A/spam.py
from . import grok
导入另一个目录的
B.bar
模块,可使用:
# mypackage/A/spam.py
from ..B import bar
相对导入的语法如下:
-
.
表示在当前目录查找。
-
..
表示在上级目录查找。
相对导入只能用于位于合适包内的模块,不能用于顶级脚本中的简单模块,也不能在将包的部分作为脚本直接执行时使用。若要使用相对导入,可通过
python -m
选项运行脚本。
10.4 将模块拆分为多个文件
若想将一个模块拆分为多个文件,同时又不破坏现有代码,可以将模块转换为包。例如,将下面的简单模块拆分:
# mymodule.py
class A:
def spam(self):
print('A.spam')
class B(A):
def bar(self):
print('B.bar')
将
mymodule.py
替换为一个名为
mymodule
的目录,并在其中创建以下文件:
mymodule/
__init__.py
a.py
b.py
a.py
文件内容:
# a.py
class A:
def spam(self):
print('A.spam')
b.py
文件内容:
# b.py
from .a import A
class B(A):
def bar(self):
print('B.bar')
__init__.py
文件内容:
# __init__.py
from .a import A
from .b import B
这样,
mymodule
包就可以作为一个逻辑模块使用:
import mymodule
a = mymodule.A()
a.spam()
b = mymodule.B()
b.bar()
为了实现“懒加载”,可以修改
__init__.py
文件:
# __init__.py
def A():
from .a import A
return A()
def B():
from .b import B
return B()
不过,懒加载可能会导致继承和类型检查出现问题。
10.5 创建共享命名空间的代码目录
当有一个大型代码库,其不同部分由不同人员维护和分发时,可以将这些部分组织成包,并将它们合并到一个公共的包前缀下。只需将代码组织成普通的Python包结构,但在作为公共命名空间的目录中省略
__init__.py
文件。例如:
foo-package/
spam/
blah.py
bar-package/
spam/
grok.py
将这两个包添加到Python的模块搜索路径后,就可以统一导入:
import sys
sys.path.extend(['foo-package', 'bar-package'])
import spam.blah
import spam.grok
这种机制称为“命名空间包”,其
__path__
属性会保存所有包含相同包名的目录列表。可以通过检查
__file__
属性是否存在来判断一个包是否为命名空间包。
10.6 重新加载模块
若对已加载模块的源代码进行了修改,想重新加载该模块,可以使用
imp.reload()
函数。例如:
import spam
import imp
imp.reload(spam)
不过,重新加载模块在生产环境中使用不安全,因为它可能不会按预期工作。
reload()
会清除模块字典的内容,重新执行模块的源代码,但不会更新使用
from module import name
语句导入的定义。
10.7 创建可作为主脚本运行的目录或zip存档
如果程序从简单脚本发展成包含多个文件的应用程序,可以将其放在一个目录中,并添加
__main__.py
文件。例如:
myapplication/
spam.py
bar.py
grok.py
__main__.py
在顶级目录运行Python解释器:
bash % python3 myapplication
解释器会将
__main__.py
作为主程序执行。同样,也可以将代码打包成zip存档并运行:
bash % ls
spam.py
bar.py
grok.py
__main__.py
bash % zip -r myapp.zip *.py
bash % python3 myapp.zip
10.8 读取包内的数据文件
若包中包含需要读取的数据文件,为确保最大的可移植性,可以使用以下方法。以下是一个简单的流程图展示读取包内数据文件的一般流程:
graph LR
A[确定数据文件路径] --> B[打开数据文件]
B --> C[读取数据]
C --> D[处理数据]
D --> E[关闭数据文件]
在实际代码中,可以使用
importlib.resources
模块(Python 3.7及以上)来读取包内的数据文件。例如:
import importlib.resources
package = 'your_package_name'
resource = 'data_file.txt'
with importlib.resources.open_text(package, resource) as f:
data = f.read()
print(data)
综上所述,Python的元编程和模块管理提供了丰富的功能和灵活的机制,能够帮助开发者更好地组织和管理代码,提高代码的可维护性和可扩展性。在实际应用中,需要根据具体需求选择合适的方法和技巧。
超级会员免费看

被折叠的 条评论
为什么被折叠?



