Динамическое добавление методов в класс в Python 3.0

Я пытаюсь написать уровень абстракции базы данных в Python, который позволяет создавать SQL-статусы с использованием связанных вызовов функций, таких как:

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

но у меня возникают проблемы, когда я пытаюсь динамически добавить необходимые методы в класс db.

Вот важные части моего кода:

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'}

Я иду об этом неправильно? Есть ли лучший способ получить имя вызываемой функции или как-то сделать «печатную копию» функции?


person Codahk    schedule 26.11.2011    source источник
comment
Могу я спросить, почему вы пытаетесь заново изобрести SQLAlchemy?   -  person Lennart Regebro    schedule 26.11.2011
comment
На самом деле я довольно часто пытаюсь написать свои собственные библиотеки, которые воспроизводят уже существующие, чтобы я мог больше узнать о языке и лучше понять, как объединяются более продвинутые части :)   -  person Codahk    schedule 26.11.2011
comment
Хорошо, обучающее упражнение, это хорошая причина. Хотя в этом случае я думаю, что чтение исходного кода SQLAlchemy будет хорошим началом. ORM сложны и волшебны.   -  person Lennart Regebro    schedule 26.11.2011


Ответы (3)


Вы можете просто добавить функции поиска (методы) после создания класса:

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'}

PS. В этом классе есть метод __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>

Установка имени метода таким образом облегчает понимание возможных сообщений об ошибках, связанных с этим методом.

person Eric O Lebigot    schedule 26.11.2011
comment
+1. Ты прав. Я был слишком умен. В этих методах установки условий нет ничего особенного, что требует их создания в каждом экземпляре. - person Eryk Sun; 26.11.2011
comment
Я предполагал, что эти методы должны быть установлены автоматически из схемы, возможно, я слишком много предполагал. :-) - person Lennart Regebro; 26.11.2011
comment
В соответствии с описанием проблемы имена методов будут различаться в зависимости от семейства, поэтому установка имен в классе приведет либо к отсутствию имен для некоторых семейств, либо ко всем именам для каждого семейства. - person Ethan Furman; 29.11.2011
comment
@EthanFurman: Ты прав. Я предположил (возможно, неправильно), что семья на самом деле не является правильным параметром __init__(), потому что __init__() всегда вызывается с одним и тем же семейством в данной программе. Я добавил PS, который подчеркивает этот момент и дает решение, которое дает каждому экземпляру Search собственное семейство возможных вариантов поиска. - person Eric O Lebigot; 30.11.2011

Во-первых, вы ничего не добавляете к классу, вы добавляете его к экземпляру.

Во-вторых, вам не нужен доступ к dict. self.__dict__[opt] = self.__Set__ лучше делать с setattr(self, opt, self.__Set__).

В-третьих, не используйте __xxx__ в качестве имен атрибутов. Они зарезервированы для внутреннего использования Python.

В-четвертых, как вы заметили, Python нелегко обмануть. Внутреннее имя метода, который вы вызываете, по-прежнему __Set__, хотя вы обращаетесь к нему под другим именем. :-) Имя задается, когда вы определяете метод как часть оператора def.

Вероятно, вы захотите создать и установить методы параметров с помощью метакласса. Вы также можете создать эти методы вместо того, чтобы пытаться использовать один метод для всех из них. Если вы действительно хотите использовать только один __getattr__, это способ, но это может быть немного неудобно, я обычно не рекомендую его. Лямбды или другие динамически генерируемые методы, вероятно, лучше.

person Lennart Regebro    schedule 26.11.2011
comment
Вот ссылка на __*__ имена: docs.python.org/ ссылка/ - person Eric O Lebigot; 26.11.2011
comment
@EOL Я всегда думал об именах «xxx» как о «частных» свойствах или методах класса, которые не могут быть вызваны извне? Есть ли какое-либо соглашение, которое вы должны использовать для них? - person Codahk; 26.11.2011
comment
@Ben: Да, есть соглашение, которое очень похоже на нотацию, зарезервированную Python: это __xxx (без завершающего dunder) - та же ссылка, что и выше. - person Eric O Lebigot; 26.11.2011
comment
@EOL: Нет, это не соглашение, это то, что вызывает искажение имен. Другой. - person Lennart Regebro; 26.11.2011
comment
@Ben: Соглашение помечать что-то как внутреннее состоит в том, чтобы начинать его с одного подчеркивания. Наличие двух знаков подчеркивания вызывает искажение имени, которое может быть полезно, чтобы избежать конфликтов имен. - person Lennart Regebro; 26.11.2011
comment
@LennartRegebro: я не вижу проблемы в том, чтобы ссылаться на __xxx как на соглашение Python для «частных» методов или соглашение для запуска искажения имен. Я действительно вижу разницу, но она между соглашением для «частных» методов (__xxx) и изменением имени (которое добавляется поверх соглашения). Вопрос Бена касался соглашения, но действительно интересно отметить, что Python придает ему большую выразительность, искажая имена. Также интересно отметить, что существует понятие полуприватного, неискаженного имени метода (_xxx). - person Eric O Lebigot; 26.11.2011
comment
@EOL: Дело в том, что ведущие двойные подчеркивания вовсе не являются соглашением. Это вызывает изменение имен, чтобы избежать конфликтов имен. Это действительно что-то делает, и это не условность. Соглашение для обозначения того, что что-то является частным/внутренним/используется на свой страх и риск, является одним из ведущих подчеркиваний. - person Lennart Regebro; 26.11.2011

Вот некоторый рабочий код для начала (не вся программа, которую вы пытались написать, а то, что показывает, как части могут сочетаться друг с другом):

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())
person Raymond Hettinger    schedule 26.11.2011
comment
При каждом поиске создается экземпляр Assign, что занимает много времени и памяти. :) Есть ли какое-либо преимущество этого подхода по сравнению с методами прямого добавления после метода создания класса, описанными в моем ответе? - person Eric O Lebigot; 26.11.2011
comment
@EOL Этот подход - это то, как работает сам Python, и это распространенная идиома. Например, Python создает новый экземпляр BoundMethod каждый раз, когда вы вызываете такой метод, как a.m(x). ОП пытается изучить, как работает Python, и уместно научить __call__ для вызовов и __getattr__ для динамического точечного поиска. Вот для чего они нужны. - person Raymond Hettinger; 26.11.2011
comment
Спасибо. Да, __getattr__ и __call__ действительно важно знать, когда речь идет о динамическом поиске. Однако я не уверен, что это то, что нужно ОП: я понимаю, что он хочет определить нединамические атрибуты класса вместо динамических атрибутов экземпляра. Нет? (Я думаю, это в значительной степени суммирует два разных подхода, использованных в наших ответах.) - person Eric O Lebigot; 26.11.2011