Python For Quantum Mechanics#

Week 3: Functions and Lambda Functions#

from IPython.display import YouTubeVideo
YouTubeVideo('h-TmiynfpCs',width=700, height=400)

Functions are useful to compactify code. Rather than writing 20 lines of code to do a specific task with certain values for the variables used, instead we define a function. The function performs the task taking in arbitrary arguments for the variables used. Here’s how we do it:

How To Write A Function And Return Values#

Notice the use of def, the name of the function modulus, the parentheses () which contain the arguments, and the colon :.

The string we have inside the function is called a docstring, and is used to describe what the function does and what arguments it takes in. It is not necessary, but is good practice.

def modulus(x,y):
    '''Docstring: This prints the modulus of two numbers \nhello hi right'''
    print((x**2 + y**2)**0.5)

Now we call the function using it’s name, followed by inputting the arguments.

modulus(3,4)
print(type(modulus))
5.0
<class 'function'>

If a function gives some value we would like to use, rather than just printing a result, we use the return statement as follows

def modulus(x,y):
    '''Docstring: This returns the modulus of two numbers'''
    return((x**2 + y**2)**0.5)

We usually assign the returned value to a variable like so

Mod = modulus(1,1)
Mod = Mod*3
print(Mod)

#Or you can treat the returned value like you would it's corresponding data type
print(3*modulus(1,1))#Here the returned value is not assigned to a variable but can still be acted upon by functions
4.242640687119286
4.242640687119286

We can also see the docstring of a function by using the help() function

help(modulus)
Help on function modulus in module __main__:

modulus(x, y)
    Docstring: This returns the modulus of two numbers

Returning Multiple Values#

L = [0,1,2,3,4,5,6,7,8,9,10]

def describe_list(List):
    '''This function takes in a list and returns values that describe said list'''
    len_value = len(List)
    max_value = max(List)
    min_value = min(List)
    sum_value = sum(List)
    return len_value, max_value, min_value, sum_value

values = describe_list(L)
print(values)
print(type(values))
(11, 10, 0, 55)
<class 'tuple'>

As we see, returning multiple values gives us a tuple of those values. Just like we assigned a tuple to multiple variables before, we can do the same when calling this function

len_L,max_L,min_L,sum_L = describe_list(L)

print(len_L)
print(type(len_L))

print(sum_L)
print(type(sum_L))
11
<class 'int'>
55
<class 'int'>

No Arguments#

A function doesn’t actually have to take in arguments, for example:

def PrintingPress():
    print("I am a futuristic printing press")

We call it without inserting arguments

PrintingPress()
I am a futuristic printing press

Implicit/Default Arguments#

Often times, some arguments carry the same value and on the rare occasion require change. We can take advantage of this using implicit arguments, written as follows

def modulus3d(x,y,z=0):
    '''Docstring: This returns the modulus of three numbers'''
    return((x**2 + y**2 + z**2)**0.5)

We set the argument to a commonly used value in the parantheses. We can now call this function without specifying the third argument “z”.

Mod = modulus3d(1,1)
print(Mod)
1.4142135623730951

If we want to change the value of this implicit argument, we simply specify it when we call the function.

Mod = modulus3d(1,1,1)
print(Mod)
1.7320508075688772

When using default arguments, you must be wary of using mutable objects like lists. Lets illustrate this in the following example.

def append_if(x,L=[]):
    if  x<10:
        L.append(x)
    return L

print(append_if(6))
print(append_if(3))
print(append_if(12))
print(append_if(4,[]))
print(append_if(7))
[6]
[6, 3]
[6, 3]
[4]
[6, 3, 7]

By not specifying the second argument, the same list will appear in future calls of the function except when we actually specify the argument. This can be a problem, in the definition of the function we want the list to be empty by default, but this is ruined for future calling. Let’s see how we can fix this

def append_if(x,L=None):
    if not L:
        L = []
    if  x<10:
        L.append(x)
    return L

print(append_if(6))
print(append_if(3))
print(append_if(12))
print(append_if(4,[]))
print(append_if(7))
[6]
[3]
[]
[4]
[7]

Keyword Arguments#

It is not necessary to enter the arguments in the correct order when calling a function, so long as we assign them to the correct variable. For example

def xyz_expression(x,y,z):
    return 2*x**2 - y + z/2.

print(xyz_expression(y=1.,z=-8.,x=3.))
13.0

Unknown Amount Of Arguments, *args#

You may need to define a function in which want to input varying numbers of arguments. To do this, we precede the argumentsw with * For example

#Here args represent the individual cost of various items purchased
def grocery_change(money=100,*args):
    for i in args:
        money = money - i
    return money

print(grocery_change(100,20,40,35))
print(grocery_change(100,30,40,3,9,14))
print(grocery_change())

print(grocery_change(20,10))#Notice here, money is set to 20
5
4
100
10

**kwargs#

This is similar to *args except it is used for an unknown amount of keyword arguments or the inputting of a dictionary. Recall, items() is a dictionary method used to display a dictionary’s key, value pairs.

def kw_demo(**kwargs):
    for key, value in kwargs.items():
        print("key: {}, value: {}".format(key,value))
kw_demo(Name="Conor",Age="24",Occupation="Quantum Computational Scientist")
key: Name, value: Conor
key: Age, value: 24
key: Occupation, value: Quantum Computational Scientist
Job_Details = {"Name":"Conor","Age":24,"Occupation":"Quantum Computational Scientist"}
kw_demo(**Job_Details)#Notice the use of ** for the dictionary
key: Name, value: Conor
key: Age, value: 24
key: Occupation, value: Quantum Computational Scientist

Global And Local Variables#

Global variables are defined outside of functions. Local Variables are defined inside functions. We can use gloabl variables both outside and inside a function. However local variable can only be used inside a function. For example

def subtractor(a):
    b = 4
    return a-b

a=10
print(subtractor(a))
print(b)
6
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[21], line 7
      5 a=10
      6 print(subtractor(a))
----> 7 print(b)

NameError: name 'b' is not defined

Here a is a global variable and b is a local variable. When we attempt to use b outside of the function we get en error. However, we can define b as global inside the function by making the declaration global b, in the following way

def subtractor(a):
    global b
    b = 4
    return a-b

a=10
print(subtractor(a))
print(b)
6
4

Lambda Functions#

Lambda functions are compactified, unnamed functions that are very commonly used in cojunction with lists.

y = lambda x:x**2 + x +2

We call it as follows

print(y(4))
22

The map() Function#

This map() function is useful for manipulating lists. It inputs each element in the list into a lambda function.

L = [1,2,3,4,5,6,7,8,9,10]

M = map(lambda x:x**2,L)
print(type(M))

M = list(M) #Cast as a list
print(M)
<class 'map'>
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

Observe what happens when we use an inequality.

print(M)

M = map(lambda x:x<=50,M)
print(type(M))
M = list(M)

print(M)
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
<class 'map'>
[True, True, True, True, True, True, True, False, False, False]

We can also also add more arguments, or lists, allowing us to perform various operations on multiple lists.

print(L)
print(M)

S = map(lambda x,y:x*y,L,M)
S = list(S)

print(S)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[True, True, True, True, True, True, True, False, False, False]
[1, 2, 3, 4, 5, 6, 7, 0, 0, 0]

We just multiplied elements of a list together. We can re-interpret lists as matrices and perform fundamental matrix operations using lambda functions. We will later see that the Numpy package greatly simplifies this by giving us a library to manipulate lists/arrays/matrices.

The map() function is not only constrained to using lambda functions. We can also use other built-in functions.

Float_List = map(float,S)
Float_List = list(Float_List)

print(Float_List)
[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 0.0, 0.0, 0.0]

We have converted each element in our list to a float.

The filter() Function#

The filter() function is useful for eliminating particular elements from lists.

M = [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
print(M)

M = filter(lambda x:x<=50,M)
print(type(M))
M = list(M)

print(M)
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
<class 'filter'>
[1, 4, 9, 16, 25, 36, 49]

This is the same as using inequalities with the map() above, except the true values are returned instead.