21

a.count(0) always returns 11, so what should I do to discount the False and return 10?

a = ["a",0,0,"b",None,"c","d",0,1,False,0,1,0,3,[],0,1,9,0,0,{},0,0,9]
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Tony Wang
  • 971
  • 4
  • 16
  • 33
  • In Python 3.x True and False are keywords and will always be equal to 1 and 0 – Praveen Sep 09 '16 at 05:49
  • @Praveen You're saying that as if there's no way to distinguish False from 0. – Aran-Fey Sep 09 '16 at 05:50
  • @Praveen: try `assert False is not 0` out for size. Equal, yes, but the same object, no – Eric Sep 09 '16 at 05:57
  • Now I got it.. thanks Rawing and Eric – Praveen Sep 09 '16 at 05:58
  • No matter what others say to convince me, I think this is a bug. `count` shouldn't have counted False as 0 in the first place. – Ozgur Vatansever Sep 09 '16 at 05:59
  • 2
    @ozgur: Well, it's completely expected according to the language semantics. You can file a bug report if you want, but it'll get closed as not a bug immediately. – user2357112 Sep 09 '16 at 06:00
  • @ozgur: This is consistent with `0 in [False]`, which returns True. Evidently it was decided that list membership should be tested by equality not identity. – Eric Sep 09 '16 at 06:00
  • 1
    @user2357112 I am well aware of the language semantics but I also think these kind of things can lead to very subtle bugs which can easily slip through. – Ozgur Vatansever Sep 09 '16 at 06:03
  • @ozgur: The fact that an interaction can lead to bugs doesn't make it a bug, especially when changing it would lead to worse bugs. Trying to special-case this would just lead to even weirder bugs. Python's True and False are numerically equal to 0 and 1. It's much better to be consistent about that than to try to make them sometimes numeric and sometimes not. Similarly, if you tried to make `list.count` care about types, you'd get `[0L].count(0) == 0`, and that'd be far worse than `[False].count(0) == 1`. – user2357112 Sep 09 '16 at 16:50
  • @user2357112 Well, luckily your last example wouldn't matter in python3+ since there'd be only the long type. However we could argue that we might wish to have `[0.0].count(0) == 1`. – Bakuriu Sep 09 '16 at 18:21

9 Answers9

13

Python 2.x interprets False as 0 and vice versa. AFAIK even None and "" can be considered False in conditions. Redefine count as follows:

sum(1 for item in a if item == 0 and type(item) == int)

or (Thanks to Kevin, and Bakuriu for their comments):

sum(1 for item in a if item == 0 and type(item) is type(0))

or as suggested by ozgur in comments (which is not recommended and is considered wrong, see this), simply:

sum(1 for item in a if item is 0)  

it may (“is” operator behaves unexpectedly with integers) work for small primary types, but if your list contains objects, please consider what is operator does:

From the documentation for the is operator:

The operators is and is not test for object identity: x is y is true if and only if x and y are the same object.

More information about is operator: Understanding Python's "is" operator

Community
  • 1
  • 1
Ahmad Siavashi
  • 979
  • 1
  • 12
  • 29
  • 1
    _"Always"_ is a fabrication - clearly `type(0)` is treated differently to `type(False)`, otherwise your code wouldn't work – Eric Sep 09 '16 at 05:59
  • @Eric It's about implicit and explicit type conversion. You are right, `always` is not that accurate, I remove it to make the answer less controversial. – Ahmad Siavashi Sep 09 '16 at 06:09
  • @ozgur It would work for primary types. For objects, it depends on the programmer that how & what he/she wants to count. I do not suggest it as it may cause undefined behavior. – Ahmad Siavashi Sep 09 '16 at 06:38
  • 1
    It will not count 0.0 == 0. – VPfB Sep 09 '16 at 06:44
  • @AhmadSiavashi `list.count(0)` counts also 0.0, so I would say yes. – VPfB Sep 09 '16 at 06:50
  • 8
    No, no, no. **Never** do `if x is 0` (or any integer instead of 0). It may happen to work, but that's an implementation detail (small integer interning) which doesn't even work for larger numbers. A conforming implementation is perfectly free to create multiple distinct zero objects and return false for `0 is 0`. The correct way to check this is `x == 0 and type(x) is int` (note that there's only one `int` class, ever, so it's safe to use `is` here). – Kevin Sep 09 '16 at 07:12
  • @Kevin You're totally right Kevin. I'll modify the answer. – Ahmad Siavashi Sep 09 '16 at 07:24
  • @VPfB tune the answer according to your needs. You can use `or` to include floats as well. – Ahmad Siavashi Sep 09 '16 at 07:25
  • @Kevin I would still keep the `is` solution along with reasons not to use it. So that later readers do not get caught in this pitfall. – Ahmad Siavashi Sep 09 '16 at 07:34
  • 3
    @Kevin Well, you can always do `int = bool`. If you want a 1000% working solution `type(x) is type(0)` works because there is no way to change the type of an integer literal. Also, you can simplify the whole thing using tuple comparisons: `list(map(lambda x: (x, type(x)), sequence).count((0, type(0)))`. – Bakuriu Sep 09 '16 at 10:00
  • @Bakuriu: You can always do `type = SomethingCrazy` too, so if you care about correctness in the face of redefined builtins, it'd have to be something like `x.__class__ is (0).__class__`, or `().__class__.__class__(x) is (0).__class__` if you want to catch types that lie about their class through `__class__`. – user2357112 Sep 09 '16 at 16:41
10

This can be done with sum and a generator expression that compares each element to False:

>>> a = ["a",0,0,"b",None,"c","d",0,1,False,0,1,0,3,[],0,1,9,0,0,{},0,0,9]
>>> sum((x == 0 and x is not False) for x in a)
10

This works because bool is a subclass of int - False equals 0 and True equals 1.

Aran-Fey
  • 39,665
  • 11
  • 104
  • 149
8

You need to filter out the Falses yourself.

>>> a = ["a",0,0,"b",None,"c","d",0,1,False,0,1,0,3,[],0,1,9,0,0,{},0,0,9]
>>> len([x for x in a if x == 0 and x is not False])
10

Old answer is CPython specific and it's better to use solutions that work on all Python implementations.

Since CPython keeps a pool of small integer objects, zero included, you can filter the list with the is operator.

This of course shouldn't be used for value comparisons but in this case it works since zeros are what we want to find.

>>> a = ["a",0,0,"b",None,"c","d",0,1,False,0,1,0,3,[],0,1,9,0,0,{},0,0,9]
>>> [x for x in a if x is 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
>>> len(_)
10
Community
  • 1
  • 1
Sevanteri
  • 3,749
  • 1
  • 23
  • 27
  • 3
    More specifically, _CPython_ keeps a small pool. I don't believe a conforming python implementation is required to do this. – Eric Sep 09 '16 at 05:57
  • 5
    Relying on CPython's internal optimization details is a bad idea, especially when it's so easy to write code that doesn't rely on them. – user2357112 Sep 09 '16 at 05:58
  • 2
    Small remark: `filter(lambda` is antipattern almost always. Comprehensions works faster in most cases, lambda is too heavy – Slam Sep 09 '16 at 05:59
6

How about this functional style solution:

print zip(a, map(type, a)).count((0, int))
>>> 10

Timing this against some of the other answers here, this also seems to be one of the quickest:

t0 = time.time()
for i in range(100000):
    zip(a, map(type, a)).count((0, int))
print time.time() - t0
>>> 0.275855064392

t0 = time.time()
for i in range(100000):
    sum(1 for item in a if item == 0 and type(item) == int)
print time.time() - t0
>>> 0.478030204773

t0 = time.time()
for i in range(100000):
    sum(1 for item in a if item == 0 and type(item) is type(0))
print time.time() - t0
>>> 0.52236700058

t0 = time.time()
for i in range(100000):
    sum((x==0 and x is not False) for x in a)
print time.time() - t0
>>> 0.450266122818
kezzos
  • 3,023
  • 3
  • 20
  • 37
3

Solving this problem more generally, you could make your own subclass of list:

class key_equality_list(list):
    def __init__(self, key, items):
        super(key_equality_list, self).__init__(items)
        self._f = key

    def __contains__(self, x):
        return any(self._f(x) == self._f(item) for item in self)

    def count(self, x):
        return sum(self._f(x) == self._f(item) for item in self)

And then define a key function that checks types:

type_and_val = lambda x: (x, type(x))

So your usage becomes:

>>> a = ["a",0,0,"b",None,"c","d",0,1,False,0,1,0,3,[],0,1,9,0,0,{},0,0,9]
>>> a = key_equality_list(type_and_val, a)
>>> a.count(0)
10
>>> a.count(False)
1
Eric
  • 95,302
  • 53
  • 242
  • 374
2

Another option is to first "decorate" the list by adding the types and the count:

decorated_seq = list(map(lambda x: (x, type(x)), sequence))
decorated_seq.count((0, type(0)))

If you want to have 0 == 0.0 you could do:

decorated_seq = list(map(lambda x: (x, isinstance(x, bool)), sequence))
decorated_seq.count((0, False))

Which counts all 0s that are not of type bool (i.e. Falses). In this way you can define what count should do in any way you want, at the cost of creating a temporary list.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Bakuriu
  • 98,325
  • 22
  • 197
  • 231
2

Since count returns the number of items equal to its input, and since 0 is equal to False in Python, one way to approach this is to pass in something that is equal to 0 but is not equal to False.

Python itself provides no such object, but we can write one:

class Zero:
    def __eq__(self, other):
        return other == 0 and other is not False

print([1, 2, 0, False].count(Zero()))

Be aware that there are other things in Python that, like False, are equal to 0. I can think of 0.0, decimal.Decimal(0), and fractions.Fraction(0,1). Furthermore, anybody can subclass int to create other "fake 0 values", just like False. The code above counts all of these, treating False as the only special case, but you might want to do something different in your __eq__.

Of course "under the covers" this is very similar to just doing sum(other == 0 and other is not False for other in a). But considering how count() is defined, it should come as no surprise that a.count(x) is similar to sum(x == y for y in a).

You would only do this in the rare case where you have for some reason already decided to use count (perhaps because you're using some third-party code that's going to call it), and need to come up with a suitable object to pass it. Basically, it treats count as a function that calls an arbitrary function we write (requiring only that this function be the __eq__ function of the argument) and counts the number of true return values. This is an accurate description of what count does, but not really the point of count since sum is more explicit.

Steve Jessop
  • 273,490
  • 39
  • 460
  • 699
1

Many appropriate answers already given. Of course, an alternative is to filter the list on the type of the element you are querying for first.

>>> a = ["a",0,0,"b",None,"c","d",0,1,False,0,1,0,3,[],0,1,9,0,0,{},0,0,9]
>>> def count(li, item):
...     return list(filter(lambda x: type(x) == type(item), li)).count(item)
...
>>>
>>> count(a, 0)
10
>>> count(a, False)
1
>>> count(a, "a")
1
>>> class my_own_int(int): pass
...
>>> a.append(my_own_int(5))
>>> a.count(5)
1
>>> count(a, 5)
0

In case you are worried about the copy, you can use sum and abuse the fact we are trying to circumvent above (that True == 1).

def count(li, item):
    return sum(map(lambda x: x == item, filter(lambda x: type(x) == type(item), li))) 
user1556435
  • 966
  • 1
  • 10
  • 22
0

If you run into that kind of problems frequently you can introduce the following class*

class Exactly:
    def __init__(self, something):
        self.value = something

    def __eq__(self, other):
        return type(self.value) == type(other) and self.value == other

and use it as follows:

a.count(Exactly(0)) 

* Of course you may need to enhance it for other operations too.

Leon
  • 31,443
  • 4
  • 72
  • 97