How to call methods on Python class descriptor objects?
Asked Answered
H

4

5

I created a class String() with __get__(), __set__(), and a method to_db(); however, when I do name = String(), I can't do self.name.to_db() because it's calling to_db() on the value returned by __get__(), not the object "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)

One way to deal with this is to not call self.name.to_db() directly and instead set a flag in instance and create a conditional in __get__() to check for it and call to_db() if it's True, but this seems kludgy. Is there a better way?

Also, I'm new to descriptors -- what are the pros/cons of using instance and/or instance.__dict__ to store state vs storing it in self?

Heeler answered 16/5, 2011 at 21:20 Comment(4)
How about not using descriptors for anything that requires explicit method calls on them in the first place?Vexillum
Seems to me, you're trying to use descriptors in a wrong way. Here is a good descriptions: docs.python.org/howto/descriptor.html, especially pay attention to property() implementation.Radu
Why are you not using properties?Preciado
String descriptor object will be shared between all Example objects. So if you create exp1 = Example("Example1") and exp2 = Example("Example2"), then exp1.name and exp2.name will give same output 'Example2'. Use property instead as per @JochenRitzelCamail
M
8

It's pretty easy - just have your descriptor return a subclass of string with the extra method(s) you want.

def __get__(self, instance, owner):
    class TaggedString(str):
        def to_db(self):
            return {'type':'string', 'value': self}
    return TaggedString(self.value)`
Moniliform answered 9/8, 2011 at 11:15 Comment(1)
It doesn't work completely as per my above comment. If you really want to use descriptor - make use of instance argument with label to get value per instance level.Camail
C
1

Descriptors are used to describe "what is it" or "how it works".

For example, we can put some restriction in the __get__() or the __set__().

According to your question, I think you want to add your own method into type<str>, not to describe how to set or get the instance.

So you may use thee code below to express what you want to do.

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'}
Clinch answered 26/1, 2017 at 8:50 Comment(0)
M
0

Here's a solution that allows you to bypass any descriptors defined in the class:

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())

Note that it's not perfect - you'll always have to call real_fieldname.method() instead of fieldname.method() and you'll have to use the metaclass AllowBypassableDescriptors for all your classes which need to access this field. Then again, it's a pretty compatible solution that avoids monkey-patching the object wrapped by the descriptor.

That said, I'm not sure that descriptors are the best solution for what you're trying to do (writing to a database?).

Mango answered 16/5, 2011 at 22:52 Comment(0)
E
-1

Inside method to_db you may access directly the value via

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

or, if you are using new style classes only,

object.__set__(self, name, value)

Since you are using magic attributes, accessing the magic __dict__ is perfectly reasonable.

This is also referred in the documentation for __setattr__ [1] (sorry, there is no direct reference to __dict__ in __set__ but it's the same problem domain)

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

Ermines answered 16/5, 2011 at 21:33 Comment(2)
This is a nasty workaround at best. I'd avoid fiddling with __dict__.Vexillum
Maybe I'm missing something, but e = Example(name="Julie",age="21") then e.name.to_db() says: >>> e.name.to_db() Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'str' object has no attribute 'to_db' " -- presumably because get is being called first.Heeler

© 2022 - 2024 — McMap. All rights reserved.