获取展示 Python 模块中所有使用过的类、方法和函数
在使用新模块时,了解模块中哪些实体被实际使用过有时会很有帮助。我在博文 “Instrumenting Java Code to Find and Handle Unused Classes “中写过类似的内容,但这次我需要在 Python 中使用,而且是方法级粒度的。
简要说明
从 GitHub 下载 trace.py,用它在错误输出中打印调用树和已用方法与类的列表:
import trace trace.setup(r"MODULE_REGEX", print_location_=True)
实现
这可能是一个很难解决的问题,但当我们使用 sys.settrace 为每个方法和函数调用设置一个处理程序,问题就不难解决了。
基本上有六种不同类型的函数(示例代码在 GitHub 上):
def log(message: str):
print(message)
class TestClass:
# static initializer of the class
x = 100
def __init__(self):
# constructor
log("instance initializer")
def instance_method(self):
# instance method, self is bound to an instance
log("instance method")
@staticmethod
def static_method():
log("static method")
@classmethod
def class_method(cls):
log("class method")
def free_function():
log("free function")
这一点很重要,因为在下文中我们必须以不同的方式处理它们。但首先,让我们定义几个助手和配置变量:
indent = 0 module_matcher: str = ".*" print_location: bool = False
我们还想打印方法调用树,因此使用缩进来跟踪当前的缩进级别。module_matcher 是正则表达式,我们用它来决定是否要考虑一个模块、它的类和方法。例如,可以使用 __main__ 来只考虑主模块。print_location 告诉我们是否要打印调用树中每个元素的路径和行位置。
现在来看主辅助类:
def log(message: str):
print(message, file=sys.stderr)
STATIC_INIT = "<static init>"
@dataclass
class ClassInfo:
""" Used methods of a class """
name: str
used_methods: Set[str] = field(default_factory=set)
def print(self, indent_: str):
log(indent_ + self.name)
for method in sorted(self.used_methods):
log(indent_ + " " + method)
def has_only_static_init(self) -> bool:
return (
len(self.used_methods) == 1 and
self.used_methods.pop() == STATIC_INIT)
used_classes: Dict[str, ClassInfo] = {}
free_functions: Set[str] = set()
ClassInfo 保存了一个类的常用方法。我们将使用过的类的 ClassInfo 实例和自由函数存储在全局变量中。
现在,我们将调用处理程序传递给 sys.settrace:
def handler(frame: FrameType, event: str, *args):
""" Trace handler that prints and tracks called functions """
# find module name
module_name: str = mod.__name__ if (
mod := inspect.getmodule(frame.f_code)) else ""
# get name of the code object
func_name = frame.f_code.co_name
# check that the module matches the define regexp
if not re.match(module_matcher, module_name):
return
# keep indent in sync
# this is the only reason why we need
# the return events and use an inner trace handler
global indent
if event == 'return':
indent -= 2
return
if event != "call":
return
# insert the current function/method
name = insert_class_or_function(module_name, func_name, frame)
# print the current location if neccessary
if print_location:
do_print_location(frame)
# print the current function/method
log(" " * indent + name)
# keep the indent in sync
indent += 2
# return this as the inner handler to get
# return events
return handler
def setup(module_matcher_: str = ".*", print_location_: bool = False):
# ...
sys.settrace(handler)
现在,我们 “只 “需要获取代码对象的名称,并将其正确收集到 ClassInfo 实例或自由函数集中。基本情况很简单:当当前frame 包含一个局部变量 self 时,我们可能有一个实例方法;当当前frame 包含一个 cls 变量时,我们有一个类方法。
def insert_class_or_function(module_name: str, func_name: str,
frame: FrameType) -> str:
""" Insert the code object and return the name to print """
if "self" in frame.f_locals or "cls" in frame.f_locals:
return insert_class_or_instance_function(module_name,
func_name, frame)
# ...
def insert_class_or_instance_function(module_name: str,
func_name: str,
frame: FrameType) -> str:
"""
Insert the code object of an instance or class function and
return the name to print
"""
class_name = ""
if "self" in frame.f_locals:
# instance methods
class_name = frame.f_locals["self"].__class__.__name__
elif "cls" in frame.f_locals:
# class method
class_name = frame.f_locals["cls"].__name__
# we prefix the class method name with "<class>"
func_name = "<class>" + func_name
# add the module name to class name
class_name = module_name + "." + class_name
get_class_info(class_name).used_methods.add(func_name)
used_classes[class_name].used_methods.add(func_name)
# return the string to print in the class tree
return class_name + "." + func_name
那么其他三种情况呢?我们使用方法的header line来区分它们:
class StaticFunctionType(Enum):
INIT = 1
""" static init """
STATIC = 2
""" static function """
FREE = 3
""" free function, not related to a class """
def get_static_type(code: CodeType) -> StaticFunctionType:
file_lines = Path(code.co_filename).read_text().split("\n")
line = code.co_firstlineno
header_line = file_lines[line - 1]
if "class " in header_line:
# e.g. "class TestClass"
return StaticFunctionType.INIT
if "@staticmethod" in header_line:
return StaticFunctionType.STATIC
return StaticFunctionType.FREE
当然,这些只是近似值,但对于用于探索的小型实用程序来说,它们已经足够好用了。
如果你还知道其他不使用 Python AST 的方法,请在下面的评论中留言。
使用 get_static_type 函数,我们现在可以完成 insert_class_or_function 函数了:
def insert_class_or_function(module_name: str, func_name: str,
frame: FrameType) -> str:
""" Insert the code object and return the name to print """
if "self" in frame.f_locals or "cls" in frame.f_locals:
return insert_class_or_instance_function(module_name,
func_name, frame)
# get the type of the current code object
t = get_static_type(frame.f_code)
if t == StaticFunctionType.INIT:
# static initializer, the top level class code
# func_name is actually the class name here,
# but classes are technically also callable function
# objects
class_name = module_name + "." + func_name
get_class_info(class_name).used_methods.add(STATIC_INIT)
return class_name + "." + STATIC_INIT
elif t == StaticFunctionType.STATIC:
# @staticmethod
# the qualname is in our example TestClass.static_method,
# so we have to drop the last part of the name to get
# the class name
class_name = module_name + "." + frame.f_code.co_qualname[
:-len(func_name) - 1]
# we prefix static class names with "<static>"
func_name = "<static>" + func_name
get_class_info(class_name).used_methods.add(func_name)
return class_name + "." + func_name
free_functions.add(frame.f_code.co_name)
return module_name + "." + func_name
最后要做的是注册一个teardown处理程序,以便在退出时打印收集到的信息:
def teardown():
""" Teardown the tracer and print the results """
sys.settrace(None)
log("********** Trace Results **********")
print_info()
# trigger teardown on exit
atexit.register(teardown)
使用方法
现在,我们在示例程序的开头加上前缀
import trace trace.setup(r"__main__")
收集 __main__ 模块的所有信息,并直接传递给 Python 解释器。
我们在程序中添加一些代码来调用所有方法/函数:
def all_methods():
log("all methods")
TestClass().instance_method()
TestClass.static_method()
TestClass.class_method()
free_function()
all_methods()
我们的实用程序库在执行时会打印出以下内容:
standard error:
__main__.TestClass.<static init>
__main__.all_methods
__main__.log
__main__.TestClass.__init__
__main__.log
__main__.TestClass.instance_method
__main__.log
__main__.TestClass.<static>static_method
__main__.log
__main__.TestClass.<class>class_method
__main__.log
__main__.free_function
__main__.log
********** Trace Results **********
Used classes:
only static init:
not only static init:
__main__.TestClass
<class>class_method
<static init>
<static>static_method
__init__
instance_method
Free functions:
all_methods
free_function
log
standard output:
all methods
instance initializer
instance method
static method
class method
free function
结论
这个小工具利用 sys.settrace(和一些字符串处理)的强大功能来查找模块使用的类、方法和函数以及调用树。在试图掌握模块的内部结构和自己的应用程序代码中转使用的模块实体时,该工具非常有用。
我在 GitHub 上以 MIT 许可发布了这段代码,所以请随意改进、扩展和修改它。过几周再来看看我为什么要开发这个工具…
本文文字及图片出自 Finding all used Classes, Methods and Functions of a Python Module

