Sneaky Python

Some Python gotchas and interesting details for functional programmers.

In [1]:
# None is the sole *value* of the class NoneType and None == None.
a = None
b = None
assert a == b
assert None == None
assert a is b
assert None is None
print(id(a), id(b), id(None))
print(type(None))
140489726610016 140489726610016 140489726610016
<class 'NoneType'>
In [2]:
# Interesting (but probably useless) implementation details:
a = 'tony'
b = 'tony'
assert 'tony' is 'tony'
assert a is b

a = 7
b = 7
assert 7 is 7
assert a is b

a = 7.0
b = 7.0
assert 7.0 is 7.0
assert a is not b
In [3]:
# = binds to an existing object, it does not create a new copy of it
a = 1
b = a
assert b is a

a = (1, 2)
b = a
assert b is a

a = [1, 2]
b = a
assert b is a

# In fact, if you think = as a function, it does different things on different occasions:
# a = [] : use a to reference []
# b = a : use b to reference the object that a references to
# But you can always argue that: a "deep-references" the RHS
In [4]:
# Again.
a = []
b = a
b.append(1)
print(a, b)
assert a == b
assert a is b
[1] [1]
In [5]:
# Again.
# And value of the global x is not copied as the argument of f.
def f(x):
    y = x
    print(('id of y', id(y)))
    return y

# x is immutable
x = 1
print(('id of x', id(x)))
y = f(x)
print(x, y)
assert x is y
('id of x', 140489726757504)
('id of y', 140489726757504)
1 1
In [6]:
# Again.
def f(x):
    y = x
    print(('id of y', id(y)))
    y.append(1)
    return y

# x is mutable
x = []
print(('id of x', id(x)))
y = f(x)
print(x, y)
assert x is y
('id of x', 140488930292232)
('id of y', 140488930292232)
[1] [1]
In [7]:
# Just because it got class, doesn't mean it will make a difference.
class C:
    def __init__(self, a):
        self.a = a
        print(('id of self.a', id(self.a)))
        self.a.append(1)
        
a = []
print(('id of      a', id(a)))
c = C(a)
print(a, c.a)
assert a is c.a
('id of      a', 140488941211144)
('id of self.a', 140488941211144)
[1] [1]
In [8]:
# Don't use a mutable object as the default argument
# - unless you want to do something like memorization.
def f(a=[]):
    a.append(1)
    return (id(a), a)

print(f())
print(f())
print(f(a=[2, 3]))
print(f())
(140488930292040, [1])
(140488930292040, [1, 1])
(140488930292296, [2, 3, 1])
(140488930292040, [1, 1, 1])
In [9]:
# But function-as-default-value seems OK, but I'm not sure about this.
evil = []

def f():
  global evil
  return evil

f.data = []
f.atad = 1

def h1(g=f):
  print(f'id of the function being called: {id(g)}')
  return (g(), g.data, g.atad)

f.data.append(1)
f.atad = 2

def h2(g=None):
  if g is None:
    g = f
  print(f'id of the function being called: {id(g)}')
  return (g(), g.data, g.atad)

evil.append('evil')
print(h1(), h2())
id of the function being called: 140488941396448
id of the function being called: 140488941396448
(['evil'], [1], 2) (['evil'], [1], 2)
In [10]:
# Names in lambdas are looked up when the lambda is called,
# pretty acceptable and harmless, right? See the next cell.
try:
    del c
except:
    pass
f = lambda x: c * x
c = 3
print(f(2))
c = 4
print(f(2))
6
8
In [11]:
fs = [lambda x: c * x for c in range(2, 5)]
for f in fs:
    print(f(2))
print('-')
fs = [lambda x, c=c: c * x for c in range(2, 5)]
for f in fs:
    print(f(2))
8
8
8
-
4
6
8
In [12]:
import numpy as np
In [13]:
# np.array can be configured to return the same or a different object
xs = np.array([[11, 12, 13], [21, 22, 23]])
ys = np.array(xs, copy=True)
print(id(xs), id(ys))
print(xs)
print(ys)

print('-')

xs = np.array([[11, 12, 13], [21, 22, 23]])
ys = np.array(xs, copy=False)
print(id(xs), id(ys))
xs[0, 0] = 0
print(xs)
print(ys)
140488930327328 140488930328128
[[11 12 13]
 [21 22 23]]
[[11 12 13]
 [21 22 23]]
-
140488930328448 140488930328448
[[ 0 12 13]
 [21 22 23]]
[[ 0 12 13]
 [21 22 23]]
In [14]:
# Slicing references to the original array
xs = np.array([[11, 12, 13], [21, 22, 23]])
ys = xs[0]
print(xs)
print(ys)
print('-')

# changing the slice changes the original
ys[0] = 0
print(xs)
print(ys)
print('-')

# changing the original changes the slice
xs = np.array([[11, 12, 13], [21, 22, 23]])
ys = xs[0]
xs[0, 0] = 0
print(xs)
print(ys)
[[11 12 13]
 [21 22 23]]
[11 12 13]
-
[[ 0 12 13]
 [21 22 23]]
[ 0 12 13]
-
[[ 0 12 13]
 [21 22 23]]
[ 0 12 13]
In [15]:
# To be continued