我怎样才能以 DRY 方式分解这个 py 文件?

How can I break up this py file in a DRY fashion?

在 Flask 应用程序的业务逻辑上下文中,我正在编写 一吨 这些 class 的“定义”实例,将它们放入一个列表,并在需要的地方导入列表。在构建它之外,该列表被视为静态的。

简化示例:

definitions.py:

from my_object import MyObject

definition_registry = list()

# team 1, widget 1 definition
_definition = MyObject()
_definition.name = "team 1 widget 1"
_definition.coercer = str
definition_registry.append(_definition)

# team 1, widget 2 definition
_definition = MyObject()
_definition.name = "team 1 widget 2"
_definition.coercer = int
definition_registry.append(_definition)

# team 2, widget 1 definition
_definition = MyObject()
_definition.name = "team 2 widget 1"
_definition.coercer = float
definition_registry.append(_definition)

my_object.py:

class MyObject:
    def __init__(self):
        self.name = "unnamed"
        self.coercer = int

    def __repr__(self):
        return f"MyObject instance: {self.name} / {self.coercer}"

main.py:

from definitions import definition_registry

if __name__ == '__main__':
    print(definition_registry)

输出:

[MyObject instance: team 1 widget 1 / <class 'str'>, MyObject instance: team 1 widget 2 / <class 'int'>, MyObject instance: team 2 widget 1 / <class 'float'>]

如何将 definitions.py 分成多个文件(team_1.pyteam_2.py、...)?

重要提示: 必须在 python 中定义真实 MyObject 的实例。在我的示例中,coercer 属性用作强调这一事实的占位符。

我考虑过使用 exec,但这通常是一种不好的做法,而且这并不是该规则的一个很好的例外。例如,将 definitions.py 的第 5 到 9 行放入 team1w1.py 并将它们替换为 exec(open(team1w1.py).read()) 可以工作,但 PyCharm 的调试器不会执行 team1w1.py 行 -旁白

另一种方法是做类似

的事情
from team1w1 import definition
definition_registry.append(definition)

from team1w2 import definition
definition_registry.append(definition)
...

这个好多了,但还是有味道,因为

class MyObject():

    all_instances = []

    def __init__(name, coercer):
        self.name = name
        self.coercer = coercer
        all_instance.append(self)

您的安装程序用法是

import MyObject

# Put your specifications into a readable list
widget_specs = [
    ["Team 1 widget 1", str],
    ["Team 1 widget 2", float],
    ...
]
for name, coercer in widget_specs:
    _ = MyObject(name, coercer)

然后您访问 MyObject.all_instances 以获得您想要的小部件对象。

这会解决问题吗,或者至少让你足够接近?

我会这样组织它:

myobject.py

class MyObject:
    def __init__(self, name="unnamed", coercer= int):
        self.name = name
        self.coercer = coercer

    def __repr__(self):
        return f"MyObject instance: {self.name} / {self.coercer}"

definitions.py

# just done for 2 items - If you want to distribute those definitions, 
# you can define each element in a single file
teams = [{"name": "team 1 widget 1", "coercer": str}, {"name": "team 1 widget 2", "coercer": int}]

definition_registry = [MyObject(**element) for element in teams]

有几种方法可以做到这一点。搜索代码以实现插件。这是一种方法:

您的代码结构如下:

/myproject
    main.py
    my_object.py
    definitions/
        __init__.py
        team_1.py
        team_2.py

main.py

这与您的代码基本相同,只是增加了一些额外的代码来显示正在发生的事情。

import sys

before = set(sys.modules.keys())

import definitions

after = set(sys.modules.keys())

if __name__ == '__main__':
    print('\nRegistry:\n')
    for item in definitions.registry:
        print(f"    {item}")
    print()

    # this is just to show how to access things in team_1
    print(definitions.team_1.foo)
    print()

    # this shows that the modules 'definitions', 'definitions.team_1',
    # and 'definitions.team_2' have been imported (plus others)
    print(after - before)

my_object.py

正如其他人指出的那样,MyObject 可以将 name 和 coercer 作为参数 __init__(),注册表可以是 class 变量,注册由 __init__().

处理
class MyObject:
    registry = []
    
    def __init__(self, name="unnamed", coercer=str):
        self.name = name
        self.coercer = coercer
        
        MyObject.registry.append(self)
        
    def __repr__(self):
        return f"MyObject instance: {self.name} / {self.coercer}"

定义/init.py

这是技术的核心。导入包时,__init__.py 得到 运行,例如 main.py 得到 import definitions。主要思想是使用 pathlib.Path.glob() 查找名称如 team_* 的所有文件并使用 importlib.import_module():

导入它们
import importlib
import my_object
import pathlib

# this is an alias to the class variable so it can be referenced
# like definitions.registry
registry = my_object.MyObject.registry

package_name = __package__
package_path = pathlib.Path(__package__)

print(f"importing {package_name} from {__file__}")

for file_path in package_path.glob('team_*.py'):
    module_name = file_path.stem
    print(f"    importing {module_name} from {file_path}")
    importlib.import_module(f"{package_name}.{module_name}")

print("    done")

definitions/team_1.py

需要导入 MyObject 才能创建实例。显示模块中可以实例化多个 MyObjects,以及其他实例。

import pathlib
from my_object import MyObject

file_name = pathlib.Path(__file__).stem

print(f"        in {__package__}.{file_name}")

# assign the object (can get it through registry or as team_1.widget_1
widget_1 = MyObject("team 1 widget 1", str)

# don't assign the object (can only get it through the registry)
MyObject("team 1 widget 2", int)

# can define other things too (variables, functions, classes, etc.)
foo = 'this is team_1.foo'

definitions/team_2.py

from my_object import MyObject

print(f"        in {__package__}.{__file__}")

# team 2, widget 1 definition
MyObject("team 2 widget 1", float)

其他

如果你不能改变MyObject,也许你可以subclass它并使用team_1.py中的subclass等

或者,定义一个 make_myobject() 工厂函数:

def make_myobject(name="unknown", coercer=str):
    definition = MyObject()
    definition.name = name
    definition.coercer = coercer
    registry.append(definition)
    return definition

那么 team_1.py 看起来像:

from my_object import make_myobject

make_myobject("team 1 widget 1", int)

....

最后,intstr等类型,classes等可以按名称查找。因此,在您的简化示例中,MyObject()make_myobject() 可以采用 coercer 的名称并进行查找。

import sys

def find_coercer(name):
    """Find the thing with the given name. If it is a dotted name, look
    it up in the named module. If it isn't a dotted name, look it up in
    the 'builtins' module.
    """
    module, _, name = name.strip().rpartition('.')

    if module == '':
        module = 'builtins'

    coercer = getattr(sys.modules[module], name)

    return coercer