Intercepting module calls?

2020-03-29 18:52发布

I'm trying to 'intercept' all calls to a specific module, and reroute them to another object. I'd like to do this so that I can have a fairly simple plugin architecture.

For example, in main.py

import renderer

renderer.draw('circle')

In renderer.py

specificRenderer = OpenGLRenderer()

#Then, i'd like to route all calls from main.py so that
#specificRenderer.methodName(methodArgs) is called
# i.e. the above example would call specificRenderer.draw('circle')

This means that any function can just import renderer and use it, without worrying about the details. It also means that I can completely change the renderer just by creating another object and assigning it to the 'specificRenderer' value in renderer.py

Any ideas?

标签: python module
5条回答
ら.Afraid
2楼-- · 2020-03-29 19:28

My answer is very similar to @kindall's although I got the idea elsewhere. It goes a step further in the sense that it replaces the module object that's usually put in the sys.modules list with an instance of a class of your own design. At a minimum such a class would need to look something like this:

File renderer.py:

class _renderer(object):
    def __init__(self, specificRenderer):
        self.specificRenderer = specificRenderer
    def __getattr__(self, name):
        return getattr(self.specificRenderer, name)

if __name__ != '__main__':
    import sys
#    from some_module import OpenGLRenderer
    sys.modules[__name__] = _renderer(OpenGLRenderer())

The __getattr__() method simply forwards most attribute accesses on to the real renderer object. The advantage to this level of indirection is that with it you can add your own attributes to the private _renderer class and access them through the renderer object imported just as though they were part of an OpenGLRenderer object. If you give them the same name as something already in an OpenGLRenderer object, they will be called instead, are free to forward, log, ignore, and/or modify the call before passing it along -- which can sometimes be very handy.

Class instances placed in sys.modules are effectively singletons, so if the module is imported in other scripts in the application, they will all share the single instance created by the first one.

查看更多
我只想做你的唯一
3楼-- · 2020-03-29 19:30

If you don't mind that import renderer results an object rather than a module, then see kindall's brilliant solution.

If you want to make @property work (i.e. each time you fetch renderer.mytime, you want the function corresponding to OpenGLRenderer.mytime get called) and you want to keep renderer as a module, then it's impossible. Example:

import time
class OpenGLRenderer(object):
  @property
  def mytime(self):
    return time.time()

If you don't care about properties, i.e. it's OK for you that mytime gets called only once (at module load time), and it will keep returning the same timestamp, then it's possible to do it by copying all symbols from the object to the module:

# renderer.py
specificRenderer = OpenGLRenderer()
for name in dir(specificRenderer):
  globals()[name] = getattr(specificRenderer, name)

However, this is a one-time copy. If you add methods or other attributes to specificRenderer later dynamically, or change some attributes later, then they won't be automatically copied to the renderer module. This can be fixed, however, by some ugly __setattr__ hacking.

查看更多
▲ chillily
4楼-- · 2020-03-29 19:36

The simplest way to do that is to have main.py do

from renderer import renderer

instead, then just name specificRenderer renderer.

查看更多
地球回转人心会变
5楼-- · 2020-03-29 19:39

Edit: This answer does not do what the OP wants; it doesn't instantiate an object and then let calls to a module be redirected to that same object. This answer is about changing which rendering module is being used.

Easiest might be to import the OpenGLRenderer in the main.py program like this:

import OpenGLRenderer as renderer

That's code in just one place, and in the rest of your module OpenGLRenderer can be referred to as renderer.

If you have several modules like main.py, you could have your renderer.py file be just the same line:

import OpenGLRenderer as renderer

and then other modules can use

from renderer import renderer

If OpenGLRenderer doesn't quite quack right yet, you can monkeypatch it to work as you need in the renderer.py module.

查看更多
甜甜的少女心
6楼-- · 2020-03-29 19:46

In renderer.py:

import sys

if __name__ != "__main__":
    sys.modules[__name__] = OpenGLRenderer()

The module name is now mapped to the OpenGLRenderer instance, and import renderer in other modules will get the same instance.

Actually, you don't even need the separate module. You can just do:

import sys
sys.modules["renderer"] = OpenGLRenderer()
import renderer   # gives current module access to the "module"

... first thing in your main module. Imports of renderer in other modules, once again, will refer to the same instance.

Are you sure you really want to do this in the first place? It isn't really how people expect modules to behave.

查看更多
登录 后发表回答