Re: [Python-ideas] Augmented assignment syntax for objects.

2017-04-24 Thread Nathaniel Smith
On Mon, Apr 24, 2017 at 6:08 PM, Erik  wrote:
> Hi. I suspect that this may have been discussed to death at some point in
> the past, but I've done some searching and I didn't come up with much.
> Apologies if I'm rehashing an old argument ;)
>
> I often find myself writing __init__ methods of the form:
>
> def __init__(self, foo, bar, baz, spam, ham):
>   self.foo = foo
>   self.bar = bar
>   self.baz = baz
>   self.spam = spam
>   self.ham = ham

This isn't a direct response, but you might be interested in the attrs library:
https://attrs.readthedocs.io/en/stable/examples.html

-n

-- 
Nathaniel J. Smith -- https://vorpus.org
___
Python-ideas mailing list
Python-ideas@python.org
https://mail.python.org/mailman/listinfo/python-ideas
Code of Conduct: http://python.org/psf/codeofconduct/


Re: [Python-ideas] Augmented assignment syntax for objects.

2017-04-24 Thread Steven D'Aprano
On Tue, Apr 25, 2017 at 02:08:05AM +0100, Erik wrote:

> I often find myself writing __init__ methods of the form:
> 
> def __init__(self, foo, bar, baz, spam, ham):
>   self.foo = foo
>   self.bar = bar
>   self.baz = baz
>   self.spam = spam
>   self.ham = ham
> 
> This seems a little wordy and uses a lot of vertical space on the 
> screen. 

It does, and while both are annoying, in the grand scheme of things 
they're a very minor annoyance. After all, this is typically only an 
issue once per class, and not even every class, and vertical space is 
quite cheap. In general, the barrier for accepting new syntax is quite 
high, and "minor annoyance" generally doesn't reach it.

I'm completely with you about the annoyance factor here. It is 
especially annoying during the early stages of development when the 
__init__ method is evolving rapidly (parameters are being renamed, added 
or removed). One work-around is this pattern:

def __init__(self, spam, ham, eggs, cheese, aardvark):
vars(self).update(locals())
del self.self
...


which is cute but I've never quite been brave enough to use it in 
production. But I don't think the annoyance factor is high enough, or 
common enough, to justify syntactic sugar to "fix" it.



> Occasionally, I have considered something like:
> 
> def __init__(self, foo, bar, baz, spam, ham):
>   self.foo, self.bar, self.baz, self.spam, self.ham = \
>  foo, bar, baz, spam, ham
> 
> ... just to make it a bit more compact
[...]
> When I do that I'm torn because I know it has a runtime impact to create 
> and unpack the implicit tuples and I'm also introducing a style 
> asymmetry in my code just because of the number of parameters a method 
> happens to have.

I'm not too worried about the performance impact unless I've 
profiled my code and it is actually significant.

As far as the style asymmetry, if we use your proposed syntax, that's 
introducing style asymmetry too.



> So why not have an augmented assignment operator for object attributes? 

Wrong question. Don't ask "why not add this feature?". The question to 
ask is "why add this feature?" Every new feature has cost, whether it is 
run-time cost, development cost, feature bloat, learning curve cost, 
etc, so the feature must be justified as adding sufficiently more 
value than it costs.

Here are a few reasons why this feature fails to meet the barrier for 
new syntax.

- This feature doesn't add any new capability to the language. There's 
nothing that this feature allows you to do which you couldn't do before.

- It has quite narrow applicability. It's really only useful inside a 
small proportion of __init__ methods.

- In my experience, it is very rare to have a whole set of unadorned 
assignments in the way you show it. MUCH more common is to have some 
sort of pre-processing of the argument before assignment, or for the 
assignment target to be different from the parameter name:


def __init__(self, spam, eggs, cheese, aardvark, ...):
# type checks
if not isinstance(spam, Foo):
raise TypeError
self._spam = spam  # not all parameters are public attributes
# value checks
if eggs < 0:
raise ValueError
self.eggs = eggs
# replacement of None
if cheese is None:
cheese = make_cheese()
self.cheese = cheese
# other pre-processing
self.mammals = [aardvark, get_weasel()]


which reduces the applicability of this syntax even further.

- Having big parameter lists is something of a mild code smell. As the 
parameter list gets bigger, the smell gets worse. Having so many 
parameters that this feature becomes attractive should be painful, 
because its a warning that maybe your class has too many parameters.



> It addresses one of the same broad issues that the other augmented 
> assignment operators were introduced for (that of repeatedly spelling 
> names).
> 
> The suggestion therefore is:
> 
> def __init__(self, foo, bar, baz, spam, ham):
>   self .= foo, bar, baz, spam, ham

But there's a major difference between this and the other augmented 
assignment operators, so major that the analogy between them doesn't 
hold up.

In augmented assignment, the target can be any valid target, and the RHS 
can be any expression

mylist[0].attr['key'] += (some_value() or another) + 99

Your suggestion is very different: there are *many* targets, and the RHS 
must be a comma-separated list of identifiers. They're quite different 
kinds of assignment. And unlike augmented assignment, which calls one of 
the __iop__ methods, this would have to be handled purely as syntax.

> This is purely syntactic sugar for the original example:
> 
> def __init__(self, foo, bar, baz, spam, ham):
>   self.foo = foo
>   self.bar = bar
>   self.baz = baz
>   self.spam = spam
>   self.ham = ham
> 
> ... so if any of the attributes have setters, then they are called as 
> usual. It's purely a 

Re: [Python-ideas] Augmented assignment syntax for objects.

2017-04-24 Thread Chris Angelico
On Tue, Apr 25, 2017 at 11:08 AM, Erik  wrote:
> The suggestion therefore is:
>
> def __init__(self, foo, bar, baz, spam, ham):
>   self .= foo, bar, baz, spam, ham
>
> This is purely syntactic sugar for the original example:
>
> def __init__(self, foo, bar, baz, spam, ham):
>   self.foo = foo
>   self.bar = bar
>   self.baz = baz
>   self.spam = spam
>   self.ham = ham
>
> ... so if any of the attributes have setters, then they are called as usual.
> It's purely a syntactic shorthand. Any token which is not suitable on the
> RHS of the dot in a standard "obj.attr =" assignment is a syntax error (no
> "self .= 1").

Bikeshedding: Your example looks a lot more like tuple assignment than
multiple assignment. I'd rather it link more with the way that
multiple assignment works:

# simple multiple assignment
a = b = c = d = e = 0
# object member assignment
self .= foo .= bar .= baz .= spam .= ham

The trouble is that this syntax is really only going to be used inside
__init__. It's hard to justify new syntax for one purpose like this.
So I'm swaying between -0 and +0.5 on this.

ChrisA
___
Python-ideas mailing list
Python-ideas@python.org
https://mail.python.org/mailman/listinfo/python-ideas
Code of Conduct: http://python.org/psf/codeofconduct/


[Python-ideas] Augmented assignment syntax for objects.

2017-04-24 Thread Erik
Hi. I suspect that this may have been discussed to death at some point 
in the past, but I've done some searching and I didn't come up with 
much. Apologies if I'm rehashing an old argument ;)


I often find myself writing __init__ methods of the form:

def __init__(self, foo, bar, baz, spam, ham):
  self.foo = foo
  self.bar = bar
  self.baz = baz
  self.spam = spam
  self.ham = ham

This seems a little wordy and uses a lot of vertical space on the 
screen. Occasionally, I have considered something like:


def __init__(self, foo, bar, baz, spam, ham):
  self.foo, self.bar, self.baz, self.spam, self.ham = \
 foo, bar, baz, spam, ham

... just to make it a bit more compact - though in practice, I'd 
probably not do that with a list quite that long ... two or three items 
at most:


def __init__(self, foo, bar, baz):
   self.foo, self.bar, self.baz = foo, bar, baz

When I do that I'm torn because I know it has a runtime impact to create 
and unpack the implicit tuples and I'm also introducing a style 
asymmetry in my code just because of the number of parameters a method 
happens to have.


So why not have an augmented assignment operator for object attributes? 
It addresses one of the same broad issues that the other augmented 
assignment operators were introduced for (that of repeatedly spelling 
names).


The suggestion therefore is:

def __init__(self, foo, bar, baz, spam, ham):
  self .= foo, bar, baz, spam, ham

This is purely syntactic sugar for the original example:

def __init__(self, foo, bar, baz, spam, ham):
  self.foo = foo
  self.bar = bar
  self.baz = baz
  self.spam = spam
  self.ham = ham

... so if any of the attributes have setters, then they are called as 
usual. It's purely a syntactic shorthand. Any token which is not 
suitable on the RHS of the dot in a standard "obj.attr =" assignment is 
a syntax error (no "self .= 1").


The comma-separators in the example are not creating a tuple object, 
they would work at the same level in the parser as the import 
statement's comma-separated lists - in the same way that "from pkg 
import a, b, c" is the same as saying:


import pkg
a = pkg.a
b = pkg.b
c = pkg.c

... "self .= a, b, c" is the same as writing:

self.a = a
self.b = b
self.c = c

E.
___
Python-ideas mailing list
Python-ideas@python.org
https://mail.python.org/mailman/listinfo/python-ideas
Code of Conduct: http://python.org/psf/codeofconduct/


Re: [Python-ideas] Binary arithmetic does not always call subclasses first

2017-04-24 Thread Guido van Rossum
If this is portant I should probably ponder it.

On Apr 24, 2017 4:47 PM, "Stephan Hoyer"  wrote:

> +Georg Brandl, in case he remembers where "Move the 3k reST doc tree in
> place." moved things from:
> https://github.com/python/cpython/commit/116aa62bf54a39697e25f21d6cf679
> 9f7faa1349
>
> On Mon, Apr 24, 2017 at 4:29 PM, Nick Timkovich 
> wrote:
>
>> GitHub shows that that note hasn't changed in 10 years:
>> https://github.com/python/cpython/blame/master/Doc/
>> reference/datamodel.rst#L2210
>>
>> On Mon, Apr 24, 2017 at 3:15 PM, Terry Reedy  wrote:
>>
>>> On 4/24/2017 12:14 PM, Stephan Hoyer wrote:
>>>
>>> Based on the change in the documentation between 2.x and 3.x, I wonder
 if this is something that someone intended to clean up as part of Python
 3000 but never got around to. I would love to hear from anyone familiar
 with the historical context here.

>>>
>>> We should ask the intention of the person who made the change, which is
>>> in the repository.  If you email me the doc (the last part of the url) and
>>> location within, I will look it up.
>>>
>>> --
>>> Terry Jan Reedy
>>>
>>>
>>> ___
>>> Python-ideas mailing list
>>> Python-ideas@python.org
>>> https://mail.python.org/mailman/listinfo/python-ideas
>>> Code of Conduct: http://python.org/psf/codeofconduct/
>>>
>>
>>
>> ___
>> Python-ideas mailing list
>> Python-ideas@python.org
>> https://mail.python.org/mailman/listinfo/python-ideas
>> Code of Conduct: http://python.org/psf/codeofconduct/
>>
>>
>
> ___
> Python-ideas mailing list
> Python-ideas@python.org
> https://mail.python.org/mailman/listinfo/python-ideas
> Code of Conduct: http://python.org/psf/codeofconduct/
>
>
___
Python-ideas mailing list
Python-ideas@python.org
https://mail.python.org/mailman/listinfo/python-ideas
Code of Conduct: http://python.org/psf/codeofconduct/


Re: [Python-ideas] Binary arithmetic does not always call subclasses first

2017-04-24 Thread Stephan Hoyer
+Georg Brandl, in case he remembers where "Move the 3k reST doc tree in
place." moved things from:
https://github.com/python/cpython/commit/116aa62bf54a39697e25f21d6cf6799f7faa1349

On Mon, Apr 24, 2017 at 4:29 PM, Nick Timkovich 
wrote:

> GitHub shows that that note hasn't changed in 10 years:
> https://github.com/python/cpython/blame/master/
> Doc/reference/datamodel.rst#L2210
>
> On Mon, Apr 24, 2017 at 3:15 PM, Terry Reedy  wrote:
>
>> On 4/24/2017 12:14 PM, Stephan Hoyer wrote:
>>
>> Based on the change in the documentation between 2.x and 3.x, I wonder if
>>> this is something that someone intended to clean up as part of Python 3000
>>> but never got around to. I would love to hear from anyone familiar with the
>>> historical context here.
>>>
>>
>> We should ask the intention of the person who made the change, which is
>> in the repository.  If you email me the doc (the last part of the url) and
>> location within, I will look it up.
>>
>> --
>> Terry Jan Reedy
>>
>>
>> ___
>> Python-ideas mailing list
>> Python-ideas@python.org
>> https://mail.python.org/mailman/listinfo/python-ideas
>> Code of Conduct: http://python.org/psf/codeofconduct/
>>
>
>
> ___
> Python-ideas mailing list
> Python-ideas@python.org
> https://mail.python.org/mailman/listinfo/python-ideas
> Code of Conduct: http://python.org/psf/codeofconduct/
>
>
___
Python-ideas mailing list
Python-ideas@python.org
https://mail.python.org/mailman/listinfo/python-ideas
Code of Conduct: http://python.org/psf/codeofconduct/


Re: [Python-ideas] Binary arithmetic does not always call subclasses first

2017-04-24 Thread Nick Timkovich
GitHub shows that that note hasn't changed in 10 years:
https://github.com/python/cpython/blame/master/Doc/reference/datamodel.rst#L2210

On Mon, Apr 24, 2017 at 3:15 PM, Terry Reedy  wrote:

> On 4/24/2017 12:14 PM, Stephan Hoyer wrote:
>
> Based on the change in the documentation between 2.x and 3.x, I wonder if
>> this is something that someone intended to clean up as part of Python 3000
>> but never got around to. I would love to hear from anyone familiar with the
>> historical context here.
>>
>
> We should ask the intention of the person who made the change, which is in
> the repository.  If you email me the doc (the last part of the url) and
> location within, I will look it up.
>
> --
> Terry Jan Reedy
>
>
> ___
> Python-ideas mailing list
> Python-ideas@python.org
> https://mail.python.org/mailman/listinfo/python-ideas
> Code of Conduct: http://python.org/psf/codeofconduct/
>
___
Python-ideas mailing list
Python-ideas@python.org
https://mail.python.org/mailman/listinfo/python-ideas
Code of Conduct: http://python.org/psf/codeofconduct/


Re: [Python-ideas] Binary arithmetic does not always call subclasses first

2017-04-24 Thread Terry Reedy

On 4/24/2017 12:14 PM, Stephan Hoyer wrote:

Based on the change in the documentation between 2.x and 3.x, I wonder 
if this is something that someone intended to clean up as part of Python 
3000 but never got around to. I would love to hear from anyone familiar 
with the historical context here.


We should ask the intention of the person who made the change, which is 
in the repository.  If you email me the doc (the last part of the url) 
and location within, I will look it up.


--
Terry Jan Reedy

___
Python-ideas mailing list
Python-ideas@python.org
https://mail.python.org/mailman/listinfo/python-ideas
Code of Conduct: http://python.org/psf/codeofconduct/


Re: [Python-ideas] Binary arithmetic does not always call subclasses first

2017-04-24 Thread Steven D'Aprano
On Mon, Apr 24, 2017 at 05:57:17PM +1200, Greg Ewing wrote:
> Stephan Hoyer wrote:
> >In practice, CPython requires that the 
> >right operand defines a different method before it defers to it.
> 
> I'm not sure exactly what the rationale for this behaviour is,
> but it's probably something along the lines that the left
> method should already know how to deal with that combination
> of types, and right methods are only supposed to be called
> as a fallback if the left method can't handle the operands,
> so calling it in that situation would be wrong.

I've never seen that rationale before, and I don't think I would agree 
with it. And it goes against the rationale in the docs:

[...] the right operand’s __rop__() method is tried before the 
left operand’s __op__() method.

This is done so that a subclass can completely override binary 
operators. Otherwise, the left operand’s __op__() method would 
always accept the right operand: when an instance of a given 
class is expected, an instance of a subclass of that class is 
always acceptable.


I think your rationale goes against the intention as documented. There's 
no expectation that __rop__ methods are only to be called when the 
__op__ method can't handle the operands. 

In general, which operand "wins" should depend on the classes, not on 
whether they happen to be on the left or right of the operator. (Except 
in the case where we cannot decide between the operands, in which case 
we break ties by preferring the __op__.) The reason is that subclasses 
are usually intended to be more specialised than their parent, and so 
they ought to be given priority in mixed operations.

Given classes X, Y(X), with instances x and y, we should expect that the 
more specialised class (namely Y) gets called first whether we write:

x ⊕ y

or 

y ⊕ x

for any operator ⊕.

As documented in the 3 docs, we get that for free: the interpreter 
correctly calls the __op__ or __rop__ method, as needed, and the class 
author doesn't have to think about it. That's how it's documented, but 
not how it's implemented.

The alternative is that every class has to include boilerplate testing 
for subclasses, as you say:


> Following that logic, the wrapper's __add__ method in your
> example needs to allow for the subclassing case, e.g.
> 
>def __add__(self, other):
>t1 = type(self)
>t2 = type(other)
>t = t2 if issubclass(t2, t1) else t1
>return t(self.value + other.value)

but that's bad. That makes each and every class (that might ever be 
subclassed) responsible for checking for subclasses, instead of putting 
the check in one place (whichever part of the interpreter handles 
calling __op__/__rop__ methods).

Remember that a specialised subclass might not overload the __op__ and 
__rop__ methods themselves. It might overload a data attribute, or 
another method that __op__ / __rop__ call.

class A:
def __add__(self, other):
self.log()
...
__radd__ = __add__

class B(A):
def log(self):
...

A() + B()

As the more specialised instance (a subclass of A), the right hand 
operand should get the priority.


> >the behavior is different for comparisons, which defer to 
> >subclasses regardless of whether they implement a new method
> 
> Comparisons are a bit different, because they don't have
> separate left and right methods, although it's hard to see
> exactly how that affects the logic.

It doesn't affect the logic, and comparisons implement exactly the 
documented (in 3) behaviour. The only difference is that the 
reversed methods aren't spelled __rop__:

__eq__ and __ne__ are their own reflection;
__lt__ and __gt__
__le__ and __ge__


For example:

py> class A(object):
... def __lt__(self, other):
... print("lt", self)
... return True
... def __gt__(self, other):
... print("gt", self)
... return False
...
py> class B(A):
... pass
...
py> A() < B()
gt <__main__.B object at 0xb7a9e8ec>
False


The more specialised class (B) has its method called, even though it 
isn't over-ridden.



-- 
Steve
___
Python-ideas mailing list
Python-ideas@python.org
https://mail.python.org/mailman/listinfo/python-ideas
Code of Conduct: http://python.org/psf/codeofconduct/


Re: [Python-ideas] Binary arithmetic does not always call subclasses first

2017-04-24 Thread Stephan Hoyer
On Sun, Apr 23, 2017 at 10:57 PM, Greg Ewing 
wrote:

> Stephan Hoyer wrote:
>
>> In practice, CPython requires that the right operand defines a different
>> method before it defers to it.
>>
>
> I'm not sure exactly what the rationale for this behaviour is,
> but it's probably something along the lines that the left
> method should already know how to deal with that combination
> of types, and right methods are only supposed to be called
> as a fallback if the left method can't handle the operands,
> so calling it in that situation would be wrong.
>
>>
Yes, this could makes sense. But in that case, why check for explicitly
overridden methods on subclasses at all? I can rationalize either not
treating subclasses differently or always trying subclasses first, but not
the current behavior.

Of these two options, I prefer always trying subclasses first because I
agree with the rationale in the docs: "This behavior allows subclasses to
override their ancestors’ operations."

In general, code should be written such that subclasses are aware of
super-classes, not the other way around.


> The 3.x docs don't have the "and overrides" language;
>>
>
> so arguably we would be changing the implementation to match
> the docs.
>

Based on the change in the documentation between 2.x and 3.x, I wonder if
this is something that someone intended to clean up as part of Python 3000
but never got around to. I would love to hear from anyone familiar with the
historical context here.

Nonetheless, at this point the behavior has been around for quite some
time. Almost assuredly, there is *someone* relying on this feature/bug,
though probably unintentionally. So I would also love to hear from anyone
who knows of code that this change would actually break (as opposed to fix
and/or allow for removing redundant methods).

More broadly: is this change significant enough that it needs a PEP?
___
Python-ideas mailing list
Python-ideas@python.org
https://mail.python.org/mailman/listinfo/python-ideas
Code of Conduct: http://python.org/psf/codeofconduct/