# Function call¶

What happens if you call a function? A familiar example:

In :
def square(L):
new_L = []
for i in L:
new_L.append(i*i)
return new_L

numbers = [5, 1, 8]
squared_numbers =  square(numbers)

print numbers
print squared_numbers

[5, 1, 8]
[25, 1, 64]


Here we make a new list out of the squared values of the original list.

What happens if you square the elements in the original L directly?

In :
def square(L):
for i in range(len(L)):
L[i] = L[i] * L[i]
return L

numbers = [5, 1, 8]
squared_numbers =  square(numbers)

print numbers
print squared_numbers

[25, 1, 64]
[25, 1, 64]


The original numbers list has changed also. An other example:

In :
def square(n):
n = n * n
return n

number = 5
squared_number = square(number)

print number
print squared_number

5
25


Now the original number is unchanged. To understand this mechanism, we have to go deeper.

## Value vs. reference¶

Some data types are stored as a concrete value, like int(5), float(3.14). They are the primitive data types.

• int
• bool
• float

Other data types are stored as a reference to an actual memory location.

• list
• dict
• str
• any other class

In this case: l = [1,2,3] the variable l is a reference to the actual triplet of numbers.

## Copy¶

All of this becomes interesting when you want to copy an object.

If you make a copy of a primitive type, then a new number is created which is the copy of the original. Modifying the copy does not effect the original one.

If you copy of a reference, only the reference itself is copied and both the original and the new reference refer to the same thing. In this case modifying the copy modifies the original too.

### Primitive¶

In :
x = 5
y = x
y = 6
print x, y

5 6


### Reference¶

In :
L1 = [1, 5, 2]
L2 = L1

L2.sort()
print L1
print L2

[1, 2, 5]
[1, 2, 5]


You can force a copy of a list in this way:

In :
L = [1,2,3]
M = L[:]
M = 9
print L
print M

[1, 2, 3]
[1, 9, 3]


But it's more complicated in deeper structures. Here I only copy the outermost list, not the inner lists.

In :
M1 = [[1, 2], [3, 4]]
M2 = M1[:]

M2 = 5

print M1
print M2

[[5, 2], [3, 4]]
[[5, 2], [3, 4]]

In :
M1 = [[1, 2], [3, 4]]
M2 = M1[:]

M2 = [5, 2]

print M1
print M2

[[1, 2], [3, 4]]
[[5, 2], [3, 4]]


To copy this properly, you need to copy it recursively. But you have a function for that.

In :
import copy

M1 = [[1, 2], [3, 4]]
M2 = copy.deepcopy(M1)

M2 = 5

print M1
print M2

[[1, 2], [3, 4]]
[[5, 2], [3, 4]]


## Pass argument by value¶

In python (and many other languages) the function gets a copy of its arguments.

But this means different things in case of primitive and reference types.

In the second square example the list L was a copy of a reference, therefore we were able to modify the original numbers also.

In the third square example the number was a primitive type, so the copy n does not interfere with the original one.

If you don't want to modify the original variables then it is a good practice to deepcopy them inside a function and modify the copies. Or create a whole new variable like in the first square example.

Not that modifying the reference itself does not change the refered object, only that the reference will refer to some new object.

In :
L = [1,2,5]
def erase(L):
L = []

erase(L)
print L

[1, 2, 5]


This would be a correct erase:

In :
def erase(L):
while len(L) > 0:
del L

erase(L)
print L

[]


It is also a bit more tricky for immutable objects. Even if you get a reference to a string, the str type is immutable, meaning that you cannot change its value, even if it is a reference.

In :
def MakeCapital(s):
s = s.upper() # it works neither inside a function nor outside

a = "abc"
MakeCapital(a)
print a

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-12-eeb5b88001fc> in <module>()
3
4 a = "abc"
----> 5 MakeCapital(a)
6 print a

<ipython-input-12-eeb5b88001fc> in MakeCapital(s)
1 def MakeCapital(s):
----> 2     s = s.upper() # it works neither inside a function nor outside
3
4 a = "abc"
5 MakeCapital(a)

TypeError: 'str' object does not support item assignment

## Extra function arguments¶

### Optional¶

In :
def passed(students, limit):
L = []
for name in students:
if students[name] >= limit:
L.append(name)
return L

students = {'ABCDEF': 50, 'BATMAN': 23, '123ABC': 67}
print passed(students, 40)

['ABCDEF', '123ABC']


You can set the limit parameter to default 40

In :
def passed(students, limit=40):
L = []
for name in students:
if students[name] >= limit:
L.append(name)
return L

students = {'ABCDEF': 50, 'BATMAN': 23, '123ABC': 67}
print passed(students)
print passed(students, 60)

['ABCDEF', '123ABC']
['123ABC']


You can specify more optional parameters but only from the right.

In :
def passed(students, limit=40, sort=True):
L = []
for name in students:
if students[name] >= limit:
L.append(name)
if sort:
return sorted(L)
else:
return L

students = {'ABCDEF': 50, 'BATMAN': 23, '123ABC': 67}
print passed(students)
print passed(students, 40, False)

['123ABC', 'ABCDEF']
['ABCDEF', '123ABC']


The following is wrong, because you set the limit to False not the sort parameter.

In :
print passed(students, False)

['123ABC', 'ABCDEF', 'BATMAN']


#### Pass parameter by name¶

You can substitute the optional parameters by name, not only by place.

In :
print passed(students, sort=False)
print passed(students, sort=False, limit=40)

['ABCDEF', '123ABC']
['ABCDEF', '123ABC']

In :
print "P({x}, {y})".format(y=3, x=4)

P(4, 3)


Lets take a function that multiples numbers.

In :
def product(L):
result = 1
for i in L:
result *= i
return result

print product([1,2,3])

6


You can make a multi-variate function instead of a function with one list parameter. You can call this function with either 0,1,2 or 3 parameters.

In :
def product2(x=1, y=1, z=1):
return x*y*z

print product2()
print product2(1)
print product2(1, 2)
print product2(1, 2, 3)

1
1
2
6


Instead, you can make a function with arbitrary many parameters. variadic function

See that the only difference is the *

In :
def product3(*L):
result = 1
for i in L:
result *= i
return result

print product3(1,2,3)
print product3(1, 2, 3, 4, 5, 6)

6
720


In this case the parameters are copied into a tuple, that can be any long.

In :
def variadic(*x):
return type(x)


<type 'tuple'>


Let's say you have a list but you wanto to multiply them with a variadic function.

This is not correct:

In :
L = [1, 2, 3]
print product3(L)

[1, 2, 3]

In :
# Correct
print product3(*L)

# equivalent to this
print product3(L, L, L)

6
6


## Scope¶

wikipedia

There can be more than one variable with the same name, but it matters where.

In :
def function(L):
# i is different
for i in L:
if i <> 0:
return True
return False
i = [0, 1, -1]
print function(i)

True

In :
def something(L):
# i is mixed up
i = 0
for i in L:
i = i+i
return i
print something([1, 2, 3])

6

In :
def something2(L):
i = 0
for j in L:
i = i+j
return i

# i is unchanged outside of the function
i = 10
print something2([1, 2, 3])
print i

6
10


A variable defined inside a function is local to that function and does not interfere with the same named variable outside of the function.

If you call that function many times, the values between runs are not connected in any way.

If you define a variable outside of any function, then it can be seen anywhere in the code: global.

In :
i = 10
def f(x):
# i = 0 # this would create a local i
print i

f(None)

10


Mind that an if branch can introduce a variable and some variables can be present in some cases and undefined in other cases.

In :
def f(x):
if x:
i = 0
return i

print f(True)
print f(False)

0

---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-29-799dc34d37e2> in <module>()
5
6 print f(True)
----> 7 print f(False)

<ipython-input-29-799dc34d37e2> in f(x)
2     if x:
3         i = 0
----> 4     return i
5
6 print f(True)

UnboundLocalError: local variable 'i' referenced before assignment
In [ ]: