2

I have a class LabelMapper (a boost::python class), which implements the dictionary protocol. I would like to have a proxy class which will use attributes for accessing that dicionary. I've seen many posts for overriding __setitem__ and __getitem__ but I can't seem to get it right.

The naive approach (below) leads to infinite recursion due to self.mapper invoking LabelMapperProxy.__getattr__, which in turn needs self.mapper and so on.

class LabelMapper(object):
   def __init__(self): self.map={}
   def __getitem__(self,key): return self.map[key]
   def __setitem__(self,key,val): self.map[key]=val
   def __delitem__(self,key): del self.map[key]

class LabelMapperProxy(object):
   def __init__(self,mapper): self.mapper=mapper
   def __getattr__(self,key): return self.mapper[key]
   def __setattr__(self,key,val): self.mapper[key]=val
   def __delattr__(self,key): del self.mapper[key]

lm=LabelMapper()
lm['foo']=123

# construct the proxy
lmp=LabelMapperProxy(mapper=lm)
print lmp.foo                    # !!! recursion
lmp.bar=456
print lmp.bar,lm['bar']

What is the solution? Perhaps is there such a proxy pre-cooked in the standard library?

eudoxos
  • 18,545
  • 10
  • 61
  • 110

2 Answers2

6

You are trying to set a new attribute on your Proxy instance:

class LabelMapperProxy(object):
    def __init__(self, mapper): self.mapper = mapper

This triggers a __setattr__, which tries to access the non-existent self.mapper attribute, so __getattr__ is consulted (which is called for all missing attributes). And __getattr__ tries to access self.mapper....

The solution is to set mapper directly in self.__dict__:

class LabelMapperProxy(object):
    def __init__(self, mapper): self.__dict__['mapper'] = mapper

Alternatively, use the original baseclass __setattr__ just for the mapper attribute:

class LabelMapperProxy(object):
   def __init__(self, mapper): self.mapper = mapper

   def __setattr__(self, key, val):
       if key == 'mapper':
           return super(LabelMapperProxy, self).__setattr__(key, val)
       self.mapper[key] = val
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • Just my 2c: I'd rename `mapper` into something like `__mapper` so that if somebody wants to use `mapper` as an attribute name it will remain consistent. – bereal Apr 17 '13 at 13:28
  • @bereal: sure, but that would not have solved the problem at hand here. :-) – Martijn Pieters Apr 17 '13 at 13:29
  • Great! How come `__getattr__` does not get in loop when `self.mapper` is looked up in `__getattr__`? Is that the `__getattribute__` and `__getattr__` difference? – eudoxos Apr 17 '13 at 14:07
  • 1
    @eudoxos: Exactly; `__getattr__` is only called if the attribute has not been found elsewhere first. – Martijn Pieters Apr 17 '13 at 14:11
  • @bereal: if you use `__mapper` (with two leading underscores), it actually breaks, since python will mange that name when looking it up. – eudoxos Apr 17 '13 at 14:20
  • @eudoxos: There are ways around that, but that does complicate things. – Martijn Pieters Apr 17 '13 at 14:22
  • @eudoxos: Anything else I need to add to my answer? Was it helpful in any way? – Martijn Pieters Apr 17 '13 at 14:25
  • @MartijnPieters: "Since there is no `.mapper` attribute yet, that triggers a `__setattr__`" Should it be different if there was a `.mapper` attribute already? The [docs](https://docs.python.org/2/reference/datamodel.html#object.__setattr__) and my test on Python 3.4 show that even if `object.attribute` exist, `object.__setattr__` will be invoked. "Note that if the attribute is found through the normal mechanism, __getattr__() is not called. (This is an intentional asymmetry between __getattr__() and __setattr__().)" – Jérôme Sep 08 '16 at 08:48
  • @Jérôme: the sentence is indeed a little confusing, sorry. I mention that the attribute is not there jet because `__setattr__` then triggers `__getattr__`; it is the latter step where this matters. I'll see about improving it. – Martijn Pieters Sep 08 '16 at 09:44
1

Here is the trap:

class LabelMapperProxy(object):
   def __init__(self, mapper):
       # This will not assign this object's attribute 
       # since __setattr__ is overriden.
       # Instead, it will do self.mapper['mapper'] = mapper
       self.mapper=mapper

   def __getattr__(self, key): 
       # And this won't find `self.mapper` and resort to __getattr__
       # (which is itself)
       return self.mapper[key]  

   def __setattr__(self, key, val): 
       self.mapper[key]=val

   def __delattr__(self, key): 
       del self.mapper[key]
bereal
  • 32,519
  • 6
  • 58
  • 104