Как вызывать методы для объектов дескриптора класса Python?

Я создал class String() с __get__(), __set__() и методом to_db(); однако, когда я делаю name = String(), я не могу выполнить self.name.to_db(), потому что он вызывает to_db() для значения, возвращаемого __get__(), а не для объекта "name".

class String(object):

    def __init__(self, value=None):
        if value:
            self.value = str(value)

    def __get__(self, instance, owner):
        return self.value

    def __set__(self, instance, value):
        self.value = str(value)

    def to_db(self):
        return {'type':'string', 'value': self.value}

class Example(object):

    name = String()
    age = Integer()

    def __init__(self,name,age):
        self.name = name
        self.age = age

    def save():
        data = dict(name=self.name.to_db(), age=self.age.to_db())
        db.save(data)

Один из способов справиться с этим — не вызывать self.name.to_db() напрямую, а вместо этого установить флаг в instance и создать условное выражение в __get__(), чтобы проверить его и вызвать to_db(), если оно True, но это кажется неуклюжим. Есть ли способ лучше?

Кроме того, я новичок в дескрипторах — каковы плюсы и минусы использования instance и/или instance.__dict__ для хранения состояния по сравнению с сохранением его в self?


person espeed    schedule 16.05.2011    source источник
comment
Как насчет того, чтобы не использовать дескрипторы для всего, что требует явных вызовов методов?   -  person    schedule 17.05.2011
comment
Мне кажется, вы пытаетесь использовать дескрипторы неправильным образом. Вот хорошее описание: docs.python.org/howto/descriptor.html, особенно обратите внимание на реализацию свойства().   -  person Zaur Nasibov    schedule 17.05.2011
comment
Почему вы не используете свойства?   -  person Jochen Ritzel    schedule 17.05.2011
comment
String объект дескриптора будет общим для всех Example объектов. Итак, если вы создадите exp1 = Example("Example1") и exp2 = Example("Example2"), то exp1.name и exp2.name дадут тот же результат 'Example2'. Вместо этого используйте property согласно @JochenRitzel   -  person Jaynti Kanani    schedule 02.01.2014


Ответы (4)


Это довольно просто - просто сделайте так, чтобы ваш дескриптор возвращал подкласс строки с дополнительными методами, которые вы хотите.

def __get__(self, instance, owner):
    class TaggedString(str):
        def to_db(self):
            return {'type':'string', 'value': self}
    return TaggedString(self.value)`
person Jeff Rush    schedule 09.08.2011
comment
Это не работает полностью, как в моем комментарии выше. Если вы действительно хотите использовать дескриптор - используйте аргумент instance с меткой, чтобы получить value за уровень экземпляра. - person Jaynti Kanani; 02.01.2014

Вот решение, позволяющее обойти любые дескрипторы, определенные в классе:

class BypassableDescriptor(object):
    pass

class String(BypassableDescriptor):
    def __init__(self, value=None):
        if value:
            self.value = str(value)

    def __get__(self, instance, owner):
        return self.value

    def __set__(self, instance, value):
        self.value = str(value)

    def to_db(self):
        return {'type': 'string', 'value': self.value}

class Integer(BypassableDescriptor):
    def __init__(self, value=None):
        if value:
            self.value = str(value)

    def __get__(self, instance, owner):
        return self.value

    def __set__(self, instance, value):
        self.value = int(value)

    def to_db(self):
        return {'type': 'integer', 'value': self.value}

class BypassDescriptor(object):
    def __init__(self, descriptor):
        self.descriptor = descriptor

    def __getattr__(self, name):
        return getattr(self.descriptor, name)

class AllowBypassableDescriptors(type):
    def __new__(cls, name, bases, members):
        new_members = {}
        for name, value in members.iteritems():
            if isinstance(value, BypassableDescriptor):
                new_members['real_' + name] = BypassDescriptor(value)
        members.update(new_members)
        return type.__new__(cls, name, bases, members)

class Example(object):
    __metaclass__ = AllowBypassableDescriptors

    name = String()
    age  = Integer()

    def __init__(self,name,age):
        self.name = name
        self.age  = age

    def save(self):
        data = dict(name = self.real_name.to_db(), age = self.real_age.to_db())

Обратите внимание, что это не идеально — вам всегда придется вызывать real_fieldname.method() вместо fieldname.method(), и вам придется использовать метакласс AllowBypassableDescriptors для всех ваших классов, которым необходим доступ к этому полю. Опять же, это довольно совместимое решение, которое позволяет избежать исправления обезьяной объекта, обернутого дескриптором.

Тем не менее, я не уверен, что дескрипторы — лучшее решение для того, что вы пытаетесь сделать (запись в базу данных?).

person Boaz Yaniv    schedule 16.05.2011

Дескрипторы используются для описания «что это такое» или «как это работает».

Например, мы можем ввести некоторые ограничения в __get__() или __set__().

По вашему вопросу, я думаю, вы хотите добавить свой метод в type<str>, а не описывать, как установить или получить экземпляр.

Таким образом, вы можете использовать приведенный ниже код, чтобы выразить то, что вы хотите сделать.

class String(str):
    def __init__(self, value):
        self.value = value

    def to_db(self):
        return {'type':'string', 'value': self.value}

ss = String('123')
print ss #123
print ss.to_db() #{'type':'string', 'value': '123'}
person IvanaGyro    schedule 26.01.2017

Внутри метода to_db вы можете получить прямой доступ к значению через

self.__dict__['value'] # value as key is not ideal, but that's what OP used

или, если вы используете только новые классы стиля,

object.__set__(self, name, value)

Поскольку вы используете магические атрибуты, доступ к магическому __dict__ вполне разумен.

Это также упоминается в документации для __setattr__ [1] (извините, нет прямой ссылки на __dict__ в __set__, но это тот же проблемный домен)

If __setattr__() wants to assign to an instance attribute, it should not 
simply execute   self.name = value — this would cause a recursive call to itself. 
Instead, it should insert the value in the dictionary of instance attributes, e.g., 
self.__dict__[name] = value. For new-style classes, rather than accessing the instance 
dictionary, it should call the base class method with the same name, for example, 
object.__setattr__(self, name, value).

[1] http://docs.python.org/2/reference/datamodel.html#customizing-attribute-access

person Riccardo Galli    schedule 16.05.2011
comment
Это в лучшем случае неприятный обходной путь. Я бы не стал возиться с __dict__. - person ; 17.05.2011
comment
Возможно, я что-то упускаю, но e = Example(name=Julie,age=21) then e.name.to_db() говорит: ››› e.name.to_db() Traceback (последний последний вызов): File ‹ stdin›, строка 1, в ‹module› AttributeError: объект 'str' не имеет атрибута 'to_db' -- предположительно потому, что сначала вызывается get. - person espeed; 17.05.2011