这种层次结构定义在 python 项目中有点不寻常,这就是为什么你很难用日常语法来实现它。你应该退后一步,想想你对这个架构的投入程度,如果现在以更接近常见 python 习语的方式重写它还为时不晚,也许你应该这样做(“显式比隐含更好”尤其是我想到的)。
话虽这么说,如果日常的python 不切实际,你可以用奇怪的python 写你想要的东西,而不用太麻烦。如果您想详细了解函数是如何转化为方法的,请考虑阅读the descriptor protocol。
MyFunPackage/worlds/__init__.py
from . import world1, world2
需要为您创建的任何新的world_n.py 文件更新此行。虽然它可以自动动态导入,但它会破坏任何 IDE 的成员提示,并且需要更多多变的代码。您确实写道,在添加模块时您不想更改任何其他内容,但是希望将文件名添加到这一行是可以的。
此文件不应包含任何其他代码。
MyFunPackage/worlds/world*.py
def frobulate(self):
return f'{self.name} has been frobulated'
无需向world1.py、world2.py 或worlds 文件夹中的任何新文件添加任何特殊代码。只需在其中编写您认为合适的函数即可。
MyFunPackage/helloworlds.py
from types import MethodType, FunctionType, SimpleNamespace
from . import worlds
_BASE_ATTRIBUTES = {
'__builtins__', '__cached__', '__doc__', '__file__',
'__loader__', '__name__', '__package__', '__path__', '__spec__'
}
class Worlds:
def __init__(self, name):
self.name = name
# for all modules in the "worlds" package
for world_name in dir(worlds):
if world_name in _BASE_ATTRIBUTES:
continue # skip non-packages and
world = getattr(worlds, world_name)
function_map = {}
# collect all functions in them, by
for func in dir(world):
if not isinstance(getattr(world, func), FunctionType):
continue # ignoring non-functions, and
if getattr(world, func).__module__ != world.__name__:
continue # ignoring names that were only imported
# turn them into methods of the current worlds instance
function_map[func] = MethodType(getattr(world, func), self)
# and add them to a new namespace that is named after the module
setattr(self, world_name, SimpleNamespace(**function_map))
模块添加逻辑是完全动态的,当你向worlds添加新文件时不需要以任何方式更新。
将其设置为包并安装后,尝试您的示例代码应该可以工作:
>>> from MyFunPackage.helloworld import Worlds
>>> x = Worlds('foo')
>>> x.world1.frobulate()
'foo has been frobulated'
感谢python,如此刻意地暴露你的内部运作。
Tangent:向对象动态添加函数,修补 vs 描述
使用types.MethodType 将函数转换为方法会在其上配置所述描述符协议,并将函数的所有权传递给拥有的实例。由于多种原因,这比将实例修补到签名中更可取。
我会很快举一个例子,因为我认为这很高兴知道。我将在这里跳过命名空间,因为它不会改变行为并且只会让它更难阅读:
class Foo:
"""An example class that does nothing yet."""
pass
def bar(self, text: str) -> str:
"""An example function, we will add this to an instance."""
return f"I am {self} and say {text}."
import inspect
import timeit
import types
# now the gang's all here!
使用 lambda 修补
>>> foo = Foo()
>>> foo.bar = lambda *args, **kwargs: bar(foo, *args, **kwargs)
>>> foo.bar('baz')
'I am <__main__.Foo object at 0x000001FB890594E0> and say baz.'
# the behavior is as expected, but ...
>>> foo.bar.__doc__
None
# the doc string is gone
>>> foo.bar.__annotations__
{}
# the type annotations are gone
>>> inspect.signature(foo.bar)
<Signature (*args, **kwargs)>
# the parameters and their names are gone
>>> min(timeit.repeat(
... "foo.bar('baz')",
... "from __main__ import foo",
... number=100000)
... )
0.1211023000000182
# this is how long a single call takes
>>> foo.bar
<function <lambda> at 0x000001FB890594E0>
# as far as it is concerned, it's just some lambda function
简而言之,在复制基本功能时,会丢失很多信息。这很有可能会成为一个问题,无论是因为您想要正确记录您的工作,想要使用 IDE 的类型提示,还是在调试期间必须通过堆栈跟踪并想知道究竟是哪个函数导致问题。
虽然做这样的事情来修补测试套件中的依赖是完全没问题的,但这不是你应该在代码库的核心中做的事情。
更改描述符
>>> foo = Foo()
>>> foo.bar = types.MethodType(foo, bar)
>>> foo.bar('baz')
'I am <__main__.Foo object at 0x00000292AE287D68> and say baz.'
# same so far, but ...
>>> foo.bar.__doc__
'An example function, we will add this to an instance.'
# the doc string is still there
>>> foo.bar.__annotations__
{'text': <class 'str'>, 'return': <class 'str'>}
# same as type annotations
>>> inspect.signature(foo.bar)
<Signature (text: str) -> str>
# and the signature is correct, without us needing to do anything
>>> min(timeit.repeat(
... "foo.bar('baz')",
... "from __main__ import foo",
... number=100000)
... )
0.08953189999999722
# execution time is 25% lower due to less overhead, no delegation necessary here
>>> foo.bar
<bound method bar of <__main__.Foo object at 0x00000292AE287D68>>
# and it knows that it's a method and belongs to an instance of Foo
以这种方式将函数绑定为方法可以正确保留所有信息。就 python 而言,它现在与任何其他静态绑定而非动态绑定的方法相同。