Class Variable Access and Assignment

Bengt Richter bokr at oz.net
Sat Nov 5 16:26:22 EST 2005


On Fri, 04 Nov 2005 21:14:17 -0500, Mike Meyer <mwm at mired.org> wrote:

>bokr at oz.net (Bengt Richter) writes:
>> On Thu, 03 Nov 2005 13:37:08 -0500, Mike Meyer <mwm at mired.org> wrote:
>> [...]
>>>> I think it even less sane, if the same occurce of b.a refers to two
>>>> different objects, like in b.a += 2
>>>
>>>That's a wart in +=, nothing less. The fix to that is to remove +=
>>>from the language, but it's a bit late for that.
>>>
>> Hm, "the" fix? Why wouldn't e.g. treating augassign as shorthand for a source transformation
>> (i.e., asstgt <op>= expr  becomes by simple text substitution asstgt = asstgt <op> expr)
>> be as good a fix? Then we could discuss what
>>
>>     b.a = b.a + 2
>>
>> should mean ;-)
>
>The problem with += is how it behaves, not how you treat it. But you
>can't treat it as a simple text substitution, because that would imply
>that asstgt gets evaluated twice, which doesn't happen.
I meant that it would _make_ that happen, and no one would wonder ;-)

BTW, if b.a is evaluated once each for __get__ and __set__, does that not
count as getting evaluated twice?

 >>> class shared(object):
 ...     def __init__(self, v=0): self.v=v
 ...     def __get__(self, *any): print '__get__'; return self.v
 ...     def __set__(self, _, v): print '__set__'; self.v = v
 ...
 >>> class B(object):
 ...     a = shared(1)
 ...
 >>> b=B()
 >>> b.a
 __get__
 1
 >>> b.a += 2
 __get__
 __set__
 >>> B.a
 __get__
 3

Same number of get/sets:

 >>> b.a = b.a + 10
 __get__
 __set__
 >>> b.a
 __get__
 13

I posted the disassembly in another part of the thread, but I'll repeat:

 >>> def foo():
 ...     a.b += 2
 ...     a.b = a.b + 2
 ...
 >>> import dis
 >>> dis.dis(foo)
   2           0 LOAD_GLOBAL              0 (a)
               3 DUP_TOP
               4 LOAD_ATTR                1 (b)
               7 LOAD_CONST               1 (2)
              10 INPLACE_ADD
              11 ROT_TWO
              12 STORE_ATTR               1 (b)

   3          15 LOAD_GLOBAL              0 (a)
              18 LOAD_ATTR                1 (b)
              21 LOAD_CONST               1 (2)
              24 BINARY_ADD
              25 LOAD_GLOBAL              0 (a)
              28 STORE_ATTR               1 (b)
              31 LOAD_CONST               0 (None)
              34 RETURN_VALUE

It looks like the thing that's done only once for += is the LOAD_GLOBAL (a)
but DUP_TOP provides the two copies of the reference which are
used either way with LOAD_ATTR followed by STORE_ATTR, which UIAM
lead to the loading of the (descriptor above) attribute twice -- once each
for the __GET__ and __SET__ calls respectively logged either way above.

>
>> OTOH, we could discuss how you can confuse yourself with the results of b.a += 2
>> after defining a class variable "a" as an instance of a class defining __iadd__ ;-)
>
>You may confuse yourself that way, I don't have any problems with it
>per se.
I should have said "one can confuse oneself," sorry ;-)
Anyway, I wondered about the semantics of defining __iadd__, since it seems to work just
like __add__ except for allowing you to know what source got you there. So whatever you
return (unless you otherwise intercept instance attribute binding) will get bound to the
instance, even though you internally mutated the target and return None by default (which
gives me the idea of returning NotImplemented, but (see below) even that gets bound :-(

BTW, semantically does/should not __iadd__ really implement a _statement_ and therefore
have no business returning any expression value to bind anywhere?

 >>> class DoIadd(object):
 ...     def __init__(self, v=0, **kw):
 ...         self.v = v
 ...         self.kw = kw
 ...     def __iadd__(self, other):
 ...         print '__iadd__(%r, %r) => '%(self, other),
 ...         self.v += other
 ...         retv = self.kw.get('retv', self.v)
 ...         print repr(retv)
 ...         return retv
 ...
 >>> class B(object):
 ...     a = DoIadd(1)
 ...
 >>> b=B()
 >>> b.a
 <__main__.DoIadd object at 0x02EF374C>
 >>> b.a.v
 1

The normal(?) mutating way:
 >>> b.a += 2
 __iadd__(<__main__.DoIadd object at 0x02EF374C>, 2) =>  3
 >>> vars(b)
 {'a': 3}
 >>> B.a
 <__main__.DoIadd object at 0x02EF374C>
 >>> B.a.v
 3

Now fake attempt to mutate self without returning anything (=> None)
 >>> B.a = DoIadd(1, retv=None) # naive default
 >>> b.a
 3
Oops, remove instance attr
 >>> del b.a
 >>> b.a
 <__main__.DoIadd object at 0x02EF3D6C>
 >>> b.a.v
 1
Ok, now try it
 >>> b.a +=2
 __iadd__(<__main__.DoIadd object at 0x02EF3D6C>, 2) =>  None
 >>> vars(b)
 {'a': None}
Returned value None still got bound to instance
 >>> B.a.v
 3
Mutation did happen as planned

Now let's try NotImplemented as a return
 >>> B.a = DoIadd(1, retv=NotImplemented) # mutate but probably do __add__ too
 >>> del b.a
 >>> b.a
 <__main__.DoIadd object at 0x02EF374C>
 >>> b.a.v
 1
 >>> b.a +=2
 __iadd__(<__main__.DoIadd object at 0x02EF374C>, 2) =>  NotImplemented
 __iadd__(<__main__.DoIadd object at 0x02EF374C>, 2) =>  NotImplemented
 >>> vars(b)
 {'a': NotImplemented}
 >>> B.a.v
 5

No problem with that? ;-)

I'd say it looks like someone got tired of implementing __iadd__ since
it's too easy to work around the problem. If _returning_ NotImplemented
could have the meaning that return value processing (binding) should not
be effected, then mutation could happen without a second evaluation of
b.a as a target. ISTM a return value for __iadd__ is kind of strange in any case,
since it's a statement implementation, not an expression term implementation.

>
>> Or point out that you can define descriptors (or use property to make it easy)
>> to control what happens, pretty much in as much detail as you can describe requirements ;-)
>
>I've already pointed that out.
Sorry, missed it. Big thread ;-)

Regards,
Bengt Richter



More information about the Python-list mailing list