加载 py 文件并在不知道存在多少文件的情况下从目录的 for 循环中对 class 执行某些操作?

Load py files and do something with the class in a for loop from directory without knowing how many files exist?

我正在尝试使用一个目录来保存我不断增长的框架列表,当我的 MAIN.py 文件被执行时,我希望代码将每个框架加载到 Tkinter 笔记本小部件中。

我可以通过从我的 MAIN 文件夹的子目录中导入每个文件来手动执行此操作,但是我正在尝试以实用的方式加载它们,而不必知道每个文件名,也不必自己导入它们。

我取得了一些进展,但我在实际加载文件中的 class 时卡住了。我可以获得所有文件的列表,我相信我正在导入它们,我只是无法加载它们,因为它一直告诉我模块不存在。

我一定是误解了什么,因为我不知道如何从导入的文件中正确调用 class。

错误:

C:\Users\USER\PycharmProjects\TEST\venv\Scripts\python.exe C:/Users/USER/PycharmProjects/TEST/MAIN/MAIN.py
# This below list is a result of my print statement to see if I got all the file names.
['TaskFrame', 'TestFrame1', 'TestFrame2', 'TestFrame3']
Traceback (most recent call last):
  File "C:/Users/MCDOMI3/PycharmProjects/TEST/MAIN/MAIN.py", line 46, in <module>
    ScriptManager().mainloop()
  File "C:/Users/MCDOMI3/PycharmProjects/TEST/MAIN/MAIN.py", line 39, in __init__
    self.add_frame_to_book(foo.tab_title)
AttributeError: module 'ScriptFrames.TaskFrame' has no attribute 'tab_title'

主要代码:

import tkinter as tk
import tkinter.ttk as ttk
from os.path import dirname, basename, isfile, join
import glob
import importlib.util

modules = glob.glob(join(dirname('.\ScriptFrames\'), "*.py"))
__all__ = [basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')]
print(__all__)


class ScriptManager(tk.Tk):

    def __init__(self):
        super().__init__()
        self.book = ttk.Notebook(self)
        self.book.grid(row=0, column=0, sticky='nsew')
        for fn in __all__:
            spec = importlib.util.spec_from_file_location('ScriptFrames.{}'.format(fn),
                                                          '.\ScriptFrames\{}.py'.format(fn))
            foo = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(foo)
            # Here is my problem I do not know if I am attempting to get the class 
            # attribute correctly here. I am sure that I am calling this wrong.
            self.add_frame_to_book(foo.tab_title)

    def add_frame_to_book(self, fname):
        self.book.add(fname, text='test')


if __name__ == '__main__':
    ScriptManager().mainloop()

每个测试文件都是一个简单的 tkinter 框架。

测试帧代码:

import tkinter as tk
import tkinter.ttk as ttk


class TabFrame(ttk.Frame):

    def __init__(self):
        super().__init__()
        self.tab_title = 'Test Frame 1'
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)
        tk.Label(self, text='test').grid(row=0, column=0, sticky='nsew')

如果对我有帮助,这是我的文件结构:

我就是这样做的。

def load(self, file=None):
    file_type = path.splitext(file)[1].lstrip('.').lower()

    if file_type == 'py' and path.exists(file):
        spec = spec_from_file_location("module.name", file)
        module = module_from_spec(spec)
        spec.loader.exec_module(module)
        self.nodes = module.config

您正在尝试访问模块的 tab_title,而不是从 (Frame) class里面。

加载模块后,使用 [Python 3.Docs]: Built-in Functions:

获取其属性
  • 获取模块属性名称(使用dir
    • 对于每个名字,获取对应的属性(使用getattr
    • 测试属性是否为ttk.Frame(因为模块还有其他属性)
      • 如果是帧class,实例化它以便访问tab_title,否则就丢弃它

示例

# ...
# The rest of your code

spec.loader.exec_module(foo)

# At this point, 'foo' module is loaded. Get its attributes
module_attrs = [getattr(foo, attr_name, None) for attr_name in dir(foo)]

# Filter out the attributes only keeping the ones that we care about
frame_classes = [attr for attr in module_attrs if isinstance(attr, (type,)) and issubclass(attr, (ttk.Frame,))]

# Then handle the module classes (it will probably be only one)
for frame_class in frame_classes:
    if frame_class.__name__.startswith("Tab"):  # Or any other filtering
    # It would have been much more efficient to filter names when computing attr_names, but it's more consistent to be here (especially if other filtering beside name is needed)
        frame_class_instance = frame_class()  # Instantiate the class, as tab_title is an instance attribute
        print(frame_class.__name__, frame_class_instance.tab_title)