类由元类配置。但我们可以配置元类吗?
我发现使用元类可以让你在创建类的过程中省去很多代码,它提供了一种优雅的方式来处理类的创建。我在我的应用中使用了这个方法,那里有几个相互作用的服务器被实例化。具体来说:
每个设备都会实例化一个特定于其操作的服务器类,这个类是最终继承自这个 BaseServer
类的子类。现在,有些设备服务器需要 ThreadedTCPserver
,而有些则需要 SimpleTCPServer
(模块:socketserver
)。它们不能都从同一个类派生,因为使用 ThreadingMixin
会覆盖 SimpleTCPServer
的行为。
为了处理这种动态的类配置,我创建了一个 MetaServerType
,它根据需要选择 BaseServer
的基类,可以是 (SimpleTCPServer,)
或 (ThreadedTCPServer,)
——这样就能动态配置出我想要的服务器类!(太棒了)
接下来,我有个问题:
我想使用一个配置文件来存储参数,这些参数默认会被 MetaServerType
使用。例如:config.default_loglevel,或者 config.default_handler 等等。而且每个服务器可以根据元类的规定被覆盖(通过命令行或其他方式)。
在设计上,只有一个实例的配置对象在程序中流动是个好习惯吗?实现这一点的一种方法是在元类的类体中初始化配置对象——但我的程序流是在其他地方开始的,这意味着元类会被调用多次,从而产生多个配置实例。看起来元类是在导入时被调用的(?)
所以,详细的回答会很受欢迎,关于:
- 如何给元类提供配置信息?
- 有什么好的方法可以让单个配置实例在程序中流动,以便进行编辑、更新,甚至最终写入?
- 元类的输入参数是否可以在
Metaclass.__new__(meta, name, bases, attrs)
之外进行扩展?- 附加问题:这是否让我们更接近于一个有限状态机(服务器),以便状态(而不是实例)可以被“暂停”或“恢复”?
1 个回答
1 - 如何给 metaclass 提供配置信息?
有几种方法可以做到这一点。因为你的 metaclass 生活在自己的模块里(而且是的,这个模块在 import
时只会执行一次,无论在同一个应用中被导入多少次),一个不错的配置方式是创建一个可调用的对象(可以是同一模块中的类或函数),用来设置将用于配置的“全局变量”。
尽管全局变量在 C 语言中名声不好,但在 Python 中,全局变量实际上是“模块”变量:这意味着该模块中的所有函数(包括方法)都可以访问这些变量。而其他模块中的函数或代码需要在前面加上模块名才能访问。
所以像这样的函数:
def configure_servers(p1, p2,...):
global opt1, opt2, ...
opt1 = p1
opt2 = p2
(...)
可以在你的应用程序入口点被调用,在服务器实例创建之前。(当然,你也可以传递一个配置文件路径来读取,而不是 p1
, p2
, ...)
2 - 有什么好的方法可以让一个单一的配置实例在程序流程中传递,以便编辑、更新,甚至最终写入?
在 metaclass 模块中,一个全局(模块)变量名可以被所有人读取,并且可以与一个复杂的配置对象关联。也许像上面那样的“config”函数的存在可以让这个问题变得不再重要。
但是如果你真的需要一个“单例”对象,也就是只有一个实例的对象,你可以简单地做到这一点:在 metaclass 字典中有一个单一的类,并传递这个类,而不是它的实例。如果你有一个字典而不是一个类,那会更好、更干净。
如果你需要创建一个“真正的”单例对象,你应该定义一个类,并重写 __new__
方法,使其始终返回第一个创建的实例 - 示例:
class Singleton(object):
_instance = None
def __new__(cls, *args, **kw):
if cls._instance is not None:
return cls._instance
self = object.__new__(cls, *args, **kw)
cls._instance = self
return self
3 - metaclass 的输入参数是否可以在 Metaclass.new(meta, name, bases, attrs) 之外扩展?
不利用语言的语法。我是说,调用 metaclass 作为普通的 Python 调用总是可以的,但这样会阻止你使用语言语法来描述你的类:你需要将类体定义为一个字典,以便作为 attrs
传入。
例如,要创建一个派生的异常类,可以这样做:
MyException = type("MyException", (Exception, ), {})
而不是:
class MyException(Exception):
pass
通常向 metaclass 传递额外信息的方法是使用类体中固定名称的属性。然后,metaclass 会检查这些属性在 attrs
中,并使用它们。它可以选择将这些属性保留在生成的类中,或者在此时从 attrs
字典中删除它们。
如果你需要传递给 metaclass 的信息只有在运行时才能知道,这些属性可以指向其他(模块级别的)变量,或者包含在类创建时被评估的 Python 表达式。
mod_server_type = "TCP"
class YAServer(ParentServer):
__metaclass__ = ServerMetaBase
_sever_type = mod_server_type
with open("config_file") as config:
_server_params = pickle.load(config)
del config
def __init__(self,...):
...
在上面的例子中,你的 metaclass 可以使用 _server_type
和 _server_params
属性来进一步控制类的创建。