Shorthand syntax for get/set/delattr (was Re: Dict-like object with property access)

Rather than throwing out random ideas in a popularity contest, it's important to carefully review what it is that people don't like about the current state of affairs. Currently, when dealing with attributes that are statically determined, your code looks like this: x.attr # Reference x.attr = y # Bind del x.attr # Unbind Now, suppose for some reason we want to determine the attributes *dynamically*. The reason for doing this could be as simple as wanting to avoid code duplication when performing the same operation on multiple attributes (i.e. "for attr in 'attr1 attr2 attr3'.split(): ..."). At this point, dedicated syntactic support disappears and we're now using builtin functions instead: getattr(x, attr) # Reference setattr(x, attr, y) # Bind delattr(x, attr) # Unbind hasattr(x, attr) # Existence query (essentially a shorthand for getattr() in a try/except block) So, that's the status quo any proposals are competing against. It's easy enough to write, easy to read and easy to look up if you don't already know what it does (an often underestimated advantage of builtin operations over syntax is that the former are generally *much* easier to look up in the documentation). However, it can start to look rather clumsy when multiple dynamic attribute operations are chained together. Compare this static code: x.attr1 = y.attr1 x.attr2 = y.attr2 x.attr3 = y.attr3 With the following dynamic code: for attr in "attr1 attr2 attr3".split(): setattr(x, attr, getattr(y, attr)) The inner assignment in that loop is *very* noisy for a simple assignment. Splitting out a temporary variable cleans things up a bit, but it's still fairly untidy: for attr in "attr1 attr2 attr3".split(): val = getattr(y, attr) setattr(x, attr, val) It would be a *lot* cleaner if we could just use a normal assignment statement instead of builtin functions to perform the name binding. As it turns out, for ordinary instances, we can already do exactly that: for attr in "attr1 attr2 attr3".split(): vars(x)[attr] = vars(y)[attr] In short, I think proposals for dedicated syntax for dynamic attribute access are misguided - instead, such efforts should go into enhancing vars() to return objects that support *full* dict-style access to the underlying object's attribute namespace (with descriptor protocol support and all). Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On Sun, Feb 5, 2012 at 10:20 AM, Nick Coghlan <ncoghlan@gmail.com> wrote:
That can obviously also be written: xa, ya = vars(x), vars(y) for attr in "attr1 attr2 attr3".split(): va[attr] = ya[attr] In other words, don't think about new syntax. Think about how to correctly implement a full object proxy that provides the MutableMapping interface, with get/set/delitem on the proxy corresponding with get/set/delattr on the underlying object. Then think about whether or not returning such an object from vars() would be backwards compatible, or whether a new API would be needed to create one (e.g. attrview(x)). Finally, such an object can be prototyped quite happily outside the standard library, so consider writing it and publishing it on PyPI as a standalone module. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On 05Feb2012 10:20, Nick Coghlan <ncoghlan@gmail.com> wrote: [...] | In short, I think proposals for dedicated syntax for dynamic attribute | access are misguided - instead, such efforts should go into enhancing | vars() to return objects that support *full* dict-style access to the | underlying object's attribute namespace (with descriptor protocol | support and all). +10 _Where_ do you people find the time to write these well thought out posts? I'm very much for making vars() better supported (the docs have caveats about assigning to it). All the syntax suggestions I've seen look cumbersome or ugly and some are actively misleading to my eye (did I really see an "<-" in there?) I see my random sig quote picker has worked well again:-) Cheers, -- Cameron Simpson <cs@zip.com.au> DoD#743 http://www.cskk.ezoshosting.com/cs/ A strong conviction that something must be done is the parent of many bad measures. - Daniel Webster

On Sat, Feb 4, 2012 at 7:20 PM, Nick Coghlan <ncoghlan@gmail.com> wrote:
I love the idea, and I think such a solution is much more straight forward than any syntax change. While it would be great to extend the functionality of vars(), it would be easier to add a new builtin that returns some kind of proxy. If vars() was changed to return this proxy, it could potentially break a lot of existing code mutating the dict returned by vars. The question is: Is yet another builtin or the messy compatibility change the worse option? -- Read my blog! I depend on your acceptance of my opinion! I am interesting! http://techblog.ironfroggy.com/ Follow me if you're into that sort of thing: http://www.twitter.com/ironfroggy

On Sun, Feb 5, 2012 at 1:00 PM, Calvin Spealman <ironfroggy@gmail.com> wrote:
The question is: Is yet another builtin or the messy compatibility change the worse option?
That's actually a question for (much) further down the road. The *current* question is whether anyone is interested enough in the concept to prototype it as a PyPI module. That's a lot more work than just posting suggestions here :) Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On Feb 5, 2012, at 2:58 AM, yoav glazner wrote:
It's not that hard to make something that basically works with properties:
In a real module, you'd probably want to be more thorough about emulating a __dict__ dictionary though by adding item() and keys() etc.

2012/2/5 Carl M. Johnson <cmjohnson.mailinglist@gmail.com>:
It's not that hard to make something that basically works with properties:
Indeed...
In a real module, you'd probably want to be more thorough about emulating a __dict__ dictionary though by adding item() and keys() etc.
... and precisely! The discussions so far have concentrated on the "easy" side of things. Writing a working module would ensure that all the corner cases get covered. And as a benefit, would provide an implementation that could be taken straight into the core/stdlib, hugely reducing the core developer effort that is otherwise needed to take even the best thought out proposal into reality. Paul.

05.02.12 15:25, Carl M. Johnson написав(ла):
del v['pop'] AttributeError: P instance has no attribute 'pop'
In a real module, you'd probably want to be more thorough about emulating a __dict__ dictionary though by adding item() and keys() etc.
It's impossible in general. class A: def __getattr__(self, name): return len(name)

Le 05/02/2012 16:33, yoav glazner a écrit :
Hi, +1 on extending vars(). I like this idea much more than adding syntax. In this case, proxy(a).key() would be based on dir(a) (or something similar) and have the same (documented) limitations. I think this is acceptable, and the proxy object is still useful. Regards, -- Simon Sapin

On 5 February 2012 20:45, Simon Sapin <simon.sapin@kozea.fr> wrote:
vars() and dir() do very different things:
In my view, following the spec of vars() is far more useful and matches better the original requirement, which was to simulate javascript's index/attribute duality. Methods (and even more so special methods) don't really fit in here. I'd argue for the definition: proxy(obj)['a'] <=> obj.a proxy(obj)['a' = val] <=> obj.a = val del proxy(obj)['a'] <=> del obj.a 'a' in proxy(obj) <=> hasattr(obj, 'a') proxy(obj).keys() <=> vars(obj).keys() len(proxy(obj)) <=> len(vars(obj)) In other words, indexing defers to getattr/setattr/delattr, containment uses hasattr, but anything else goes via vars. In terms of ABCs, Sized/Iterable behaviour comes from vars(), Container/Mapping/MutableMapping behaviour comes from {has,get,set,del}attr. It's mildly inconsistent for objects which implement their own attribute access, but those aren't the key use case, and the behaviour is well defined even for those. Paul

Le 05/02/2012 22:18, Paul Moore a écrit :
I’m fine with that too and I agree it is probably better. My point was that not all keys that can be used in proxy(a)[key] without KeyError will be in proxy(a).keys(), but that’s okay because the same already happens with getattr() and dir() By the way, the proxy should also turn AttributeError into KeyError, for consistency with other Mapping types. Regards, -- Simon Sapin

On 5 February 2012 21:30, Simon Sapin <simon.sapin@kozea.fr> wrote:
By the way, the proxy should also turn AttributeError into KeyError, for consistency with other Mapping types.
Clearly. And arguably, this is a good case for the new "raise KeyError from None" form to suppress exception chaining... Paul.

On 02/05/2012 04:16 PM, Paul Moore wrote:
class attrs(object): __slots__ = 'obj', def __init__(self,obj): self.obj = obj def __getitem__(self, key): try: return getattr(self.obj, key) except AttributeError: raise KeyError(key) def __setitem__(self, key, value): try: setattr(self.obj, key, value) except AttributeError: raise KeyError(key) def __delitem__(self, key): try: delattr(self.obj, key) except AttributeError: raise KeyError(key) def __contains__(self, key): return hasattr(self.obj, key) def get(self, key, default=None): try: return getattr(self.obj, key, default) except AttributeError: raise KeyError(key) def keys(self): return iter(dir(self.obj)) def values(self): for key in dir(self.obj): yield getattr(self.obj, key) def items(self): for key in dir(self.obj): yield key, getattr(self.obj, key) def __len__(self): return len(dir(self.obj)) def __iter__(self): return iter(dir(self.obj)) def __repr__(self): return repr(dict(self))

On Mon, Feb 6, 2012 at 11:07 AM, Mathias Panzenböck <grosser.meister.morti@gmx.net> wrote: This is a good start, but still has a few issues.
This will never raise KeyError. It needs to use a dedicated sentinel object so it can tell the difference between "default=None" and "default not supplied" and invoke getattr() accordingly.
These 3 methods should return views with the appropriate APIs rather than iterators.
def __repr__(self): return repr(dict(self))
The appropriate output for str() and repr() is definitely open for question. Interaction with serialisation APIs such as pickle and json will also need investigation. These kinds of question are why I think it is well-worth exploring this concept on PyPI. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On Sun, Feb 5, 2012 at 10:20 AM, Nick Coghlan <ncoghlan@gmail.com> wrote:
That can obviously also be written: xa, ya = vars(x), vars(y) for attr in "attr1 attr2 attr3".split(): va[attr] = ya[attr] In other words, don't think about new syntax. Think about how to correctly implement a full object proxy that provides the MutableMapping interface, with get/set/delitem on the proxy corresponding with get/set/delattr on the underlying object. Then think about whether or not returning such an object from vars() would be backwards compatible, or whether a new API would be needed to create one (e.g. attrview(x)). Finally, such an object can be prototyped quite happily outside the standard library, so consider writing it and publishing it on PyPI as a standalone module. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On 05Feb2012 10:20, Nick Coghlan <ncoghlan@gmail.com> wrote: [...] | In short, I think proposals for dedicated syntax for dynamic attribute | access are misguided - instead, such efforts should go into enhancing | vars() to return objects that support *full* dict-style access to the | underlying object's attribute namespace (with descriptor protocol | support and all). +10 _Where_ do you people find the time to write these well thought out posts? I'm very much for making vars() better supported (the docs have caveats about assigning to it). All the syntax suggestions I've seen look cumbersome or ugly and some are actively misleading to my eye (did I really see an "<-" in there?) I see my random sig quote picker has worked well again:-) Cheers, -- Cameron Simpson <cs@zip.com.au> DoD#743 http://www.cskk.ezoshosting.com/cs/ A strong conviction that something must be done is the parent of many bad measures. - Daniel Webster

On Sat, Feb 4, 2012 at 7:20 PM, Nick Coghlan <ncoghlan@gmail.com> wrote:
I love the idea, and I think such a solution is much more straight forward than any syntax change. While it would be great to extend the functionality of vars(), it would be easier to add a new builtin that returns some kind of proxy. If vars() was changed to return this proxy, it could potentially break a lot of existing code mutating the dict returned by vars. The question is: Is yet another builtin or the messy compatibility change the worse option? -- Read my blog! I depend on your acceptance of my opinion! I am interesting! http://techblog.ironfroggy.com/ Follow me if you're into that sort of thing: http://www.twitter.com/ironfroggy

On Sun, Feb 5, 2012 at 1:00 PM, Calvin Spealman <ironfroggy@gmail.com> wrote:
The question is: Is yet another builtin or the messy compatibility change the worse option?
That's actually a question for (much) further down the road. The *current* question is whether anyone is interested enough in the concept to prototype it as a PyPI module. That's a lot more work than just posting suggestions here :) Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On Feb 5, 2012, at 2:58 AM, yoav glazner wrote:
It's not that hard to make something that basically works with properties:
In a real module, you'd probably want to be more thorough about emulating a __dict__ dictionary though by adding item() and keys() etc.

2012/2/5 Carl M. Johnson <cmjohnson.mailinglist@gmail.com>:
It's not that hard to make something that basically works with properties:
Indeed...
In a real module, you'd probably want to be more thorough about emulating a __dict__ dictionary though by adding item() and keys() etc.
... and precisely! The discussions so far have concentrated on the "easy" side of things. Writing a working module would ensure that all the corner cases get covered. And as a benefit, would provide an implementation that could be taken straight into the core/stdlib, hugely reducing the core developer effort that is otherwise needed to take even the best thought out proposal into reality. Paul.

05.02.12 15:25, Carl M. Johnson написав(ла):
del v['pop'] AttributeError: P instance has no attribute 'pop'
In a real module, you'd probably want to be more thorough about emulating a __dict__ dictionary though by adding item() and keys() etc.
It's impossible in general. class A: def __getattr__(self, name): return len(name)

Le 05/02/2012 16:33, yoav glazner a écrit :
Hi, +1 on extending vars(). I like this idea much more than adding syntax. In this case, proxy(a).key() would be based on dir(a) (or something similar) and have the same (documented) limitations. I think this is acceptable, and the proxy object is still useful. Regards, -- Simon Sapin

On 5 February 2012 20:45, Simon Sapin <simon.sapin@kozea.fr> wrote:
vars() and dir() do very different things:
In my view, following the spec of vars() is far more useful and matches better the original requirement, which was to simulate javascript's index/attribute duality. Methods (and even more so special methods) don't really fit in here. I'd argue for the definition: proxy(obj)['a'] <=> obj.a proxy(obj)['a' = val] <=> obj.a = val del proxy(obj)['a'] <=> del obj.a 'a' in proxy(obj) <=> hasattr(obj, 'a') proxy(obj).keys() <=> vars(obj).keys() len(proxy(obj)) <=> len(vars(obj)) In other words, indexing defers to getattr/setattr/delattr, containment uses hasattr, but anything else goes via vars. In terms of ABCs, Sized/Iterable behaviour comes from vars(), Container/Mapping/MutableMapping behaviour comes from {has,get,set,del}attr. It's mildly inconsistent for objects which implement their own attribute access, but those aren't the key use case, and the behaviour is well defined even for those. Paul

Le 05/02/2012 22:18, Paul Moore a écrit :
I’m fine with that too and I agree it is probably better. My point was that not all keys that can be used in proxy(a)[key] without KeyError will be in proxy(a).keys(), but that’s okay because the same already happens with getattr() and dir() By the way, the proxy should also turn AttributeError into KeyError, for consistency with other Mapping types. Regards, -- Simon Sapin

On 5 February 2012 21:30, Simon Sapin <simon.sapin@kozea.fr> wrote:
By the way, the proxy should also turn AttributeError into KeyError, for consistency with other Mapping types.
Clearly. And arguably, this is a good case for the new "raise KeyError from None" form to suppress exception chaining... Paul.

On 02/05/2012 04:16 PM, Paul Moore wrote:
class attrs(object): __slots__ = 'obj', def __init__(self,obj): self.obj = obj def __getitem__(self, key): try: return getattr(self.obj, key) except AttributeError: raise KeyError(key) def __setitem__(self, key, value): try: setattr(self.obj, key, value) except AttributeError: raise KeyError(key) def __delitem__(self, key): try: delattr(self.obj, key) except AttributeError: raise KeyError(key) def __contains__(self, key): return hasattr(self.obj, key) def get(self, key, default=None): try: return getattr(self.obj, key, default) except AttributeError: raise KeyError(key) def keys(self): return iter(dir(self.obj)) def values(self): for key in dir(self.obj): yield getattr(self.obj, key) def items(self): for key in dir(self.obj): yield key, getattr(self.obj, key) def __len__(self): return len(dir(self.obj)) def __iter__(self): return iter(dir(self.obj)) def __repr__(self): return repr(dict(self))

On Mon, Feb 6, 2012 at 11:07 AM, Mathias Panzenböck <grosser.meister.morti@gmx.net> wrote: This is a good start, but still has a few issues.
This will never raise KeyError. It needs to use a dedicated sentinel object so it can tell the difference between "default=None" and "default not supplied" and invoke getattr() accordingly.
These 3 methods should return views with the appropriate APIs rather than iterators.
def __repr__(self): return repr(dict(self))
The appropriate output for str() and repr() is definitely open for question. Interaction with serialisation APIs such as pickle and json will also need investigation. These kinds of question are why I think it is well-worth exploring this concept on PyPI. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia
participants (11)
-
Calvin Spealman
-
Cameron Simpson
-
Carl M. Johnson
-
Eric Snow
-
Mathias Panzenböck
-
Nick Coghlan
-
Paul Moore
-
Serhiy Storchaka
-
Simon Sapin
-
Terry Reedy
-
yoav glazner