Verbose and flexible args and kwargs syntax

Steven D'Aprano steve+comp.lang.python at pearwood.info
Mon Dec 12 21:43:00 EST 2011


On Mon, 12 Dec 2011 04:21:15 -0800, Eelco wrote:

>> No more, or less, explicit than the difference between "==" and "is".
> 
> == may be taken to mean identity comparison; 'equals' can only mean one
> thing.

Nonsense. "Equals" can be taken to mean anything the language designer 
chooses, same as "==". There is no language police that enforces The One 
True Meaning Of Equals. In fact, there is no one true meaning of equals. 
Even my tiny Pocket Oxford dictionary lists five definitions.

It is ironic that the example you give, that of identity, is the standard 
definition of equals in mathematics. 2*2 = 4 does not merely say that 
"there is a thing, 2*2, which has the same value as a different thing, 
4", but that both sides are the same thing. Two times two *is* four. All 
numbers are, in some sense, singletons and equality implies identity.

A language designer might choose to define equals as an identity test, or 
as a looser "values are the same" test where the value of an object or 
variable is context dependent, *regardless* of how they are spelled: = == 
=== "is" "equals" or even "flibbertigibbet" if they wanted to be 
whimsical. The design might allow types to define their own sense of 
equality.

Triangle.equals(other_triangle) might be defined to treat any two 
congruent triangles as equal; set equality could be defined as an 
isomorphism relation; string equality could be defined as case-
insensitive, or to ignore leading and trailing whitespace. Regardless of 
whether you or I *would* make those choices, we *could* make those 
choices regardless of how our language spells the equality test.


> Of course 'formally' these symbols are well defined, but so is
> brainf*ck

I don't understand your point here.



>>  Modulo is hardly an obscure operation. "What's the remainder...?" is a
>>  simple question that people learn about in primary school.
> 
> 
> So is 'how much wood would a woodchucker chuck if a woodchucker could
> chuck wood?'. But how often does that concept turn up in your code?

You didn't make a statement about how often modulo turns up in code 
(which is actually quite frequently, and possibly more frequently than 
regular division), but about the obscurity of the operation. Taking the 
remainder is not an obscure operation. The names "modulo" and "modulus" 
may be obscure to those who haven't done a lot of mathematics, but the 
concept of remainder is not. "How many pieces are left over after 
dividing into equal portions" is something which even small children get.


>> And you can blame C for the use of % instead of mod or modulo.
> 
> I didnt know one of Python's design goals was backwards compatibility
> with C.

Don't be silly. You know full well Python is not backwards compatible 
with C, even if they do share some syntactical features.

C is merely one of many languages which have influenced Python, as are 
Haskell, ABC, Pascal, Algol 68, Perl (mostly in the sense of "what not to 
do" <wink>), Lisp, and probably many others. It merely happens that C's 
use of the notation % for the remainder operation likely influenced 
Python's choice of the same notation.

I note that the *semantics* of the operation differs in the two 
languages, as I understand that the behaviour of % with negative 
arguments is left undefined by the C standard, while Python does specify 
the behaviour.


>>  I can't imagine what sort of Python code you have seen that you
>>  consider 90% attribute access "typical". I've just run the Python
>>  tokenizer over my startup.py file, and I get these results:
> 
> Yes, that was a hyperbole; but quite an often used construct, is it not?

It's hard, but not quite impossible, to write useful Python code without 
it, so yes.

 
>>  If you can supply any function at all, what happens if I write this:
> 
> 
> You cannot; only constructors modelling a sequence or a dict, and only
> in that order. Is that rule clear enough?

But why limit yourself to those restrictive rules?

If I want to collect a sequence of arguments into a string, why shouldn't 
I be allowed to write this?

    def func(parg, str(args)): ...

If I want to sum a collection of arguments, why not write this?

    def func(pargs, sum(args)): ...

Isn't that better than this?

    def func(pargs, *args):
        args = sum(args)
        ...


But no. I don't mean those examples to be taken seriously: when you 
answer to your own satisfaction why they are bad ideas, you may be closer 
to understanding why I believe your idea is also a bad idea.



>> I believe that your proposal leads to an over-generalisation "call
>> arbitrary functions when handling parameter lists".
> 
> I hope the above clears that up. It is as much about calling functions
> as ** is about raising kwargs to the power of.

I don't understand this comment. Nobody has suggested that ** in function 
parameter lists is the exponentiation operator.

As for "calling functions", how else do you expect to generate a type if 
you don't call the type constructor? One of your early examples was 
something like:

def func(parg, attrdict(kwargs)): ...

If you expect kwargs to be an attrdict, which is not a built-in, 
presumably you will have needed to have defined attrdict as a type or 
function, and this type or function will get called at run time to 
collect the kwargs. That is all.

 
>> I don't believe you
>> need this added complication. If you want to your var args as a list,
>> call list(args) inside your function.
> 
> We dont strictly 'need' any language construct. Real men use assembler,
> right?

"We're not using assembly" is not a reason to add a feature to a 
language. Every feature adds cost to the language:

* harder to implement;
* harder to maintainer;
* larger code base;
* more documentation to be written;
* more tests to be written;
* more for users to learn

etc.


>>  >  head, tuple(tail) = iterable
>>  In Python 3, that is spelled:
>>  head, *tail = iterable
>>  tail = tuple(tail)
> 
> Yes, I know. How is that not a lot more verbose and worse than what I
> have proposed in all possible ways?

That *specific* syntax, outside of function declarations, is something 
I've often thought might be useful. But if I were to argue its case, I 
would allow arbitrary functions, and treat it as syntactic sugar for:

head, *tail = iterable
tail = func(tail)  # or possibly func(*tail)

But that's pie-in-the-sky musing. I certainly wouldn't put it in function 
parameter lists. Param lists should be declarations, not code. Apart from 
the most limited collation of args, code belongs inside the body of the 
function, not the param list:

def foo(a, 2*b+1, c):  # double the second arg and add 1


>> head, tail = somestring[0], somestring[1:]
> 
> Well yes, splendid; we can do that with lists too since the dawn of
> Python. What you are saying here in effect is that you think the
> head/tail syntax is superfluous; that youd rather see it eliminated than
> generalized.

No. 

It is not "head/tail" syntax, but sequence unpacking, and it has been 
nicely generalised to allow things like this in Python 3:

a, b, c, d, *lump, x, y z = any_iterable

any_iterable isn't limited to a list, str, or other object which supports 
slicing. It can be any object supporting the iteration protocol.

What I'm saying is that there is no need to OVER-generalise this to 
specify the type of *lump within the packing operation. If you want lump 
to be something other that Python's choice, perform the conversion 
yourself.


-- 
Steven



More information about the Python-list mailing list