Suggestion: make sequence and map interfaces more similar

Steven D'Aprano steve at pearwood.info
Wed Mar 23 07:26:44 EDT 2016


On Wed, 23 Mar 2016 06:54 am, Marco S. wrote:

> I noticed that the sequence types does not have these methods that the map
> types  has: get(), items(), keys(), values().
> 
> It could seem useless to have them for sequences, 

That's putting it mildly.

> but I think it will ease 
> the creation of functions and methods that allow you to input a generic
> iterable as parameter, but needs to use one of these methods in case the
> parameter is a map.

Perhaps it would. But countering that is the disadvantage that you're adding
methods to sequences that have no sensible meaning for a sequence.

Strings and floats are quite different things, but occasionally it makes
sense to write a function that accepts either a string or a float. Perhaps
we could introduce methods to strings and floats to blur the difference,
to "ease the creation of functions and methods that allow you to input a
generic scalar (string, float, int) as parameter..."?

Some of these methods would be easy:

class float:
    def upper(self):
        return self


Some a bit more work:

    def find(self, substring, start, end):
        s = str(self)
        return s.find(substring, start, end)

but some perplex me. What would (27.25).expand_tabs() do?


Of course this is silly. Even Perl and Javascript don't go this far in
making floats and strings interchangeable. This is a good thing: if you're
calling some_float.upper(), you've almost certainly made a programming
error, and you don't want this nonsensical method call to silently succeed.

So it is with sequences and mappings. They are *not* the same sort of thing,
even though they have a few superficial similarities, and they shouldn't
try to behave as the same thing.

What should some_dict.append(None) do? The very concept is nonsense: dicts
aren't *ordered sequences*, you can't append to a dict. Maybe you could
give dicts a method *called* "append", but it wouldn't mean the same thing
as list.append, and it probably wouldn't have the same signature:


list.append(value)
    Appends value to the end of the list.

dict.append(key, value)
    Adds a new key to the dict, with the given value. 
    Same as dict[key] = value.


Forcing these two completely different methods to have the same name doesn't
do anything useful. That's like putting a door handle on your iPhone, in
case some day you want to treat your iPhone as a door.


> In one word, it will facilitate duck typing.

I think you have misunderstood the purpose and meaning of duck-typing.

Duck-typing is not about forcing unrelated, dissimilar types to have the
same interface just in case you want to write a function that will accept
either type.

Duck-typing is about accepting anything which "quacks like a duck". If all
you need is something which lays an egg, you shouldn't care whether it is a
chicken or a goose or a duck. It doesn't mean that dogs and cats should
have a "lay_egg" method, just in case somebody wants to accept a duck or a
dog.


> For the same reason, I would suggest the introduction of a new map type,
> vdict, a dict that by default iterates over values instead over keys. So a
> vdict object "d" wiil have iter(d) == iter(d.values()), and should also
> have a count() method, like sequence types.

I don't really see the point in this. What sort of function will expect to
iterate over a mapping, but not care whether it is getting keys or values?

Sure, there are generic functions that will iterate over *any iterable*, and
you can pass dict.keys() or dict.values(), and both will work fine. That's
exactly what duck-typing is about.

But let's imagine this hypothetical function that will accept any mapping,
and some mappings will iterate over keys and some mappings iterate over
values. What would you do with it?

def walk(the_dict):
    for obj in the_dict:
        print("Key =", obj)  # That's wrong, it might be a value.
        value = the_dict[obj]  # That's wrong too.
        the_dict[obj] = "processed"  # Still wrong.


There's nothing useful or interesting you can do with a mapping and
something which might be a key, or might be a value, but you don't know
which. You can treat them in isolation, as if they had nothing to do with a
dict, and that's about it. But in that case, why insist on a dict?

walk(the_dict.keys())
walk(the_dict.values())
walk(the_list)
walk(the_iterator)


> Indeed sequences are, in my humble opinion, a specialized case of maps,
> when keys are numeric only, are always contiguous without gaps and start
> from 0.

That's a very superficial similarity: a list ['a', 'b', 'x', 'y'] is
something like a mapping {0: 'a', 1: 'b', 2: 'x', 3: 'y'}. Seems logical,
since in both cases we write collection[2] and get 'x' back.

But think about it a bit more, and you will see that the behaviour is in
fact *very different*. For example:

the_list = ['a', 'b', 'x', 'y']
# the_list is equivalent to {0: 'a', 1: 'b', 2: 'x', 3: 'y'}
the_list.insert(0, 'z')
# the_list is now equivalent to {0: 'z', 1: 'a', 2: 'b', 3: 'x', 4: 'y'}

Every existing "key:value" pair has changed! What sort of mapping operates
like that?

del the_list[2]
# the_list is now equivalent to {0: 'z', 1: 'a', 2: 'x', 3: 'y'}

I've said to delete the "key" 2, but there it still is. "Key" 2 still
exists, it just has a different value! And it is "key" 4 which has been
deleted! But not the *value* attached to "key" 4, that's been moved to 3!

What sort of crazy mapping behaves like this?

The answer is, of course, *no* sort of mapping. Sequences are not mappings.
The only reason we think of them as kinda-sorta like mappings is because of
a superficial similarity between key:value and index:item. That similarity
is real, but virtually everything else about mappings and sequences is
different.



> This way we will have a simpler way to let people to use sequences 
> or maps indifferently, and let the code untouched.

Have you ever actually wanted to use sequences or maps indifferently? To do
what? 

The only case I've ever seen of that is the dict constructor, and
dict.update, which will accept either a mapping or a sequence of (key,
value) pairs.




-- 
Steven




More information about the Python-list mailing list