Usually Python descriptor are defined as class attributes. But in my case, I want every object instance to have different set descriptors that depends on the input. For example:

class MyClass(object):
  def __init__(self, **kwargs):
    for attr, val in kwargs.items():
      self.__dict__[attr] = MyDescriptor(val)

Each object are have different set of attributes that are decided at instantiation time. Since these are one-off objects, it is not convenient to first subclass them.

tv = MyClass(type="tv", size="30")
smartphone = MyClass(type="phone", os="android")

tv.size   # do something smart with the descriptor

Assign Descriptor to the object does not seem to work. If I try to access the attribute, I got something like

<property at 0x4067cf0>

Do you know why is this not working? Is there any work around?

有帮助吗?

解决方案

This is not working because you have to assign the descriptor to the class of the object.

class Descriptor:

    def __get__(...):
        # this is called when the value is got

    def __set__(...
    def __del__(...

if you write

obj.attr
=> type(obj).__getattribute__(obj, 'attr') is called
=> obj.__dict__['attr'] is returned if there else:
=> type(obj).__dict__['attr'] is looked up
if this contains a descriptor object then this is used.

so it does not work because the type dictionairy is looked up for descriptors and not the object dictionairy.

there are possible work arounds:

  1. put the descriptor into the class and make it use e.g. obj.xxxattr to store the value. If there is only one descriptor behaviour this works.

  2. overwrite setattr and getattr and delattr to respond to discriptors.

  3. put a discriptor into the class that responds to descriptors stored in the object dictionairy.

其他提示

You are using descriptors in the wrong way.

Descriptors don't make sense on an instance level. After all the __get__/__set__ methods give you access to the instance of the class.

Without knowing what exactly you want to do, I'd suggest you put the per-instance logic inside the __set__ method, by checking who is the "caller/instance" and act accordingly.

Otherwise tell us what you are trying to achieve, so that we can propose alternative solutions.

This looks like a use-case for named tuples

The reason it is not working is because Python only checks for descriptors when looking up attributes on the class, not on the instance; the methods in question are:

It is possible to override those methods on your class in order to implement the descriptor protocol on instances as well as classes:

# do not use in production, example code only, needs more checks
class ClassAllowingInstanceDescriptors(object):
    def __delattr__(self, name):
        res = self.__dict__.get(name)
        for method in ('__get__', '__set__', '__delete__'):
            if hasattr(res, method):
                # we have a descriptor, use it
                res = res.__delete__(name)
                break
        else:
            res = object.__delattr__(self, name)
        return res
    def __getattribute__(self, *args):
        res = object.__getattribute__(self, *args)
        for method in ('__get__', '__set__', '__delete__'):
            if hasattr(res, method):
                # we have a descriptor, call it
                res = res.__get__(self, self.__class__)
        return res
    def __setattr__(self, name, val):
        # check if object already exists
        res = self.__dict__.get(name)
        for method in ('__get__', '__set__', '__delete__'):
            if hasattr(res, method):
                # we have a descriptor, use it
                res = res.__set__(self, val)
                break
        else:
            res = object.__setattr__(self, name, val)
        return res
    @property
    def world(self):
        return 'hello!'

When the above class is used as below:

huh = ClassAllowingInstanceDescriptors()
print(huh.world)
huh.uni = 'BIG'
print(huh.uni)
huh.huh = property(lambda *a: 'really?')
print(huh.huh)
print('*' * 50)
try:
    del huh.world
except Exception, e:
    print(e)
print(huh.world)
print('*' * 50)
try:
    del huh.huh
except Exception, e:
    print(e)
print(huh.huh)

The results are:

hello!

BIG

really?


can't delete attribute

hello!


can't delete attribute

really?

I dynamically create instances by execing a made-up class. This may suit your use case.

def make_myclass(**kwargs):

    class MyDescriptor(object):
        def __init__(self, val):
            self.val = val

        def __get__(self, obj, cls):
            return self.val

        def __set__(self, obj, val):
            self.val = val

    cls = 'class MyClass(object):\n{}'.format('\n'.join('    {0} = MyDescriptor({0})'.format(k) for k in kwargs))

    #check if names in kwargs collide with local names
    for key in kwargs:
        if key in locals():
            raise Exception('name "{}" collides with local name'.format(key))

    kwargs.update(locals())
    exec(cls, kwargs, locals())
    return MyClass()  

Test;

In [577]: tv = make_myclass(type="tv", size="30")

In [578]: tv.type
Out[578]: 'tv'

In [579]: tv.size
Out[579]: '30'

In [580]: tv.__dict__
Out[580]: {}  

But the instances are of different class.

In [581]: phone = make_myclass(type='phone')

In [582]: phone.type
Out[582]: 'phone'

In [583]: tv.type
Out[583]: 'tv'

In [584]: isinstance(tv,type(phone))
Out[584]: False

In [585]: isinstance(phone,type(tv))
Out[585]: False

In [586]: type(tv)
Out[586]: MyClass

In [587]: type(phone)
Out[587]: MyClass

In [588]: type(phone) is type(tv)
Out[588]: False
许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top