I've run the following small test in python 2.7.6:
s = set(xrange(0, 1000000))
for i in xrange(0, 5000000):
    if s.__contains__(i):
        pass
and got the following output for running time python py.py:
real    0m0.616s
then I changed my code to:
s = set(xrange(0, 1000000))
for i in xrange(0, 5000000):
    if i in s:
        pass
and got run time of 0.467s. I also got same results for dict. My question is "why there is performance difference?", maybe some explanation of how python performs calls of s.__contains__(i) and i in s
When using s.__contains__(i) you need to do the attribute lookup in Python, as well as make a method call in Python. These are executed as separate bytecodes:
>>> import dis
>>> dis.dis(compile('s.__contains__(i)', '<>', 'exec'))
  1           0 LOAD_NAME                0 (s)
              3 LOAD_ATTR                1 (__contains__)
              6 LOAD_NAME                2 (i)
              9 CALL_FUNCTION            1
             12 POP_TOP             
             13 LOAD_CONST               0 (None)
             16 RETURN_VALUE        
LOAD_ATTR loads the __contains__ attribute, and CALL_FUNCTION then executes the method.
Using i in s takes just one bytecode:
>>> dis.dis(compile('i in s', '<>', 'exec'))
  1           0 LOAD_NAME                0 (i)
              3 LOAD_NAME                1 (s)
              6 COMPARE_OP               6 (in)
              9 POP_TOP             
             10 LOAD_CONST               0 (None)
             13 RETURN_VALUE        
Here COMPARE_OP does all the work.
In C, then, looking up the __contains__ slot (or rather, its C equivalent) and calling it is much faster.
Note that when comparing two approaches in Python, it is much better to use the timeit module rather than the UNIX time command; it'll try to eliminate environmental factors and repeat the test for you:
>>> import timeit, random
>>> testset = {random.randrange(50000) for _ in xrange(1000)}
>>> tests = [random.randrange(5000) for _ in xrange(500)]
>>> timeit.timeit('for i in tests: i in s',
...               'from __main__ import testset as s, tests',
...               number=100000)
2.5375568866729736
>>> timeit.timeit('for i in tests: s.__contains__(i)',
...               'from __main__ import testset as s, tests',
...               number=100000)
4.208703994750977
Using __contains__ is slower by some distance.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With