
On Sat, Apr 29, 2017 at 1:31 AM, Steven D'Aprano <steve@pearwood.info> wrote:
On Fri, Apr 28, 2017 at 05:23:59PM +1000, Chris Angelico wrote: __init__ is called, the argument 42 is bound to the formal parameter "attr", the assignment self.attr = attr is run, and THEN the body of the method is called. (Before you object, see below for justification for why there has to be both a local variable and an attribute.)
If there is an exception, what do you think the stack trace will point to? It cannot point to a line of source code that says "self.attr = attr", because there is no such line. At best, it can point to the declaration "def __init__(self, self.attr):" although it might not even do that.
I don't see why it couldn't. You'll get an AttributeError trying to set "attr", or maybe it'll be an error from inside __setattr__, but either way, it's going to come from the 'def' line.
Why should it be defined in terms of general assignment? That's the point I'm making. While function sigs are a form of assignment, they're more of a declaration than an executable statement like the other binding statements. There's a superficial connection, but they really are quite different things.
There's a lot more similarities than you might think. For example, both of these will create "spam" as a local name, shadowing the global:
spam = 1 def func1(spam): print(spam) def func2(): spam = 2 print(spam)
Right. Function parameters are local variables. But you can't write:
def func(global spam):
to make it assign to a global instead of a local, or nonlocal, and you can't (currently) write:
def func(math.spam):
to make it assign to an attribute of the math module. It's a big conceptual leap to go from "formal parameters are always bound to local variables of the function" to "formal parameters of the function are bound to anything, anywhere".
I wonder whether any other language allows this?
It's a big conceptual leap to go from "iteration steps through a sequence, binding a variable to successive items in it" to the full flexibility of Python's for loop. Just look at this: for idx, val in enumerate(iterable): It's completely intuitive to an expert Python programmer because it's a common idiom, but think about it. Firstly, you create an iterable as a derivative of another iterable. We're building not just lists out of lists, but lazy iterables out of each other. And that iterable yields two-item tuples. Finally, we do an unpacking assignment, stashing the two items into two separate names. And nobody is suggesting that we should restrict this syntax to simple examples like this; there is absolutely nothing wrong with having incredibly complicated and ridiculous assignments inside a for loop. Why? Because a for loop does simple assignment, nothing more, nothing less.
As will many other forms of assignment, like "import spam" or "with x as spam:". Some forms are more restricted than others ("import" requires a NAME, not an arbitrary expression),
Yes, we could restrict the syntax to simple identifiers with a maximum of a single dot:
def __init__(self, self.attr, # allowed self[0].foo(1)['key'], # SyntaxError ):
which will avoid the worst of the horrors.
Yeah. In that sense, it actually is more like decorator syntax, which is defined as a strict subset of expression syntax - you have an absolute guarantee that any legal decorator is semantically equivalent to the same expression elsewhere, but there are lots of expressions that you can't use in a decorator. I'd be fine with that. It's still defined in terms of assignment, but your example would indeed give a SyntaxError.
You need to speak to more beginners if you think the connection between spam.x and x is obvious:
def func(spam.x): print(x)
Where is x declared? It looks like there's a local spam.x which isn't used, and a global x that is. But that's completely wrong. Without the context of somebody telling you "this is syntax for magically assigning to self.attributes in the constructor", I believe this will be unfathomable to the average non-expert.
Not sure what you mean. By my reading, that's exactly correct - there IS a global x that is being used here,
No there isn't. "x" is the parameter name, even though it is written with a "spam." prefix. Let's go back to the original:
class MyClass: def __init__(self, self.attr): ...
instance = MyClass(42)
That binds 42 to the local variable "attr", as well as doing the self.attr=attr assignment. After all, surely we want to be able to refer to the parameter by its local variable, for efficiency (avoiding redundant look-ups of self), or to by-pass any __getattr__ on self.
Waaaaaaaaaait. Why? If you're slapping it in there, you should have no guarantee that it exists under any other name.
Or to really push the message, think about calling it by keyword:
instance = MyClass(self.attr=42) # No. instance = MyClass(attr=42) # Yes.
In other words, even though the formal parameter is written as "self.attr", the argument is bound to the local "attr".
Quite frankly, I thought this was so self-evident that it didn't even occur to me that anyone would have imagined that no local variable "attr" would be created. As weird as it is to have a parameter declared as "self.attr" but bound to "attr", that's not as weird as having a parameter declared as "self.attr" but not bound to any local at all.
No way is that self-evident. If you want something in two places, you put it there yourself. The parameter puts the value into exactly one place. In all my examples of equivalence, there was not a single hint of a local name "attr". Maybe that's why you consider this weird? Because your imagined semantics are NOT equivalent to assignment?
(If instance attributes and locals used the same notation, namely a bare identifier, we could gloss over this distinction. But we're not using Java :-)
Haha. Yeah, well, that actually doesn't work - or at least, not in C++ (not sure about Java but I expect it's the same). You can have locals and instance attributes with the same names, and then you refer to the latter as "this->foo". So it still wouldn't help. :) ChrisA