We recently wrote about funny, cunning, and weird JavaScript examples. Now it's Python's turn. Python, a high-level and interpreted language, has many convenient features. But sometimes the result of the work of some pieces of code at first glance looks unobvious.
Below is a fun project that collects examples of unexpected behavior in Python with a discussion of what is happening under the hood. Some examples do not fall into the category of real WTF?!, But instead they demonstrate interesting features of the language that you may want to avoid. I think this is a good way to learn the internal workings of Python, and I hope you find it interesting.
If you are already an experienced programmer in Python, then many examples may be familiar to you, and even cause nostalgia for those times when you racked your brains over them :)
is
not what it isis not ...
different from is (not ...)
Return
returns everywhereNote: All examples given are tested on the Python 3.5.2 interactive interpreter and should work in all versions of the language, unless otherwise explicitly stated in the description.
Structure of examples:
# . # ...
Result (Python version):
>>> _ ,
(Optional): A one-line description of the unexpected result.
Explanation:
A brief explanation of what happened and why.
( )
Result:
>>> # - , #
It seems to me that the best way to get the most out of these examples is to read them in chronological order:
PS You can also read these examples on the command line. Only first install the wtfpython
npm package,
$ npm install -g wtfpython
Now run wtfpython
on the command line, and as a result, this collection will open in your $PAGER
.
#TODO: Add the pypi package for reading at the command line.
Result:
>>> value = 11 >>> valu = 32 >>> value 11
Wat?
Note: The easiest way to reproduce this example is by copying and pasting into your file / shell.
Explanation
Some Unicode characters look the same as ASCII, but differ in interpreter.
>>> value = 42 #ascii e >>> valu = 23 #cyrillic e, Python 2.x interpreter would raise a `SyntaxError` here >>> value 42
def square(x): """ . """ sum_so_far = 0 for counter in range(x): sum_so_far = sum_so_far + x return sum_so_far
Result (Python 2.x):
>>> square(10) 10
Shouldn't it have turned 100?
Note: if you cannot reproduce the result, try running the file mixed_tabs_and_spaces.py in the shell.
Explanation
square
is replaced by eight spaces and falls into a loop.Result (Python 3.x):
TabError: inconsistent use of tabs and spaces in indentation
one.
some_dict = {} some_dict[5.5] = "Ruby" some_dict[5.0] = "JavaScript" some_dict[5] = "Python"
Result:
>>> some_dict[5.5] "Ruby" >>> some_dict[5.0] "Python" >>> some_dict[5] "Python"
Python destroyed the existence of javascript?
Explanation
Immutable objects with the same values ​​in Python always get the same hashes.
>>> 5 == 5.0 True >>> hash(5) == hash(5.0) True
Note: objects with different values ​​can also get the same hash (this situation is called a hash collision).
some_dict[5] = "Python"
existing expression “JavaScript” is rewritten to “Python”, because Python recognizes 5
and 5.0
as identical keys of the dictionary some_dict
. array = [1, 8, 15] g = (x for x in array if array.count(x) > 0) array = [2, 8, 22]
Result:
>>> print(list(g)) [8]
Explanation
in
processed during the declaration, and the conditional clause is processed during the run time.array
is reassigned to the list [2, 8, 22]
, and since from 1
, 8
and 15
only the value of the counter 8
greater than 0
, the generator produces only 8
. x = {0: None} for i in x: del x[i] x[i+1] = None print(i)
Result:
0 1 2 3 4 5 6 7
Yes, it runs exactly eight times and stops.
Explanation:
list_1 = [1, 2, 3, 4] list_2 = [1, 2, 3, 4] list_3 = [1, 2, 3, 4] list_4 = [1, 2, 3, 4] for idx, item in enumerate(list_1): del item for idx, item in enumerate(list_2): list_2.remove(item) for idx, item in enumerate(list_3[:]): list_3.remove(item) for idx, item in enumerate(list_4): list_4.pop(idx)
Result:
>>> list_1 [1, 2, 3, 4] >>> list_2 [2, 4] >>> list_3 [] >>> list_4 [2, 4]
Do you know why the result was [2, 4]
?
Explanation:
Changing an object during its iteration is always a bad idea. It is better then to iterate a copy of the object, which list_3[:]
does.
>>> some_list = [1, 2, 3, 4] >>> id(some_list) 139798789457608 >>> id(some_list[:]) # Notice that python creates new object for sliced list. 139798779601192
The difference between del
, remove
and pop
:
del var_name
simply removes the var_name
binding of the local or global namespace (so list_1
remains unaffected).remove
removes the first matching value, not the specific index, causing ValueError
if there is no value.pop
removes the item with a specific index and returns it, causing IndexError
if an invalid index is specified.Why did it happen [2, 4]
?
1
from list_2
or list_4
, the contents of the lists become [2, 3, 4]
. The rest are shifted down, that is, 2
turns on index 0, 3
- on index 1. Since the next iteration will be performed with reference to index 1 (where we have 3
), 2
will be skipped. The same thing happens with every second item in the list. A similar example related to dictionaries in Python is beautifully explained on StackOverflow.Result:
>>> print("\\ some string \\") >>> print(r"\ some string") >>> print(r"\ some string \") File "<stdin>", line 1 print(r"\ some string \") ^ SyntaxError: EOL while scanning string literal
Explanation
r
, the backslash has no special meaning.This is not WTF, but only some cool things, and they need to be wary :)
def add_string_with_plus(iters): s = "" for i in range(iters): s += "xyz" assert len(s) == 3*iters def add_string_with_format(iters): fs = "{}"*iters s = fs.format(*(["xyz"]*iters)) assert len(s) == 3*iters def add_string_with_join(iters): l = [] for i in range(iters): l.append("xyz") s = "".join(l) assert len(s) == 3*iters def convert_list_to_string(l, iters): s = "".join(l) assert len(s) == 3*iters
Result:
>>> timeit(add_string_with_plus(10000)) 100 loops, best of 3: 9.73 ms per loop >>> timeit(add_string_with_format(10000)) 100 loops, best of 3: 5.47 ms per loop >>> timeit(add_string_with_join(10000)) 100 loops, best of 3: 10.1 ms per loop >>> l = ["xyz"]*10000 >>> timeit(convert_list_to_string(l, 10000)) 10000 loops, best of 3: 75.3 µs per loop
Explanation
+
to generate long strings: in Python, str
is immutable, so for each pair of concatenations, the left and right strings must be copied into a new string. If you concatenate four lines 10 characters long, then copy (10 + 10) + ((10 + 10) + 10) + (((10 + 10) + 10) +10) = 90 characters instead of 40. As you grow the number and size of rows, the situation worsens fourfold..format.
syntax .format.
or %
(but on short lines it works a little slower than +).''.join(iterable_object)
. >>> a = "some_string" >>> id(a) 140420665652016 >>> id("some" + "_" + "string") # Notice that both the ids are same. 140420665652016 # using "+", three strings: >>> timeit.timeit("s1 = s1 + s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100) 0.25748300552368164 # using "+=", three strings: >>> timeit.timeit("s1 += s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100) 0.012188911437988281
Explanation:
+=
faster +
more than two lines, because the first line (for example, s1
for s1 += s2 + s3
) is not destroyed until the line has been completely processed.Clause else
for loops . Typical example:
def does_exists_num(l, to_find): for num in l: if num == to_find: print("Exists!") break else: print("Does not exist")
Result:
>>> some_list = [1, 2, 3, 4, 5] >>> does_exists_num(some_list, 4)
Exists!
>>> does_exists_num(some_list, -1)
Does not exist.
Clause else
in exception handling . Example:
try: pass except: print("Exception occurred!!!") else: print("Try block executed successfully...")
Result:
Try block executed successfully...
Explanation:
else
is executed after the loop only when after all iterations there is no obvious break
.else
clause after a try
block is also called a completion clause, since the availability of an else
in a try
means that the try
block has been completed successfully.is
not what it isThis example is very widely known.
>>> a = 256 >>> b = 256 >>> a is b True >>> a = 257 >>> b = 257 >>> a is b False >>> a = 257; b = 257 >>> a is b True
Explanation:
Difference between is
and ==
is
operator checks that both operands refer to the same object (that is, it checks if they are identical to each other).==
compares the values ​​of the operands and checks for identity.is
used for equivalence of links, and ==
for equivalence of values. Explanatory example: >>> [] == [] True >>> [] is [] # These are two empty lists at two different memory locations. False
256
is an existing object, and 257
is not
When you start Python, numbers from -5
to 256
are placed in memory. They are used often, so it is advisable to keep them ready.
Quote from https://docs.python.org/3/c-api/long.html
The current implementation supports an array of integer objects for all numbers from –5 to 256, so that when you create an int from this range, you get a reference to an existing object. Therefore, it should be possible to change the value to 1. But I suspect that in this case the behavior of Python will be unpredictable. :-)
>>> id(256) 10922528 >>> a = 256 >>> b = 256 >>> id(a) 10922528 >>> id(b) 10922528 >>> id(257) 140084850247312 >>> x = 257 >>> y = 257 >>> id(x) 140084850247440 >>> id(y) 140084850247344
The interpreter was not so smart, and during execution y = 257
did not understand that we had already created an integer with the value 257
, therefore it creates another object in the memory.
a
and b
refer to the same object when initialized with the same value on the same line.
>>> a, b = 257, 257 >>> id(a) 140640774013296 >>> id(b) 140640774013296 >>> a = 257 >>> b = 257 >>> id(a) 140640774013392 >>> id(b) 140640774013488
a
and b
to 257
on one line, the Python interpreter creates a new object, while at the same time making a reference to it from the second variable. If we assign values ​​in different lines, the interpreter will not “know” that we already have 257
as an object..py
file, you will not see this behavior, because the file is compiled at once.is not ...
different from is (not ...)
>>> 'something' is not None True >>> 'something' is (not None) False
Explanation
is not
is a single binary operator whose behavior differs from the situation when is
and not
are used separately.is not
False
if variables on both sides of the statement point to the same object. Otherwise, it is true. funcs = [] results = [] for x in range(7): def some_func(): return x funcs.append(some_func) results.append(some_func()) funcs_results = [func() for func in funcs]
Result:
>>> results [0, 1, 2, 3, 4, 5, 6] >>> funcs_results [6, 6, 6, 6, 6, 6, 6]
If before adding some_func
in funcs
, the x
values ​​in each iteration were different, all functions returned 6.
//OR >>> powers_of_x = [lambda x: x**i for i in range(10)] >>> [f(2) for f in powers_of_x] [512, 512, 512, 512, 512, 512, 512, 512, 512, 512]
Explanation
funcs = [] for x in range(7): def some_func(x=x): return x funcs.append(some_func)
Result:
>>> funcs_results = [func() for func in funcs] >>> funcs_results [0, 1, 2, 3, 4, 5, 6]
one.
for x in range(7): if x == 6: print(x, ': for x inside loop') print(x, ': x in global')
Result:
6 : for x inside loop 6 : x in global
But x
not defined for a loop outside the scope.
2
# This time let's initialize x first x = -1 for x in range(7): if x == 6: print(x, ': for x inside loop') print(x, ': x in global')
Result:
6 : for x inside loop 6 : x in global
3
x = 1 print([x for x in range(5)]) print(x, ': x in global')
Result (on Python 2.x):
[0, 1, 2, 3, 4] (4, ': x in global')
Result (on Python 3.x):
[0, 1, 2, 3, 4] 1 : x in global
Explanation
“The syntax form[... for var in item1, item2, ...]
no longer supported for generating lists. Use[... for var in (item1, item2, ...)]
instead. Also note that list generation has different semantics: they are closer to syntactic sugar in relation to the generating expression inside thelist()
constructor, and, in particular, loop control variables no longer flow into the surrounding scope. ”
# Let's initialize a row row = [""]*3 #row i['', '', ''] # Let's make a board board = [row]*3
Result:
>>> board [['', '', ''], ['', '', ''], ['', '', '']] >>> board[0] ['', '', ''] >>> board[0][0] '' >>> board[0][0] = "X" >>> board [['X', '', ''], ['X', '', ''], ['X', '', '']]
But we did not assign three X, right?
Explanation
This visualization explains what happens in memory when the row
variable is initialized:
And when the board
initialized by multiplying the row
, this is what happens in the memory (each of the board[0]
, board[1]
and board[2]
elements is a reference to the same list specified in row
):
def some_func(default_arg=[]): default_arg.append("some_string") return default_arg
Result:
>>> some_func() ['some_string'] >>> some_func() ['some_string', 'some_string'] >>> some_func([]) ['some_string'] >>> some_func() ['some_string', 'some_string', 'some_string']
Explanation
[]
as an argument to some_func
, there was no default value for the default_arg
variable, so the function returned what was expected. def some_func(default_arg=[]): default_arg.append("some_string") return default_arg
Result:
>>> some_func.__defaults__ #This will show the default argument values for the function ([],) >>> some_func() >>> some_func.__defaults__ (['some_string'],) >>> some_func() >>> some_func.__defaults__ (['some_string', 'some_string'],) >>> some_func([]) >>> some_func.__defaults__ (['some_string', 'some_string'],)
None
as the default value, followed by checking whether any value is passed to the function that corresponds to this argument. Example: def some_func(default_arg=None): if not default_arg: default_arg = [] default_arg.append("some_string") return default_arg
one.
a = [1, 2, 3, 4] b = a a = a + [5, 6, 7, 8]
Result:
>>> a [1, 2, 3, 4, 5, 6, 7, 8] >>> b [1, 2, 3, 4]
2
a = [1, 2, 3, 4] b = a a += [5, 6, 7, 8]
Result:
>>> a [1, 2, 3, 4, 5, 6, 7, 8] >>> b [1, 2, 3, 4, 5, 6, 7, 8]
Explanation
a += b
behaves differently than a = a + b
a = a + [5,6,7,8]
generates a new object and assigns a to it, leaving b
unchanged.a + =[5,6,7,8]
actually converted (mapped to) into the extend function, which works with the object in such a way that a
and b
still point to the same object that was changed in place. some_tuple = ("A", "tuple", "with", "values") another_tuple = ([1, 2], [3, 4], [5, 6])
Result:
>>> some_tuple[2] = "change this" TypeError: 'tuple' object does not support item assignment >>> another_tuple[2].append(1000) #This throws no error >>> another_tuple ([1, 2], [3, 4], [5, 6, 1000]) >>> another_tuple[2] += [99, 999] TypeError: 'tuple' object does not support item assignment >>> another_tuple ([1, 2], [3, 4], [5, 6, 1000, 99, 999])
But the tuples are immutable, is not it ...
Explanation
An immutable sequence type object cannot change after its creation. If an object contains references to other objects, then these objects can be editable and can be changed. However, a collection of objects that is directly referenced by an immutable object cannot be changed.
+=
changes the list in place. Item assignment does not work, but when an exception occurs, the item has already been changed in place. a = 1 def some_func(): return a def another_func(): a += 1 return a
Result:
>>> some_func() 1 >>> another_func() UnboundLocalError: local variable 'a' referenced before assignment
Explanation
another_func
local scope variable, but it was not previously initialized in the same scope that throws the error.global
to modify the external scope variable a
to another_func
. def another_func() global a a += 1 return a
Result:
>>> another_func() 2
e = 7 try: raise Exception() except Exception as e: pass
Result (Python 2.x):
>>> print(e) # prints nothing
Result (Python 3.x):
>>> print(e) NameError: name 'e' is not defined
Explanation
Source of
If it assigns an exception to the target as
, it is cleared at the end of the except
clause. As if
except E as N: foo
was converted to
except E as N: try: foo finally: del N
This means that an exception must be assigned to another name, so that you can refer to it after the except
clause. , (traceback), (reference cycle), , .
except
. , . : def f(x): del(x) print(x)
x = 5
y = [5, 4, 3]
**:**
f (x)
UnboundLocalError: local variable 'x' referenced before assignment
f(y)
UnboundLocalError: local variable 'x' referenced before assignment
x
five
y
[5, 4, 3]
- Python 2.x e `Exception()`, .
(Python 2.x):
>>> e Exception() >>> print e # Nothing is printed!
def some_func(): try: return 'from_try' finally: return 'from_finally'
Result:
>>> some_func() 'from_finally'
try
try…finally
return
, break
continue
, finally
.return
. finally
, return
, finally
, . True = False if True == False: print("I've lost faith in truth!")
Result:
I've lost faith in truth!
bool
( 0 false 1 true). True
, False
bool
, - True
False
— . >>> True is False == False False >>> False is False is False True >>> 1 > 0 < 1 True >>> (1 > 0) < 1 False >>> 1 > (0 < 1) False
https://docs.python.org/2/reference/expressions.html#not-in
, a, b, c, ..., y, z — , op1, op2, ..., opN — , a op1 b op2 c… y opN z op1 b b op2 c … y opN z, , .
, a == b == c 0 <= x <= 100
.
False is False is False
(False is False) and (False is False)
True is False == False
True is False and False == False
, ( True is False
) False
, False
.1 > 0 < 1
1 > 0
and 0 < 1
, True
.(1 > 0) < 1
True < 1
>>> int(True) 1 >>> True + 1 #not relevant for this example, but just for fun 2
1 < 1
False
one.
x = 5 class SomeClass: x = 17 y = (x for i in range(10))
Result:
>>> list(SomeClass.y)[0] 5
2
x = 5 class SomeClass: x = 17 y = [x for i in range(10)]
(Python 2.x):
>>> SomeClass.y[0] 17
(Python 3.x):
>>> SomeClass.y[0] 5
some_list = [1, 2, 3] some_dict = { "key_1": 1, "key_2": 2, "key_3": 3 } some_list = some_list.append(4) some_dict = some_dict.update({"key_4": 4})
Result:
>>> print(some_list) None >>> print(some_dict) None
, / (sequence/mapping objects) list.append
, dict.update
, list.sort
. ., None
. — , ( )
WTF, , Python . .
a = float('inf') b = float('nan') c = float('-iNf') #These strings are case-insensitive d = float('nan')
Result:
>>> a inf >>> b nan >>> c -inf >>> float('some_other_string') ValueError: could not convert string to float: some_other_string >>> a == -c #inf==inf True >>> None == None # None==None True >>> b == d #but nan!=nan False >>> 50/a 0.0 >>> a/a nan >>> 23 + b nan
'inf'
'nan'
— ( ). float
, , , «» « ».
one.
class A: x = 1 class B(A): pass class C(A): pass
Result:
>>> Ax, Bx, Cx (1, 1, 1) >>> Bx = 2 >>> Ax, Bx, Cx (1, 2, 1) >>> Ax = 3 >>> Ax, Bx, Cx (3, 2, 3) >>> a = A() >>> ax, Ax (3, 3) >>> ax += 1 >>> ax, Ax (4, 3)
2
class SomeClass: some_var = 15 some_list = [5] another_list = [5] def __init__(self, x): self.some_var = x + 1 self.some_list = self.some_list + [x] self.another_list += [x]
Result:
>>> some_obj = SomeClass(420) >>> some_obj.some_list [5, 420] >>> some_obj.another_list [5, 420] >>> another_obj = SomeClass(111) >>> another_obj.some_list [5, 111] >>> another_obj.another_list [5, 420, 111] >>> another_obj.another_list is SomeClass.another_list True >>> another_obj.another_list is some_obj.another_list True
+=
. . some_list = [1, 2, 3] try: # This should raise an ``IndexError`` print(some_list[4]) except IndexError, ValueError: print("Caught!") try: # This should raise a ``ValueError`` some_list.remove(4) except IndexError, ValueError: print("Caught again!")
(Python 2.x):
Caught! ValueError: list.remove(x): x not in list
(Python 3.x):
File "<input>", line 3 except IndexError, ValueError: ^ SyntaxError: invalid syntax
except
. — , . Example: some_list = [1, 2, 3] try: # This should raise a ``ValueError`` some_list.remove(4) except (IndexError, ValueError), e: print("Caught again!") print(e)
(Python 2.x):
Caught again! list.remove(x): x not in list
(Python 3.x):
File "<input>", line 4 except (IndexError, ValueError), e: ^ IndentationError: unindent does not match any outer indentation level
as
. Example: some_list = [1, 2, 3] try: some_list.remove(4) except (IndexError, ValueError) as e: print("Caught again!") print(e)
Result:
Caught again! list.remove(x): x not in list
from datetime import datetime midnight = datetime(2018, 1, 1, 0, 0) midnight_time = midnight.time() noon = datetime(2018, 1, 1, 12, 0) noon_time = noon.time() if midnight_time: print("Time at midnight is", midnight_time) if noon_time: print("Time at noon is", noon_time)
Result:
('Time at noon is', datetime.time(12, 0)) The midnight time is not printed.
Python 3.5 datetime.time
False
, UTC. - if obj
: , obj
null «».
one.
# A simple example to count the number of boolean and # integers in an iterable of mixed data types. mixed_list = [False, 1.0, "some_string", 3, True, [], False] integers_found_so_far = 0 booleans_found_so_far = 0 for item in mixed_list: if isinstance(item, int): integers_found_so_far += 1 elif isinstance(item, bool): booleans_found_so_far += 1
Result:
>>> booleans_found_so_far 0 >>> integers_found_so_far 4
2
another_dict = {} another_dict[True] = "JavaScript" another_dict[1] = "Ruby" another_dict[1.0] = "Python"
Result:
>>> another_dict[True] "Python"
int
>>> isinstance(True, int) True >>> isinstance(False, int) True
True
1
, False
— 0
. >>> True == 1 == 1.0 and False == 0 == 0.0 True
Python- .
t = ('one', 'two') for i in t: print(i) t = ('one') for i in t: print(i) t = () print(t)
Result:
one two o n e tuple()
t = ('one',)
t = 'one'
, ( ), , t
str
.tuple
. some_string = "wtf" some_dict = {} for i, some_dict[i] in enumerate(some_string): pass
Result:
>>> some_dict # An indexed dict is created. {0: 'w', 1: 'f', 2: 'f'}
for
Python : for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite]
exprlist
— . , {exprlist} = {next_value}
. @tukkek:
for i in range(4): print(i) i = 10
Result:
0 1 2 3
, ?
i = 10
- Python. , ( range(4)
), ( i
).enumerate(some_string)
i
( A
) some_string
. ( ) i
some_dict
. : >>> i, some_dict[i] = (0, 'w') >>> i, some_dict[i] = (1, 't') >>> i, some_dict[i] = (2, 'f') >>> some_dict
x = True y = False
Result:
>>> not x == y True >>> x == not y File "<input>", line 1 x == not y ^ SyntaxError: invalid syntax
==
, not
.not x == y
not (x == y)
, not (True == False)
, True
.x == not y
SyntaxError
, (x == not) y
, x == (not y)
, .not
— not in
( ==
not in
), not
in
, SyntaxError.PiaFraus .
a, b = a[b] = {}, 5
Result:
>>> a {5: ({...}, 5)}
(target_list "=")+ (expression_list | yield_expression)
and:
( , , ) , .
+
(target_list "=")+
, . a
, b
a[b]
( , , {}
, 5
).{}
, 5
a
, b
, a = {}
b = 5
.a
{}
, .a[b]
( , , a
b
. , a
{}
b
5
).5
({}, 5)
, ( {...}
, a
). : >>> some_list = some_list[0] = [0] >>> some_list [[...]] >>> some_list[0] [[...]] >>> some_list is some_list[0] [[...]]
( a[b][0]
, a
)
a, b = {}, 5 a[b] = a, b
, a[b][0]
,
a >>> a[b][0] is a True
Join()
— (string operation), (list operation). .join()
— , (, , ). , . , API list
.[] = ()
( tuple
list
)'a'[0][0][0][0][0]
, Python .3 --0-- 5 == 8 --5 == 5
True
. import dis exec(""" def f():* """ + """ """.join(["X"+str(x)+"=" + str(x) for x in range(65539)])) f() print(dis.dis(f))
>>> some_list = [1, 2, 3, 4, 5] >>> some_list[111:] []
! :) CONTRIBUTING.md .
• https://www.youtube.com/watch?v=sH4XF6pKKmk
• https://www.reddit.com/r/Python/comments/3cu6ej/what_are_some_wtf_things_about_python
• https://sopython.com/wiki/Common_Gotchas_In_Python
• https://stackoverflow.com/questions/530530/python-2-x-gotchas-and-landmines
• https://stackoverflow.com/questions/1011431/common-pitfalls-in-python ( StackOverflow , Python.)
Source: https://habr.com/ru/post/337364/
All Articles