len() should always return something

  • Thread starter Dr. Phillip M. Feldman
  • Start date
D

Dr. Phillip M. Feldman

Some aspects of the Python design are remarkably clever, while others leave
me perplexed. Here's an example of the latter: Why does len() give an error
when applied to an int or float? len() should always return something; in
particular, when applied to a scalar, it should return a value of 1. Of
course, I can define my own function like this:

def mylen(x):
if isinstance(x,int) or isinstance(x,float): return 1
return len(x)

But, this shouldn't be necessary.
 
D

Diez B. Roggisch

Dr. Phillip M. Feldman said:
Some aspects of the Python design are remarkably clever, while others leave
me perplexed. Here's an example of the latter: Why does len() give an error
when applied to an int or float? len() should always return something; in
particular, when applied to a scalar, it should return a value of 1. Of
course, I can define my own function like this:

def mylen(x):
if isinstance(x,int) or isinstance(x,float): return 1
return len(x)

But, this shouldn't be necessary.

Can you show some example of where that is actually making a piece of
code more elegant?

Diez
 
P

Peter Otten

Dr. Phillip M. Feldman said:
Some aspects of the Python design are remarkably clever, while others
leave me perplexed. Here's an example of the latter: Why does len() give
an error when applied to an int or float? len() should always return
something; in particular, when applied to a scalar, it should return a
value of 1. Of course, I can define my own function like this:

def mylen(x):
if isinstance(x,int) or isinstance(x,float): return 1
return len(x)

But, this shouldn't be necessary.

Python should not blur the distinction between vectors an scalars like that.
Instead of trying to be clever you should pass a vector with a single item
and send mylen() to /dev/null.

On a general note, I think one of Python's strengths is that it consistently
/avoids/ this kind of cleverness.

A prominent example is the handling of "1" + 1.

Peter
 
R

Rhodri James

Some aspects of the Python design are remarkably clever, while
others leave me perplexed. Here's an example of the latter:
Why does len() give an error when applied to an int or float?
len() should always return something; in particular, when
applied to a scalar, it should return a value of 1.

If len(7) returned a value of 1, then wouldn't one expect 7[0]
to be valid? It isn't, so you'd then have to redefine all
types so that they are sequences that can be indexed. Sounds
like a big mess to me...

[Are there types for which len() returns a value that can't be
indexed?]

Dictionaries.

Which doesn't make your point less valid. In fact I'd go so
far as to argue that what len() gives you is the number of
items in a container, so len(7) should return 0.
 
M

Mark Dickinson

Which doesn't make your point less valid.  In fact I'd go so
far as to argue that what len() gives you is the number of
items in a container, so len(7) should return 0.

Nah. 7 contains three bits, so len(7) should *clearly* return 3.

Mark
 
D

Diez B. Roggisch

Mark said:
Nah. 7 contains three bits, so len(7) should *clearly* return 3.

But it contains a minimum of 32 bits! And why are you treating ones as
special over zeros? I thought the times of BitRacism were finally over...

Diez
 
P

Piet van Oostrum

Rhodri James said:
Some aspects of the Python design are remarkably clever, while
others leave me perplexed. Here's an example of the latter:
Why does len() give an error when applied to an int or float?
len() should always return something; in particular, when
applied to a scalar, it should return a value of 1.

If len(7) returned a value of 1, then wouldn't one expect 7[0]
to be valid? It isn't, so you'd then have to redefine all
types so that they are sequences that can be indexed. Sounds
like a big mess to me...

[Are there types for which len() returns a value that can't be
indexed?]
RJ> Dictionaries.
RJ> Which doesn't make your point less valid. In fact I'd go so
RJ> far as to argue that what len() gives you is the number of
RJ> items in a container, so len(7) should return 0.

But len(7) could as well be defined as 3, 1, 32, or 64 (depending on the
implementation). Therefore it doesn't make much sense.
 
R

Rhodri James

Some aspects of the Python design are remarkably clever, while
others leave me perplexed. Here's an example of the latter:
Why does len() give an error when applied to an int or float?
len() should always return something; in particular, when
applied to a scalar, it should return a value of 1.

If len(7) returned a value of 1, then wouldn't one expect 7[0]
to be valid? It isn't, so you'd then have to redefine all
types so that they are sequences that can be indexed. Sounds
like a big mess to me...

[Are there types for which len() returns a value that can't be
indexed?]
RJ> Dictionaries.
RJ> Which doesn't make your point less valid. In fact I'd go so
RJ> far as to argue that what len() gives you is the number of
RJ> items in a container, so len(7) should return 0.

But len(7) could as well be defined as 3, 1, 32, or 64 (depending on the
implementation). Therefore it doesn't make much sense.

Quite.
 
S

Steven D'Aprano

and len("7") must return 8, by the same token... but wait!

my python installation must me outdated ;-)

No no no, you're obviously using an awesome version of Python that can
compress single-character strings to a single bit!
 
R

Roy Smith

Steven D'Aprano said:
No no no, you're obviously using an awesome version of Python that can
compress single-character strings to a single bit!

Compressing strings to a single bit is easy. It's the uncompressing that's
tricky.
 
M

Mark Lawrence

Marcus said:
I assume you mean ord("7")%2?

First one to correctly decompress the value 0 into an ASCII character
wins the title of the world's most capable hacker :p

Marcus
asciichar = chr(len(0)) if the OP's wishes come true?
 
T

Tim Chase

Marcus said:
First one to correctly decompress the value 0 into an ASCII
character wins the title of the world's most capable hacker :p

Bah...uncompressing the value 0 into *an* ASCII character is
easy. Uncompressing it into the *original* ASCII character from
which it was compressed (with the aforementioned compression
method) becomes a bit trickier ;-)

-tkc
 
B

bartc

Dr. Phillip M. Feldman said:
Some aspects of the Python design are remarkably clever, while others
leave
me perplexed. Here's an example of the latter: Why does len() give an
error
when applied to an int or float? len() should always return something; in
particular, when applied to a scalar, it should return a value of 1.

So you want len() to treat 123 as though it could potentially be a list
containing a single element?

In that case there could be an ambiguity here:

print len([10,20,30])

Should it print 3 (the elements in [10,20,30]), or 1 (treating [10,20,30] as
a potential list containing the single element [10,20,30])?
 
D

Dr. Phillip M. Feldman

Here's a simple-minded example:

def dumbfunc(xs):
for x in xs:
print x

This function works fine if xs is a list of floats, but not if it is single
float. It can be made to work as follows:

def dumbfunc(xs):
if isinstance(xs,(int,float,complex)): xs= [xs]
for x in xs:
print x

Having to put such extra logic into practically every function is one of the
annoying things about Python.

Phillip
 
C

Chris Rebert

Here's a simple-minded example:

def dumbfunc(xs):
  for x in xs:
     print x

This function works fine if xs is a list of floats, but not if it is single
float.  It can be made to work as follows:

def dumbfunc(xs):
  if isinstance(xs,(int,float,complex)): xs= [xs]
  for x in xs:
     print x

Having to put such extra logic into practically every function is one of the
annoying things about Python.

You can easily fix that using decorators:

(disclaimer: untested, except mentally)

#bit of setup; pays off later
def list_or_single(func):
def wrapper(arg):
try: iter(arg)
except TypeError: arg = [arg]
return func(arg)
return wrapper

#now just prefix functions with @list_or_single

@list_or_single
def dumbfunc(xs):
for x in xs:
print x

Using the extended call syntax as I explained earlier is another option.

Cheers,
Chris
 
C

Carl Banks

Some aspects of the Python design are remarkably clever, while
others leave me perplexed. Here's an example of the latter:
Why does len() give an error when applied to an int or float?
len() should always return something; in particular, when
applied to a scalar, it should return a value of 1.

If len(7) returned a value of 1, then wouldn't one expect 7[0]
to be valid?  It isn't, so you'd then have to redefine all
types so that they are sequences that can be indexed.  Sounds
like a big mess to me...

You can call the big mess "Matlab".


Carl Banks
 
A

Andre Engels

Here's a simple-minded example:

def dumbfunc(xs):
  for x in xs:
     print x

This function works fine if xs is a list of floats, but not if it is single
float.  It can be made to work as follows:

def dumbfunc(xs):
  if isinstance(xs,(int,float,complex)): xs= [xs]
  for x in xs:
     print x

Having to put such extra logic into practically every function is one of the
annoying things about Python.

I don't have this problem - when I have a list of one element, that's
what I use - a list with one element. I think things would get much
simpler if you do such a conversion right at the Matlab-Python
interface, rather than changing all kind of things in Python so that
its types are more matlab-like. Just like you shouldn't program Python
by simply translating Java or C into Python idiom, you also shouldn't
program Python by trying to mimic Matlab. Each language has its own
strengths and weaknesses, and moving things over too much in a
simplistic fashion will usually mean that you get the weaknesses of
both and the strengths of neither.
 
S

Steven D'Aprano

Here's a simple-minded example:

def dumbfunc(xs):
for x in xs:
print x

This function works fine if xs is a list of floats, but not if it is
single float. It can be made to work as follows:

def dumbfunc(xs):
if isinstance(xs,(int,float,complex)):
xs= [xs]
for x in xs:
print x

Having to put such extra logic into practically every function is one of
the annoying things about Python.


But it's not "practically every function". It's hardly any function at
all -- in my code, I don't think I've ever wanted this behavior. I would
consider it an error for function(42) and function([42]) to behave the
same way. One is a scalar, and the other is a vector -- they're different
things, it's poor programming practice to treat them identically.

(If Matlab does this, so much the worse for Matlab, in my opinion.)

However, if you want this behaviour, it's easy enough to get it with a
decorator:

from functools import wraps
def matlab(func):
"""Decorate func so that it behaves like Matlab, that is,
it considers a single scalar argument to be the same as a
vector of length one."""
@wraps(func)
def inner(arg, **kwargs):
# If arg is already a vector, don't touch it.
try:
# If this succeeds, it's a vector of some type.
iter(arg)
except TypeError:
# It's a scalar.
arg = [arg]
# Now call the decorated function (the original).
return func(arg, **kwargs)
return inner

With this decorator in hand, you can now easily get the behaviour you
want with a single extra line per function:
.... def mean(numbers):
.... return sum(numbers)/len(numbers)
....
mean([4.5]) 4.5
mean(4.5) 4.5
mean([4.5, 3.6])
4.0499999999999998


Decorators are extremely powerful constructs, and well worth learning. As
an example, here's a similar decorator that will accept multiple
arguments, like the built-in functions min() and max():

def one_or_many(func):
"""Decorate func so that it behaves like the built-ins
min() and max()."""
@wraps(func)
def inner(*args, **kwargs):
# If we're given a single argument, and it's a vector,
# use it as args.
if len(args) == 1:
try:
iter(args[0])
except TypeError:
pass
else:
# No exception was raised, so it's a vector.
args = args[0]
# Now call the decorated function (the original).
return func(args, **kwargs)
return inner


And then use it:
.... def minmax(numbers):
.... """Return the minimum and maximum element of numbers,
.... making a single pass."""
.... # the following will fail if given no arguments
.... smallest = biggest = numbers[0]
.... for x in numbers[1:]:
.... if x < smallest:
.... smallest = x
.... elif x > biggest:
.... biggest = x
.... return (smallest, biggest)
....
minmax([2, 4, 6, 8, 1, 3, 5, 7]) (1, 8)
minmax(2, 4, 6, 8, 1, 3, 5, 7)
(1, 8)
 
C

Carl Banks

Some aspects of the Python design are remarkably clever, while others leave
me perplexed. Here's an example of the latter: Why does len() give an error
when applied to an int or float? len() should always return something; in
particular, when applied to a scalar, it should return a value of 1. Of
course, I can define my own function like this:

def mylen(x):
   if isinstance(x,int) or isinstance(x,float): return 1
   return len(x)

But, this shouldn't be necessary.

I knew that you were coming from Matlab as soon as I saw the subject
line.

I use both Matlab and Python very often, and I understand the design
decisions behind both (insofar as Matlab can be considered a
"design").

The thing to keep in mind about Python is that it's a general purpose
language, not just for mathematical computation, and there are some
things in Python that are a bit suboptimal for math calculations.
This is one of them: when the types of objects you are dealing with
most often are numbers, then there's not much chance for ambiguity
when treating a scalar as a 1x1 matrix.

However, when doing general purpose programming, you often use lots of
types besides numbers, some of which are containers themselves. This
creates the possibility of ambiguity and inconsistent behavior. Let's
consider your example function.

def dumbfunc(xs):
for x in xs:
print x

You can use it on an list, like this, and there is no ambiguity:

dumbfunc([1,2,3])

Now suppose Python allowed numbers to be treated as degenerate
sequences of length 1. Then you could pass a number, and it would
work as you expect:

dumbfunc(1)

However, because Python is general purpose, you might also want to
pass other types around, such as strings. Say you wanted to print a
list of string, you would do this, and it would work as expected:

dumbfunc(["abc","def","ghi"])

Now, the problem. In the numbers example, you were able to treat a
single number as a degenerate list, and by our supposition, it
worked. By analogy, you would expect to be able to do the same with a
list of strings, like this:

dumbfunc("abc")

Whoops: doesn't work. Instead of printing one string "abc" it prints
three strings, "a", "b", and "c".

By allowing scalar numbers to act as degenerate lists, you've
introduced a very bad inconsistency into the language, you've made it
difficult to learn the language by analogy.

For a general purpose language there is really no second-guessing this
design decision. It would be unequivocally bad to have atomic types
act like sequences. The reason it isn't so bad in Matlab is that you
can rely on the elements of an array to be scalar.

Matlab, for its part, does contain some types (cell arrays and
structures) that can contain non-scalars for cases when you want to do
that. However, you will notice that no scalar will ever be treated as
a degenerate cell array or structure. Even a poorly designed, ad hoc
language like Matlab won't try to treat scalar numbers as cell arrays,
because ambiguity like I described above will appear. Nope, it takes
an abomination like Perl to do that.

As for how I would recommend you translate the Matlab function that
rely on this: you need to decide whether that scalar number you are
passing in is REALLY a scalar, or a degenerate vector. If it's the
latter, just get over it and pass in a single-item list. I have done
these kinds of conversions myself before, and that's the way to do
it. Trying to be cute and writing functions that can work with both
types will lead to trouble and inconsistency, especially if you are a
Python newbie. Just pass in a list if your function expects a list.


Carl Banks
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,773
Messages
2,569,594
Members
45,119
Latest member
IrmaNorcro
Top