I discovered that the existence and use of metaclasses can save you from a lot code-writing by providing an elegant handle on the process of class creation. I use this in my application, where several interacting servers are instantiated. To elaborate:
Each device instantiates a server class specific to its operation, which is a subclass of (a subclass of...) ulitmately this one BaseServer
class. Now, some device servers need a ThreadedTCPserver
, and some need a SimpleTCPServer
(module: socketserver
). They cannot all derive from the same class because using the ThreadingMixin
overrides the behavior of the SimpleTCPServer
.
To handle this dynamic class configuration, I created a MetaServerType
, which chooses the baseclasses for BaseServer as (SimpleTCPServer,)
or as (ThreadedTCPServer,)
--> producing my desired result of dynamically configured server classes! (Woo hoo)
Now, here comes my question: I would like to use a configuration file where parameters are stored, and these parameters are used by default by the MetaServerType. For example: config.default_loglevel, or config.default_handler etc. And individual servers can be overriden (from command-line or otherwise) according to the metaclass specifications.
It is good design practice to have only one instance of the configuration object being passed through the program-flow? One way to have this is to initialize the config object in the class-body of the metaclass -- but my program-flow begins elsewhere, and this means that the metaclass is called several times thus producing various instances of config. It appears that metaclass is called at import time (?)
So a detailed answer would be very welcome to:
- How can one supply metaclasses with configuration info?
- What is a good way to have a single config instance be passed through the program-flow, to be edited, updated and perhaps eventually written?
- Can the input arguments to metaclass be somehow extended beyond the
Metaclass.__new__(meta, name, bases, attrs)
?- Bonus question: Does this move us one step closer to a finite-state machine (of servers) so that the state (not the instances) can be 'paused' or 'resumed'?
1 - How can one supply metaclasses with configuration info?
There are a couple of ways to do that - since your metaclasses live in their own module
(and yes, the module is executed once at import
time, regardless of how many times it is imported in the same application), a nice way to configure them would be to have a callable object (either a class or function on the same module), that would setup "global variables" that would be used for configuration.
Despite their bad reputation due to C where the name "global" originates, global variables in Python are actually "module" variables: that means that all the functions (including methods) in that module can access these variables. Functions or code in other modules would have to prefix the module name for that.
So a function like:
def configure_servers(p1, p2,...):
global opt1, opt2, ...
opt1 = p1
opt2 = p2
(...)
Could be called from your application entry-point, before the server instances are created. (Of course, you could pass a config-file path to be read instead of p1
, p2
, ...)
2 - What is a good way to have a SINGLE config instance be passed through the program-flow, to be edited, updated and perhaps eventually written?
A global (module) variable name on the metaclass module could be read by all of them, and it could be associated with a complex configuration object. Maybe the existence of a "config" function like the one above can render this question obsolete.
But in case you really need a "singleton" object, that is, an object of which there is just one instance, you can do it the easy way: Have a single class on the metaclass dictionary, and pass that class around, instead of an instance of it. Better, and cleaner if you have a dictionary instead of a class.
If you need to create a "real" singleton object, you should a class and override the __new__
method in it so that it always returns the first created instance -
Example:
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 - Can the input arguments to metaclass be somehow extended beyond the Metaclass.new(meta, name, bases, attrs) ?
Not taking advantage of the language syntax.
I mean, it is always possible to call the metaclass as a normal Python call, but that would prevent you from using the language syntax to describe your class: you'd need to define the class body as a dictionary to pass in as attrs
for the call.
For example, to create a derived exception class, one could do:
MyException = type("MyException", (Exception, ), {})
Instead of:
class MyException(Exception):
pass
The usual way of passing additional information to the metaclass is using attributes with fixed names on the class body. The metaclass then checks these attributes inside attrs
and uses those. It can choose to keep then in the resulting class, or delete them from the attrs
dict at this point.
If the information you need to pass the metaclass is only known at runtime, these attributes can point to other (module-level) variables, or contain Python expressions that are evaluated at class creation time.
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,...):
...
In the example above, your metaclass could consume the _server_type
and _server_params
attributes to further control the class creation.
精彩评论