Re: Python 0.9.4alpha; Re: Class initialization

Tim Peters (tim@ksr.com)
Mon, 30 Dec 91 00:13:20 EST

Congratulations on 0.9.4alpha -- as usual, a very fine piece of work!
Some comments:

> ...
> New argument passing semantics
> ...
> Two other rules and a new built-in function handle conversion between
> tuples and argument lists:

The "new built-in function" was not mentioned again in this msg; from
the later "Class initialization" mail I assume it's the new "apply"
built-in.

> Rule (a): when a function with more than one argument is called with a
> single argument that is a tuple of the right size, the tuple's items
> are used as arguments.
>
> Rule (b): when a function with exactly one argument receives no
> arguments or more than one, that one argument will receive a tuple
> containing the arguments (the tuple will be empty if there were no
> arguments).

Seems to me that Python must have these rules in order to preserve the
equivalence (described in misc/CLASSES) between argument-passing
semantics and the semantics of multiple assignment. I'm all in favor of
that!

But there are still (at least) two things about variadic functions that
people are certain to trip over (not claiming you don't already realize
this, am claiming I'm not the only one who will notice <grin>):

Oddity #1:

>>> def f(x): print x
..
>>> f(1,2,3)
(1, 2, 3)
>>> f(1,2)
(1, 2)
>>> f(1)
1
>>> f()
()
>>>

That is, passing exactly one argument doesn't fit the pattern: the
called function does not see a tuple in that (& only in that) case. I
do *not* want to see that changed, though, because of the useful analogy
with the meanings of

X = 1, 2, 3 # X is a 3-tuple
X = 1, 2 # X is a 2-tuple
X = 1 # X is *not* a tuple
X = () # X is an empty tuple

More on this a bit later.

Oddity #2:

>>> def f(x): print x
..
>>> f()
()
>>> f(())
()
>>>

That is, a variadic function can't distinguish between being called with
no arguments and being called with exactly one argument that happens to
be an empty tuple. Believe it or not <grin>, I can picture situations
in which this will create a problem; more later.

> ...
> - for sequences (string/tuple/list), x[-i] is now equivalent to x[len(x)-i]

Spooky -- you must have read my mind <0.9 grin>.

> ...
> - C shifting and masking operators: << >> ~ & ^ | (for ints and longs).

Noted that (the new?) doc/ref.tex says that the operands must be short
integers for all of these operators except for ~. Noted too that Python
actually accepts longs for all these, so assume your note above is the
real intent.

Want to argue that for longs, the best way to specify these operators is
to (1) treat longs "as if" they were 2's-complement integers with an
unbounded number of sign bits, and (2) that ">>" be arithmetic when
applied to longs (that is, since the string of sign bits is conceptually
infinite, a zero-fill right shift on longs doesn't make sense (as there
is no "left end" for the zero bits to enter)). If those rules don't
appeal to you instantly, happy to try to justify them.

Python's first cut at this seems rather schizophrenic:

>>> m1 = -1L
>>> m1
-1L
>>> m1a = m1 >> 1 # expect -1L, mostly because nothing else makes sense
>>> m1a # but prints ~0L
~0L

If longs are modeled as unbounded 2's-comp integers, -1L and ~0L are
both infinite strings of 1's, so I would expect them to be equal:

>>> (m1 = m1a) # but they compare unequal
0
>>> (m1 = 0), (m1a = 0) # Python thinks ~0L is equal to 0 (!)
(0, 1)

In some respects m1a *is* acting like 0, but in others it's acting like
an infinite string of 1 bits, and in others ...:

>>> 42 + m1a # acts like 0 here
42L
>>> 42 & m1a # acts like binary ...1111...111 here
42L
>>> 42 | m1a # not sure what to make of this <0.9 grin>
-42L
>>> 42 * m1a # acts like 0 here
0L
>>> 42 ^ m1a # curiouser & curiouser ...
32725L

It is a bit tricky, but neither truly hard nor inefficient, to create
the consistent illusion of an infinite string of sign bits; if you agree
it's a good idea, I'd be happy to help with the algorithms.

> ... [skipping to msg "Re: Class initialization"] ...
> ... Tim notices that such a new() function is easily defined in
> Python, goes on to show how, discovers his solution doesn't quite
> work, and wonders why.
>
> Rest assured, this is not a problem with your version of Python: you
> have just discovered that the obvious way to create a tuple of one
> element (a singleton) doesn't work.

Ha -- you have done me the favor of underestimating my ignorance <smile>.
Turns out that what I actually didn't understand is that when a method
was invoked with one argument

instance.method(an_argument)

then "method" saw exactly two arguments: the instance object, and
an_argument. I erroneously *assumed* it would see the instance object
and a singleton *tuple* containing an_argument (in analogy with how,
e.g., after
instance.method(arg1,arg2)
"method" saw an instance argument and a 2-tuple (arg1,arg2)). From
there I went on to believe that parentheses in argument lists actually
did force tuple unpacking, and ... garbage in, garbage out. All your
explanations made sense here, and I'm darned glad you changed the
argument-passing method anyway <grin>.

> ... [a cleaner "new" that works under the new rules] ...
> def new(args):
> if type(args) <> type(()):
> args = (args,) # Tuple-ize
> Class = args[0]
> args = args[1:]
> instance = Class()
> return apply(instance.init, args)
>
> I think if you compare the two versions you can see why the new
> parameter handling is better. The initial "if" is still necessary;
> maybe there should be extra syntax to indicate that the (remaining)
> arguments should be stored as a tuple in the last formal parameter?

I would like that. As noted above, the current scheme still contains
some traps for the unwary (one of which is again illustrated in your
"new"). In addition, I suspect that variadic functions are rare enough
that it would help readability/maintenance if the declaration of a
variadic function had a bit of extra syntactic fluff to make it obvious.

How about stealing Icon's gimmick for this?:

def f(args()): ...
^^

The trailing parens here have no meaning in Python now (it's currently a
syntax error), and is meant to say "the arguments (if any) to f are to
be placed in a tuple and stored in 'args'; that tuple will be empty iff
f is invoked without arguments".

Thus
f() sets args to the empty tuple ()
f(()) sets args to the singleton tuple ((),)
f(1) sets args to the singleton tuple (1,)
f(1,2) sets args to the 2-tuple (1,2)
etc

This is clean & I believe it's surprise-free (at least I've never been
surprised by it when using Icon's equivalent ...).

Icon actually goes a bit farther and allows any number of "plain"
arguments to precede the (optional) catch-the-rest argument. E.g.,

def msg(outfile, tag, strings()):
outfile.write( tag )
if len(strings) > 0: outfile.write( ': ' )
for s in strings: outfile.write( s )
outfile.write( '\n' )

might be used to define a variadic msg-printer.

The one thing that bothers me about this is weakening the link between
argument-passing and assignment semantics; but perhaps this kind of
notation would also be useful in assignments? E.g.,

head, A() = A # put A[0] in head, & A[1:] in A

There are probably surprises lurking in that, though -- my head is
swimming already <0.9 grin>. Comments?

> ...
> |C) When a user can program a solution this easily, the case for building
> | it into the language is weak.
>
> I was proposing it as a *replacement* of the current notation
> "Classname()" (i.e., calling a class as if it were a parameterless
> function). I've noticed that it is quite a common mistake to write
>
> newinstance = Class.init(initialization_arguments)
>
> instead of
>
> newinstance = Class().init(initialization_arguments)
> # ^^
>
> and the error message that is issued in this case is rather confusing,
> since "Class.init" is a valid function, only it has one more argument
> than is provided...
>
> I'm not saying that a change is absolutely necessary, but I feel that
> the current solution is overloading function call with something
> rather different.

Suggest that the case for making a change got much stronger when the
class syntax changed; that is, now that a class is declared via (e.g.)

class Derived(Base):

it is much more tempting to fall into the mistake of believing that

Derived()

means something it doesn't ("hmm, it must be passing an empty tuple
to the 'Base' argument!").

> ...
> Anybody else on the list got an opinion? Should I change the language
> or not?

Think people need a concrete alternative to pick on. Since I'm on
vacation this week, I'll volunteer to be the flame absorber <0.9 grin>:

- The "Class()" notation goes away; you can no longer create a class
instance that way, & it's an error to try to. However, class
attributes continue to work exactly as they work now.

- A built-in "new(Class_name, optional_arguments)" is introduced for
creating class instances. An instance of class Class_name will be
created. If the instance does not have a method named (exactly)
"init", the instance is returned and it is an error if any optional
arguments were specified. Else the instance's init method is
invoked with an argument list composed of the instance and the
optional arguments (if any) passed to "new", and the result of the
init method is returned by "new".

- There is nothing special or unusual about a method named "init"
except that "new" looks for a method with that name.

- You want more than that, program it yourself <grin>.

fast-running-out-of-things-to-whine-about-ly y'rs - tim

Tim Peters Kendall Square Research Corp
tim@ksr.com, ksr!tim@uunet.uu.net