Python is DOOMED! Again!

Chris Angelico rosuav at gmail.com
Thu Jan 22 10:11:40 EST 2015


On Fri, Jan 23, 2015 at 1:16 AM, Steven D'Aprano
<steve+comp.lang.python at pearwood.info> wrote:
> Mario Figueiredo wrote:
>
>> def handle_employees(emp: Union[Employee, Sequence[Employee]], raise:
>> Union[float, Sequence[float]]) -> Union[Employee, Sequence[Employee],
>> None]:
>
> Using
> floats for money is Just Wrong and anyone who does so should have their
> licence to program taken away.

But using a float to specify a percentage raise that an employee (or
class of employees) gets would be reasonable. You could "handle" all
your problem employees by giving them an across-the-board 3% raise to
try to stop them from going on strike (they have a Union, as we can
see there, so strikes are clearly possible).

Not that that matters to the typing question. If it really is a
monetary type, all you need to do is replace 'float' with 'Money'
which would be a superclass to USD, AUD, GBP, and RepublicCredits.
There's still the question of what it's doing.

My best guess about this function is that it has three modes:

1) handle_employee(emp: Employee, pay_raise: float)
Give one employee a percentage raise. (Note the different function name.)
2) handle_employees(emp: Sequence[Employee], pay_raise: float)
Give all these employees the same percentage raise.
3) handle_employees(emp: Sequence[Employee], pay_raise: Sequence[float])
Equivalent to [handle_employee(e, r) for e, r in zip(emp, pay_raise)]

I can't figure out any sane meaning for passing a single employee and
a sequence of floats, and I think the one-and-one case should have a
different name. That would mean that there's really only one Union
involved, and the plural function would look like this:

def handle_employees(emp: Sequence[Employee], pay_raise: Union[float,
Sequence[float]]):
    """Atomically give many employees raises.

    If pay_raise is a float, gives each employee that raise; otherwise, it
    should be a sequence of the same length as emp.
    """
    if isinstance(pay_raise, collections.Sequence):
        for e, r in zip(emp, pay_raise):
            handle_employee(e, r)
    else:
        for e in emp:
            handle_employee(e, pay_raise)

But I still have no clue what the return value of either the
one-and-one case or the multiple case should be.

> (3) Have a shorter way to declare "Spam or Sequence (tuple?) of Spam".
>
>
> def handle_employees(
>             emp: OneOrMore[Employee],
>             pay_raise: OneOrMore[int])
>             -> OneOrMore[Employee] | None:
>     pass

Yes, that would be a reasonable thing. It might even be possible to
implement it on top of TypeVar. That said, though, APIs that accept
"just one, or maybe more" have problems; for instance, you can't write
a "pretty-print" function that can take either an arbitrary object, or
a sequence of arbitrary objects. Fundamentally not possible. But when
you can guarantee that the thing you're getting OneOrMore of is not
itself iterable, it would be useful.

>> Meanwhile there's quite a few more generics like the Sequence one above
>> you may want to take a look at and try and remember. And that's just one
>> factory (the generics support factory). You may also want to take a look
>> at TypeVar and Callable for more syntactic hell.
>
> Exaggerate, much?

Maybe. Callable does get pretty hairy; specifying the arguments and
return values of a function is never pretty. Pike is no better:

> typeof(write);
(1) Result: function(array(string) | string, mixed ... : int)

That's a function which takes either a single string or an array of
strings, followed by any number of additional arguments, and returns
an integer.

> typeof(GTK2.setup_gtk);
(2) Result: function(void | array(string) | string, void | int : array(string))

Optional arguments: one or more strings, and maybe an integer. Returns
an array of strings.

> typeof(rm);
(3) Result: function(string : int)

This one's pretty simple. It takes a string (path name), and returns
an integer (success or failure flag). And deletes a file/directory.

> typeof(asin);
(4) Result: function(int | float : float)

Could take an integer or a float, and returns a float.

The PEP 484 equivalents would be, I think:

write = Callable[[Union[str, Sequence[str]], AnyArgs], int]
# I have no idea how to do optional args.
rm = Callable[[str], int]
asin = Callable[[Union[int, float]], float]

It's not too bad when the function signature is really simple. And
that's going to be the common case. But it is syntactically
complicated to type-check a callback's arguments. Imagine, for
instance, GUI toolkit callbacks; chances are you can stuff extra args
into them (so they need a *args equivalent at the end), and they'll
take a variety of different args. They will not be pretty... or else
they'll be type-hinted simply as Callable, with nothing else.

But ultimately, that's not a fault of the type hinting structure. It's
a natural consequence of the complexity of callbacks. You can't get
away from it.

ChrisA



More information about the Python-list mailing list