slacy
slacy

Reputation: 11773

Python override __getattr__ for nested assignment, but not for referencing?

Here's the behavior I'm looking for:

>>> o = SomeClass()
>>> # Works: 
>>> o.foo.bar = 'bar' 
>>> print o.foo.bar
'bar'
>>> # The in-between object would be of type SomeClass as well:
>>> print o.foo 
>>> <__main__.SomeClass object at 0x7fea2f0ef810>

>>> # I want referencing an unassigned attribute to fail: 
>>> print o.baz
Traceback (most recent call last):
  File "<stdin>", line 5, in <module>
    print o.baz
AttributeError: 'SomeClass' object has no attribute 'baz'

In other words, I want to override __getattr__ and __setattr__ (and possibly __getattribute__) in such a way that work similarly to defaultdict, allowing assignment to arbitrary attributes, but if an attribute is just referenced but not assigned to, that it throws an AttributeError as it normally would.

Is this possible?

Upvotes: 0

Views: 1582

Answers (6)

slacy
slacy

Reputation: 11773

Here's what I've got so far:

def raise_wrapper(wrapped_method=None):
    def method(tmp_instance, *args, **kawrgs):
        raise AttributeError("'%s' object has no attribute '%s'" % (
                type(tmp_instance._parent).__name__, tmp_instance._key))
    if wrapped_method:
        method.__doc__ = wrapped_method.__doc__
    return method


class TemporaryValue(object):
    def __init__(self, parent, key):
        self._parent = parent
        self._key = key

    def __setattr__(self, key, value):
        if key in ('_parent', '_key'):
            return object.__setattr__(self, key, value)

        newval = ObjectLike()
        object.__setattr__(self._parent, self._key, newval)
        return object.__setattr__(newval, key, value)

    __eq__ = raise_wrapper(object.__eq__)
    # __del__ = raise_wrapper()
    # __repr__ = raise_wrapper(object.__repr__)
    __str__ = raise_wrapper(object.__str__)
    __lt__ = raise_wrapper(object.__lt__)
    __le__ = raise_wrapper(object.__le__)
    __eq__ = raise_wrapper(object.__eq__)
    __ne__ = raise_wrapper(object.__ne__)
    __cmp__ = raise_wrapper()
    __hash__ = raise_wrapper(object.__hash__)
    __nonzero__ = raise_wrapper()
    __unicode__ = raise_wrapper()
    __delattr__ = raise_wrapper(object.__delattr__)
    __call__ = raise_wrapper(object.__call__)


class ObjectLike(object):
    def __init__(self):
        pass

    def __getattr__(self, key):
        newtmp = TemporaryValue(self, key)
        object.__setattr__(self, key, newtmp)
        return newtmp

    def __str__(self):
        return str(self.__dict__)


o = ObjectLike()
o.foo.bar = 'baz'
print o.foo.bar
print o.not_set_yet
print o.some_function()
if o.unset > 3: 
    print "yes" 
else:
    print "no" 

Upvotes: 0

unutbu
unutbu

Reputation: 880089

How about:

class AutoVivifier(object):
    def __getattr__(self, key):
        value = type(self)()
        object.__setattr__(self,key,value)
        return value

o=AutoVivifier()
o.foo.bar='baz'
print(o.foo.bar)
# baz
print(o.foo.baz)
# <__main__.AutoVivifier object at 0xb776bb0c>
o.foo.baz='bing'
print(o.foo.baz)
# bing

This doesn't raise any AttributeErrors, but it is easy to tell when an attribute chain has no previously assigned value -- the expression will be an instance of Autovivifier. That is, isinstance(o.foo.baz,AutoVivifier) is True.

I think the implementation is cleaner this way, than if you defined all sorts of special methods like __str__ and __eq__ to raise AttributeErrors.

I'm still not clear on why you need to raise AttributeErrors in the first place, but perhaps using AutoVivifier you can write functions or methods that achieve your goals, with isinstance(...,AutoVivifier) tests replacing try...except AttributeError blocks.

Upvotes: 2

Glenn Maynard
Glenn Maynard

Reputation: 57514

This is impossible in Python.

What you're asking is for this:

>>> o = SomeClass()
>>> o.foo.bar = 'bar' 
>>> print o.foo.bar
'bar'
>>> a = o.baz
raises AttributeError

This can't be done. There's no way to distinguish

>>> o.foo.bar = 'bar' 

from

>>> temp = o.foo
>>> temp.bar = 'bar' 

They're logically equivalent, and under the hood Python is doing the same thing in both cases. You can't differentiate them in order to raise an exception in the latter case but not the former.

Upvotes: 6

carl
carl

Reputation: 50554

This is really hacky, but perhaps a start at what you want:

class SomeClass(object):
    def __init__(self):
        object.__setattr__(self, "_SomeClass__children", {})
        object.__setattr__(self, "_SomeClass__empty", True)

    def __getattr__(self, k):
        if k not in self.__children:
            self.__children[k] = SomeClass()
        return self.__children[k]

    def __setattr__(self, k, v):
        object.__setattr__(self, "_SomeClass__empty", False)
        object.__setattr__(self, k, v)

    def __str__(self):
        if not self.__hasvalue():
            raise AttributeError("Never truly existed")
        return object.__str__(self)

    def __hasvalue(self):
        if not self.__empty:
            return True
        return any(v.__hasvalue() for v in self.__children.itervalues())

o = SomeClass()
o.foo.bar = 'bar'
print o.foo.bar
print o.foo
print o.baz

And output:

bar
<__main__.SomeClass object at 0x7f2431404c90>
Traceback (most recent call last):
  File "spam.py", line 29, in <module>
    print o.baz
  File "spam.py", line 17, in __str__
    raise AttributeError("Never truly existed")
AttributeError: Never truly existed

Upvotes: 0

Falmarri
Falmarri

Reputation: 48577

[~/.src/pyusb-1.0.0-a1]
|4>class SomeClass: pass
   ...: 

[~/.src/pyusb-1.0.0-a1]
|5>o = SomeClass()

[~/.src/pyusb-1.0.0-a1]
|6>o.foo='bar'

[~/.src/pyusb-1.0.0-a1]
|7>print o.foo
bar

[~/.src/pyusb-1.0.0-a1]
|8>print o.baz
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)

AttributeError: SomeClass instance has no attribute 'baz'

[~/.src/pyusb-1.0.0-a1]
|9>

Upvotes: 0

Santa
Santa

Reputation: 11545

I'm not sure what you mean. The language features already let you do that:

>>> class MyClass(object):
...     pass
...
>>> f = MyClass()
>>> f.foo = 5
>>> print f.foo
5
>>> f.baz
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'MyClass' object has no attribute 'baz'
>>>

Upvotes: 2

Related Questions