跳转到主要内容

一个用于控制您的库公共API的小型Python模块。

项目描述

元模块 - 定义Python API的有用工具和技巧

在Python中,编写一个元类可以让您创建新的类对象,您可以通过它来控制这些对象的行为。

通过类比(以及一点英语的滥用),编写一个元模块可以让您创建具有定制行为的模块对象。

metamodule.py 是一个单文件、许可宽松的Python库,它使您能够轻松且安全地将自定义模块子类型用作库的公共接口。例如,在Python中,当有人调用已弃用的函数(mymodule.foo())时,很容易发出弃用警告,但当有人访问已弃用的常量(mymodule.FOO)时,发出弃用警告就非常困难。另一个常见请求(尽管有些危险)的功能是能够在首次访问子模块时才导入它(mymodule.submodule.subfunction())。使用元模块,这两个问题都很容易解决:我们只需为 mymodule 提供一个定制的 __getattr__ 方法,使其执行我们想要的操作。(实际上,您甚至不需要编写这个 __getattr__ 方法 - 元模块包含了一个实现,它提供上述两个功能。)

示例/文档

在此项目的源目录中,尝试启动一个Python REPL并运行

>>> import examplepkg

examplepkg 是一个模块对象

>>> import types
>>> isinstance(examplepkg, types.ModuleType)
True

但这不是一个普通的模块对象;它是一个自定义的子类

>>> examplepkg
<FancyModule 'examplepkg' from 'examplepkg/__init__.py'>

而这个子类拥有超能力

# Automatically loads the submodule on first access:
>>> examplepkg.submodule.subattr
... submodule loading ...
'look ma no import'

# Imports are cached so future usage is just as fast as regular access:
>>> examplepkg.submodule.subattr
'look ma no import'

# Accessing this attribute triggers a warning:
>>> examplepkg.a
__main__:1: FutureWarning: 'a' attribute will become 2 in next release
1

# But regular attributes continue to work fine, with no speed penalty:
>>> examplepkg.b
2

# reload() works fine (except on CPython 3.3, which is buggy)
>>> import imp
>>> imp.reload(examplepkg)
<FancyModule 'examplepkg' from 'examplepkg/__init__.pyc'>

# And functions defined in the package use the same globals dict
# as the package itself. (On py2 replace .__globals__ with .func_globals)
>>> examplepkg.__dict__ is examplepkg.f.__globals__
True

为了实现这一点,我们只需要在 examplepkg/__init__.py 文件的顶部放置以下代码

# WARNING: this should be placed at the *very top* of your module,
# *before* you import any code that might recursively re-import
# your package.
import metamodule
metamodule.install(__name__)
del metamodule

# Any strings in this set name submodules that will be lazily imported:
# NB: you probably shouldn't use this unless you have a real,
# specific need for it, since it can cause import errors and other
# side-effects to appear at weird and confusing places.
__auto_import__.add("submodule")

# Attributes that we want to warn users about:
__warn_on_access__["a"] = (
    # Attribute value
    1,
    # Warning issued when attribute is accessed
    FutureWarning("'a' attribute will become 2 in next release"))

您也可以定义自己的 ModuleType 子类,并将其作为 metamodule.install 的第二个参数传入。您的类可以执行任何您可以使用 Python 类执行的常规操作 - 定义特殊方法如 __getattribute__,使用属性,自定义 __repr__ 等。请注意,您的类实例的 __dict__ 将是模块的全局字典,因此将 self.foo 赋值为在您的模块中创建名为 foo 的全局变量,反之亦然。

需要注意的一点是,您的类的 __init__ 方法将 不会 被调用 - 相反,您应该定义一个名为 __metamodule_init__ 的方法,该方法将在您的元模块类安装后立即被调用。

支持的版本

元模块目前与以下版本进行了测试

  • CPython 2.6, 2.7

  • CPython 3.2, 3.3, 3.4,以及 3.5 的预发布版本

我怀疑它几乎可以在具有有效 ctypes 的 CPython 的任何版本上工作,我只是没有方便的途径访问旧版本进行测试。

据我所知,我们目前还不支持 PyPy、Jython 等,但一旦它们赶上了 Python 3.5 并开始允许在模块对象上执行 __class__ 赋值,我们将很快支持。

它的工作原理

Python 一直允许通过将新对象分配给 sys.modules["mymodule"] 的机制在一定程度上执行这类操作;此对象可以具有您喜欢的任何行为。这可以很好地工作,但最终结果是您有两个不同的对象都代表同一个模块:您的原始模块对象(它拥有您的模块代码执行时的 globals() 命名空间),以及您的自定义对象。根据对 sys.modules 的分配和子模块导入的相对顺序,您可能会在同一个程序中的不同代码部分得出 mymodule 指的是这两个对象中的任何一个的结论。如果它们不共享相同的 __dict__,那么它们的命名空间可能会不同步;或者,如果它们 确实 共享相同的 __dict__,这意味着您的自定义对象不能继承 ModuleType(模块对象不允许重新分配其 __dict__ 属性),这会破坏 reload()。总的来说,这是一个有点混乱的情况。如果您非常小心,可以编写正确的代码 - 例如,apipkg 是一个使用类似方法的库,但为了使事情可行,它要求您的库的公共接口完全由 apipkg 调用定义。没有简单的方法可以将遗留的 Python 包逐步切换到使用 apipkg。

元模块提供的关键特性是:它使设置 sys.modules["mymodule"] 变得既容易又安全,使其(a)成为您控制的一个类的实例,因此您可以拥有任意属性等,并且(b)是一个 ModuleType 的常规子类,其 __init__.pyglobals() 作为其 __dict__ 属性,这样您就可以继续使用定义您的 API 的常规 Python 方法。

这种组合使得将现有库转换为使用元模块变得既容易又安全 - 只需在您的 __init__.py 文件的顶部添加对 metamodule.install 的调用即可,除了您可以现在随心所欲地利用您的新超能力外,没有任何变化。

我们如何实现它?在CPython 3.5及以后版本,这很简单:元模块使用模块对象的__class__赋值(这一特性被明确添加到CPython中,以支持这种用法)。

在CPython 3.4及更早版本中,它使用ctypes技巧。这些方法很丑陋,但只要没有人回到过去更改旧版本Python中模块对象的内部内存布局,它们就是安全的。(这种情况不会发生。)基本上,我们实例化一个新的ModuleType子类对象,然后我们利用一些关于这些对象布局的神秘知识来交换原始模块和新的对象的核心内容。然后我们将新的对象赋值给sys.modules。这保持了关键的不变性,即在任意时刻,只有一个模块拥有你的全局字典,并且它在sys.modules中。然而,这意味着如果有人在你调用metamodule.install之后导入你的模块,事情就会变得非常糟糕。所以除非你只想支持Python 3.5+,否则请确保在模块定义文件的顶部调用metamodule.install

这两个技巧结合使我们能够安全地支持所有版本的CPython,随着像PyPy这样的替代实现开始支持__class__赋值,我们也会支持这些。

变更历史

1.1:

  • 在查找__metamodule_init__时,直接跳到类而不检查实例。这使得我们的行为与常规的__init__更加一致,并避免了意外触发__getattr__。(感谢Antony Lee的报告+修复。)

1.0:

  • 首次公开发布。

项目详情


下载文件

下载适用于您的平台的文件。如果您不确定选择哪个,请了解更多关于安装软件包的信息。

源分布

metamodule-1.1.zip (19.5 kB 查看散列)

上传时间

构建分布

metamodule-1.1-py2.py3-none-any.whl (11.6 kB 查看散列)

上传时间 Python 2 Python 3

由以下机构支持

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误记录 StatusPage StatusPage 状态页面