在Python 3.0中动态添加方法到类

12 投票
3 回答
6734 浏览
提问于 2025-04-17 07:05

我正在尝试用Python写一个数据库抽象层,这样可以通过链式调用来构建SQL语句,比如:

results = db.search("book")
          .author("J. K. Rowling")
          .price("<40.00")
          .title("Harry")
          .execute()

但是当我试图动态地给数据库类添加所需的方法时,遇到了一些问题。

以下是我代码中重要的部分:

import inspect

def myName():
    return inspect.stack()[1][3]

class Search():

    def __init__(self, family):
        self.family = family
        self.options = ['price', 'name', 'author', 'genre']
        #self.options is generated based on family, but this is an example
        for opt in self.options:
            self.__dict__[opt] = self.__Set__
        self.conditions = {}

    def __Set__(self, value):
        self.conditions[myName()] = value
        return self

    def execute(self):
        return self.conditions

然而,当我运行这样的示例时:

print(db.search("book").price(">4.00").execute())

输出结果是:

{'__Set__': 'harry'}

我这样做是不是错了?有没有更好的方法来获取被调用函数的名称,或者以某种方式制作一个函数的“硬拷贝”?

3 个回答

5

首先,你不是在给类添加东西,而是在给实例添加东西。

其次,你不需要直接访问 dict。用 self.__dict__[opt] = self.__Set__ 这种方式不如用 setattr(self, opt, self.__Set__) 来得好。

第三,不要用 __xxx__ 作为属性名称。这些名称是留给 Python 内部使用的。

第四,正如你注意到的,Python 不容易被愚弄。你调用的方法的内部名称仍然是 __Set__,即使你用不同的名字来访问它。:-) 这个名字是在你定义方法的时候通过 def 语句设置的。

你可能想用元类来创建和设置选项方法。你也许还想真正创建这些方法,而不是试图用一个方法来处理所有的情况。如果你真的只想用一个方法,__getattr__ 是个办法,但这可能有点麻烦,我一般不推荐这样做。使用 lambda 或其他动态生成的方法可能会更好。

5

这里有一些可以让你入门的代码(虽然不是你想写的整个程序,但它展示了各个部分是如何组合在一起的):

class Assign:

    def __init__(self, searchobj, key):
        self.searchobj = searchobj
        self.key = key

    def __call__(self, value):
        self.searchobj.conditions[self.key] = value
        return self.searchobj

class Book():

    def __init__(self, family):
        self.family = family
        self.options = ['price', 'name', 'author', 'genre']
        self.conditions = {}

    def __getattr__(self, key):
        if key in self.options:
            return Assign(self, key)
        raise RuntimeError('There is no option for: %s' % key)

    def execute(self):
        # XXX do something with the conditions.
        return self.conditions

b = Book('book')
print(b.price(">4.00").author('J. K. Rowling').execute())
13

你可以在创建类之后,简单地添加搜索功能(方法):

class Search:  # The class does not include the search methods, at first
    def __init__(self):
        self.conditions = {}

def make_set_condition(option):  # Factory function that generates a "condition setter" for "option"
    def set_cond(self, value):
        self.conditions[option] = value
        return self
    return set_cond

for option in ('price', 'name'):  # The class is extended with additional condition setters
    setattr(Search, option, make_set_condition(option))

Search().name("Nice name").price('$3').conditions  # Example
{'price': '$3', 'name': 'Nice name'}

注意:这个类有一个 __init__() 方法,但它没有 family 参数(条件设置器是在运行时动态添加的,但它们是添加到类上的,而不是单独添加到每个实例)。如果需要创建带有不同条件设置器的 Search 对象,那么可以使用下面这种方法(__init__() 方法有一个 family 参数):

import types

class Search:  # The class does not include the search methods, at first

    def __init__(self, family):
        self.conditions = {}
        for option in family:  # The class is extended with additional condition setters
            # The new 'option' attributes must be methods, not regular functions:
            setattr(self, option, types.MethodType(make_set_condition(option), self))

def make_set_condition(option):  # Factory function that generates a "condition setter" for "option"
    def set_cond(self, value):
        self.conditions[option] = value
        return self
    return set_cond

>>> o0 = Search(('price', 'name'))  # Example
>>> o0.name("Nice name").price('$3').conditions
{'price': '$3', 'name': 'Nice name'}
>>> dir(o0)  # Each Search object has its own condition setters (here: name and price)
['__doc__', '__init__', '__module__', 'conditions', 'name', 'price']

>>> o1 = Search(('director', 'style'))
>>> o1.director("Louis L").conditions  # New method name
{'director': 'Louis L'}
>>> dir(o1)  # Each Search object has its own condition setters (here: director and style)
['__doc__', '__init__', '__module__', 'conditions', 'director', 'style']

参考链接: http://docs.python.org/howto/descriptor.html#functions-and-methods


如果你真的需要知道存储属性名称的搜索方法,可以在 make_set_condition() 中简单地设置它,具体做法是:

       set_cond.__name__ = option  # Sets the function name

(在 return set_cond 之前)。在这样做之前,方法 Search.name 的名称是:

>>> Search.price
<function set_cond at 0x107f832f8>

在设置了它的 __name__ 属性后,你会得到一个不同的名称:

>>> Search.price
<function price at 0x107f83490>

这样设置方法名称可以让涉及该方法的错误信息更容易理解。

撰写回答