有没有办法在__getattr__中检查传入参数或根据传入参数重定向调用?

3 投票
3 回答
1590 浏览
提问于 2025-04-17 08:23

一些背景信息:

我们有一个处理交易的系统,根据交易的账单国家来分流。我们有一个日志表,这个表在两个实例中存在,一个是记录欧盟的交易,另一个是记录其他地方的交易。我们还有一个测试库,它管理和隐藏与数据库打交道的复杂部分,简单来说,每个表都用一个类来表示。我有一个类代表这个表,而数据库会话管理类则为这两个类的实例各有两个成员。我想做的是创建一个通用的“元数据访问对象(meta dao)”类,它可以接受任何调用,检查参数,然后根据其中一个输入参数,把调用分发到正确的数据库实例类上。起初我考虑过重载每个方法,但这样做太麻烦了。

我在考虑使用 __getattr__ 来重写方法查找,这样我就可以根据 __getattr__ 接收到的方法名称,调用正确的实例。但据我了解,我不能在 __getattr__ 中检查传入的方法参数,所以在这种情况下我无法正确分发调用。有没有人有其他的想法,或者有没有办法在 __getattr__ 中“检查”参数,而不仅仅是方法名称?

[编辑] 这是我所说的一个通用版本:

class BarBase(object):
    def __init__(self, x):
        self.x = x
    def do_bar(self, i):
        return self.x * i

class FooBar(BarBase):
    def __init__(self, x):
        super(FooBar, self).__init__(x)
    def do_foo(self, i):
        return self.x + i

class MetaFoo(object):
    def __init__(self, bar_manager):
        self.foo_manager = bar_manager
    #something here that will take an arbitrary methodname and args as
    #long as args includes a value named i, inspect i, and call
    #bar_manager.fooa.[methodname](args) if i < 10,
    #and bar_manager.foob.[methodname](args) if i >= 10

class BarManager(object):
    def __init__(self):
        self.bar_list = {}
    def __get_fooa(self):
        if 'fooa' not in self.bar_list.keys():
            self.bar_list['fooa'] = FooBar('a')
        return self.bar_list['fooa']
    fooa = property(__get_fooa)
    def __get_foob(self):
        if 'foob' not in self.bar_list.keys():
            self.bar_list['foob'] = FooBar('b')
        return self.bar_list['foob']
    foob = property(__get_foob)
    def __get_foo(self):
        if 'foo' not in self.bar_list.keys():
            self.bar_list['foo'] = MetaFoo(self)
        return self.bar_list['foo']

3 个回答

0

根据传入的参数进行调度其实是一个两步的过程:

  1. __getattr__ 会返回一个代理方法
  2. 然后 Python 会调用这个代理,代理再决定真正要调用哪个方法

下面是一个例子:

from functools import partial

class TwoFold(object):
    EU = ('GERMANY','FRANCE','ITALY','GREECE',)
    def __getattr__(self, name):
        try:
            EU = object.__getattribute__(self, 'EU_' + name)
            Other = object.__getattribute__(self, 'Other_' + name)
        except AttributeError:
            raise AttributeError(
                "%r is missing an EU_%s or Other_%s" % (self, name, name)
                )
        judge = partial(self._judge, name, EU, Other)
        return judge
    def _judge(self, method_name, EU, Other, *args, **kwargs):
        if kwargs['country'].upper() in self.EU:
            method = EU
        else:
            method = Other
        return method(*args, **kwargs)
    def EU_log(self, tax, country):
        print "logging EU access for %s, tax rate of %r" % (country, tax)
    def Other_log(self, tax, country):
        print "logging non-EU access for %s, tax rate of %r" % (country, tax)

if __name__ == '__main__':
    test = TwoFold()
    test.log(7.5, country='France')
    test.log(10.1, country='Greece')
    test.log(8.9, country='Brazil')
    test.howsat('blah')

运行这个例子会得到:

logging EU access for France, tax rate of 7.5
logging EU access for Greece, tax rate of 10.1
logging non-EU access for Brazil, tax rate of 8.9

接下来是:

Traceback (most recent call last):
  File "test.py", line 29, in <module>
    test.howsat('blah')
  File "test.py", line 10, in __getattr__
raise AttributeError("%r is missing an EU_%s or Other_%s" % (self, name, name))
AttributeError: <__main__.TwoFold object at 0x00B4A970> is missing an
    EU_howsat or Other_howsat

要让这个工作,你要么总是使用相同的关键字参数(并在调用函数时给它命名),要么总是把参数放在同一个位置。或者你可以为每种风格/类别/方法类型创建几个不同的代理。

2

Python 装饰器 是个很好的工具。你可以像这样使用它:

class MetaFoo(object):

    def overload(func):
        """
        we need to check a named variable so for simplicity just checking kwargs
        """
        def _wrapper(*args, **kwargs):
            if kwargs.get('i',0) < 10:
                # get func.func_name from foo and call it
                print "calling foo.",func.func_name
            else:
                print "calling bar.",func.func_name

            func(*args, **kwargs)

        return _wrapper

    @overload
    def func1(self, i):
        print "default functionality"


MetaFoo().func1(i=5)
MetaFoo().func1(i=10)

输出结果:

calling foo. func1
default functionality
calling bar. func1
default functionality

如果你只需要重写几个方法,可以单独给每个方法加上装饰器,甚至可以给不同的方法传递不同的参数,比如差异阈值。但是如果你想重写所有的方法,可能可以通过添加一个元类来实现,这样就能重载这个类的所有方法。不过在这种情况下,像 __getattr__ 这样的方法重写,正如 sth 所建议的,也是一个不错的选择。

2

类似这样的东西应该可以用:

class ProxyCall(object):
   '''Class implementing the dispatch for a certain method call'''
   def __init__(self, proxy, methodname):
      self.proxy = proxy
      self.methodname = methodname

   def __call__(self, *p, **kw):
      if p[0] == "EU": # or however you determine the destination
         return getattr(self.proxy.EU, self.methodname)(*p, **kw);
      else:
         return getattr(self.proxy.OTHER, self.methodname)(*p, **kw);


class Proxy(object):
   '''Class managing the different "equivalent" instances'''
   def __init__(self, EU, OTHER):
      self.EU = EU
      self.OTHER = OTHER

   def __getattr__(self, name):
      if not hasattr(self.EU, name):
         # no such method
         raise AttributeError()
      else:
         # return object that supports __call__ and will make the dispatch
         return ProxyCall(self, name)

然后你可以创建两个实例,并把它们组合到一个代理对象里:

eu = make_instance(...)
other = make_instance(...)
p = Proxy(eu, other)
p.somemethod(foo) 

撰写回答