Part 1: Functions and Lambda Functions#
In this part, we will explore functions and lambda functions in Python. Functions are blocks of code, fundamental in programming, that perform specific tasks and can be reused throughout a program. They help in organizing code, improving readability, and reducing redundancy.
We can define our own functions to encapsulate logic and operations, making our code modular and easier to maintain. It is also very common to use Lambda functions for short, throwaway functions that are not reused elsewhere in the code. Lambda functions are a way to create small, anonymous functions in a single line of code on the fly.
Example:
# Regular function
def add(a, b):
return a + b
print(add(2, 3)) # Output: 5
# Lambda function
add_lambda = lambda a, b: a + b
print(add_lambda(2, 3)) # Output: 5
The main difference in syntax between functions and lambda functions is that functions are defined in blocks of code, starting with the def
keyword and can contain multiple expressions and statements. While lambda functions are usually one single line of code, they are defined using the lambda
keyword and can only contain a single expression.
1.1 Defining Regular Functions#
Functions are blocks of reusable code designed to perform a specific task, which you can call and reuse in different parts of the code, as many times as you need. They help in breaking down complex problems into smaller, manageable pieces. They allow for modularity and reusability.
They 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 that we can call with different arguments and use it multiple times. They also help in improving code readability, by giving meaningful names to blocks of code, making it easier to understand what the code does at a glance, and consistency, by avoiding code duplication and potential mistakes of typos, variable names, etc.
If you remember from last week, algorithms are step-by-step procedures or formulas for solving a problem. Functions can be thought of as a way to implement pieces of an algorithm in the code, connected through control flow structures.
Note
Many programming languages, including Python, have built-in functions that perform common tasks. Examples: print()
, len()
, and type()
. You can also import functions from libraries and modules to extend the functionality of your code as we will see in the next session.
However they also allow you to define your own functions, where you can define entirely its behavior, arguments, and return values. Which is the main potential of computer programming.
1.1.1 Function Syntax#
In Python, a function is defined using the def
keyword, followed by the function name, parentheses ()
, and a colon :
. Remember the importance of syntax and indentation. The function body always needs to be indented below the definition line.
Syntax:
# Definition of functions always in the beginning of the code
def function_name([parameters]):
<block>
[return <expression>]
# Calling the function somewhere else in the code
function_name([arguments])
Note
There is a subtle difference between parameters and arguments: Parameters are the variables defined in the function definition, while arguments are the actual values passed to the function when it is called.
def add(a, b): # a and b are parameters
return a + b
result = add(5, 3) # 5 and 3 are arguments
Note a few things:
The function name should be descriptive and follow standard naming conventions (lowercase letters and underscores).
Parameters are optional. A function can have zero or more parameters.
The
return
statement is also optional. If a function does not have a return statement, it returnsNone
by default.Indentation is crucial in Python, as it defines the scope of the function body.
Functions are called by their name, followed by parentheses, and you can pass arguments to the function if it has parameters.
Functions can be defined anywhere in your code, but they must be defined before they are called. A common practice is to define functions at the beginning of your script or module.
Functions can be nested, meaning you can define a function inside another function.
1.1.2 Function Arguments#
As we have seen, functions can take arguments, which are values passed to the function when it is called.
There are several types of arguments:
Positional Arguments: The most common type, where the order of arguments matters. In the function definition, parameters are defined in a specific order, separated by commas. When calling the function, you must provide arguments in the same order as the parameters were defined. Also the number of arguments must match the number of parameters defined in the function.
Syntax:
def function_name(param1, param2): <block> function_name(arg1, arg2)
Keyword Arguments: Arguments that are passed by explicitly specifying the parameter name. When calling a function, you can specify the parameter names along with their corresponding values. This allows you to pass arguments in any order, as long as you specify the parameter names. Still, the number of arguments must match the number of parameters defined in the function.
Syntax:
def function_name(param1, param2): <block> function_name(param2=arg2, param1=arg1)
Default Arguments: Arguments that assume a default value if a value is not provided. In this case, when defining the function, you can give default values to parameters. If you, (or the user) do not provide a value for that parameter, the default value will be used. If a value is provided, it overrides the default value. Default arguments must come after positional arguments in the function definition. As well as in the previous cases, the number of arguments must match the number of parameters defined in the function.
Syntax:
def function_name(param1, param2=default_value): <block> function_name(arg1) # Uses default value for param2 function_name(arg1, arg2) # Overrides default value for param2
Variable-length Arguments: Arguments that allow you to pass a variable number of arguments to a function. We use
*args
for non-keyword variable-length arguments and**kwargs
for keyword variable-length arguments.Syntax:
def function_name(*args): <block> def function_name(**kwargs): <block> function_name(arg1, arg2, arg3) # For *args function_name(key1=value1, key2=value2) # For **kwargs
From the following examples, which type of arguments is being used?
def calculate_area(length, width):
return length * width
area = calculate_area(5, 3)
print(area) # Output: 15
def introduce(name, age):
print(f"My name is {name} and I am {age} years old.")
introduce(age=25, name="Alice") # Output: My name is Alice and I am 25 years old.
def sum_all(*args):
return sum(args)
# Calling the function
total = sum_all(1, 2, 3, 4, 5)
print(total) # Output: 15
# Calling again the function with different number of arguments
total_kwargs = sum_all(10, 20, 30)
print(total_kwargs) # Output: 60
def greet(name, message="Hello"):
print(f"{message}, {name}!")
greet("Alice") # Uses default message "Hello"
greet("Bob", "Hi") # Overrides default message with "Hi"
Note
The order matters:
When defining a function, you can use a combination of different types of arguments. However, there are some rules to follow in the order of arguments:
Positional arguments must come before keyword arguments.
Keyword arguments must come before default arguments.
Default arguments must come after positional and keyword arguments.
Variable-length arguments must come last in the function definition.
Example of a function definition with all types of arguments:
def example_function(pos1, pos2, kwarg1=None, kwarg2=None, default1=10, default2=20, *args, **kwargs):
<block>
1.1.3 Functions Returning a Value#
In order to save the output of a function, the common method is the return
statement. It can be used or not in a function, depending on whether you want to use the output of the function later in your code or not.
Technical detail, if there is no return
statement is used, the function returns None
by default.
Example of a function that returns a value:
# Example of a function that returns a value
def add(a, b):
return a + b
result = add(5, 3)
print(result) # Output: 8
Example of a function that does not return a value:
# Example of a function that returns None
def add(a, b):
print(a + b)
add(5, 3)
result = add(5, 3) # result will be None
If then you try to print result
, it will print None
because the function does not return anything.
1.1.4 Functions Returning Multiple Values#
Functions can also return multiple values by separating them with commas. The returned values are packed into a tuple.
# Example of a function that returns multiple values
def get_coordinates():
x = 10
y = 20
return x, y
x, y = get_coordinates()
print(x, y) # Output: 10 20
You can unpack the returned tuple into specific individual variables, as shown in the example above. Or you can also assign the returned tuple to a single variable and access the values using indexing. Example:
coords = get_coordinates()
print(coords) # Output: (10, 20)
print("x = ", coords[0], ", y = ", coords[1]) # Output: x = 10, y = 20
Whats the output of the following code?
def calculate(a, b):
sum_val = a + b
diff_val = a - b
return sum_val, diff_val
result = calculate(10, 5)
print(result)
1.1.5 Recursive Functions#
A recursive function is a function that calls itself in order to solve a problem. It is typically constructed with a base case that has a statement to stop the recursion and a recursive case to continue the recursion.
The base case is the condition to stop the recursion.
The recursive case is the part where the function calls on itself.
# Example of a recursive function to calculate factorial
def factorial(n):
if n == 0:
return 1 # Base case
else:
return n * factorial(n - 1) # Recursive case
Recursive functions can be very useful for small problems, such as calculating factorials or incrementing numbers, but they can also lead to performance issues if the recursion depth is too high. Python has a recursion limit (which can be checked and set using the sys
module) to prevent infinite recursion from crashing the program.
1.1.6 Implicit Arguments#
In Python, methods defined within a class automatically take the instance of the class as the first argument, conventionally named self
. This allows methods to access and modify the instance’s attributes. We will explore classes and objects in more detail in the next section.
# Example of a class with a method using implicit arguments
class Dog:
def __init__(self, name):
self.name = name
def bark(self):
print(f"{self.name} says woof!")
dog = Dog("Buddy")
dog.bark()
1.1.7 Keyword Arguments, *args, and **kwargs#
As we have seen previously, Python allows you to define functions that can accept a variable number of arguments using *args
and **kwargs
.
# Example of a function using *args and **kwargs
def example_function(*args, **kwargs):
for arg in args:
print(arg)
for key, value in kwargs.items():
print(f"{key}: {value}")
example_function(1, 2, 3, name="Alice", age=30)
This is very useful when you need to create functions that can handle a flexible number of inputs. This is very common in matplotlib, for example, where you can pass a variable number of arguments to customize plots.
1.1.8 Global vs Local Variables#
Variables defined inside a function are called local variables and can only be accessed within that function. Variables defined outside of any function are called global variables and can be accessed from anywhere in the code, including inside functions. Example:
# Global variable
x = 10
def my_function():
# Local variable
y = 5
print("Inside function, x =", x) # Accessing global variable
print("Inside function, y =", y) # Accessing local variable
my_function() # Calling the function
print("Outside function, x =", x) # Accessing global variable
print("Outside function, y =", y) # This will raise an error because y is not defined outside the function
1.2 Lambda Functions#
Lambda functions are small anonymous functions defined using the lambda
keyword. They can take any number of arguments but can only have one expression. The expression is evaluated and returned.
Syntax:
lambda arguments: expression
# Example of a lambda function
square = lambda x: x ** 2
print(square(5)) # Output: 25
# Lambda function with conditional expression
even_or_odd = lambda x: "Even" if x % 2 == 0 else "Odd"
print(even_or_odd(7))
# Finding the maximum of two numbers using a lambda function
max_value = lambda a, b: a if a > b else b
print(max_value(10, 25))
# Lambda function and list comprehension
nums = [1, 2, 3, 4]
squares = [(lambda x: x ** 2)(n) for n in nums]
print(squares)
1.2.1 Some useful built-in functions: Map(), Filter() and Sorted()#
Since lambda functions are often used for short, throwaway functions, they are commonly used in combination with built-in functions that operate on collections of data, such as lists or tuples. There are some very useful built-in functions like map()
, filter()
and sorted()
that can be used to efficiently work with collections of data.
For instance:
map()
function: Applies a function to all items in an input list (or any iterable) and returns a map object (which can be converted to a list). A map object is an iterator that applies the function to each item of the iterable, yielding the results one by one.
# Example of using map with a lambda function
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x ** 2, numbers))
print(squared) # Output: [1, 4, 9, 16, 25]
filter()
function: Filters items in an input list (or any iterable data structure) based on a function that returnsTrue
orFalse
. Very useful to filter data based on some condition.
# Example of using filter with a lambda function
numbers = [1, 2, 3, 4, 5]
even_numbers = list(filter(lambda x: x % 2 == 0, numbers))
print(even_numbers) # Output: [2, 4]
sorted()
function: Sorts items in an iterable based on a key function. You can use a lambda function to define the sorting criteria.
# Example of using sorted with a lambda function
points = [(1, 2), (3, 1), (5, 4), (2, 3)]
sorted_points = sorted(points, key=lambda point: point[1])
print(sorted_points) # Output: [(3, 1), (1, 2), (2, 3), (5, 4)]
Exercises#
General Python exercises:#
Write a function that takes a list of numbers as input and returns the sum of all the numbers in the list.
Click to reveal the solution!
def sum_of_list(numbers):
return sum(numbers)
print(sum_of_list([1, 2, 3, 4, 5]))
Write a function that takes a string as input and returns the string in reverse order.
Click to reveal the solution!
def reverse_string(s):
return s[::-1]
print(reverse_string("hello"))
Write a function that takes a list of strings as input and returns a new list containing only the strings that have a length greater than 3.
Click to reveal the solution!
def filter_long_strings(strings):
return [s for s in strings if len(s) > 3]
print(filter_long_strings(["hi", "hello", "hey", "greetings"]))
Write a recursive function that takes a positive integer n as input and returns the nth Fibonacci number. Hint: To print the Fibonacci sequence in Python, we need to generate a series of numbers where each number is the sum of the two preceding ones, starting from 0 and 1. The Fibonacci sequence follows a specific pattern that begins with 0 and 1, and every subsequent number is the sum of the two previous numbers. \(F(n) = F(n-1) + F(n-2)\)
Click to reveal the solution!
def fibonacci(n):
if n <= 0:
return 0
elif n == 1:
return 1
else:
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(6))
Write a function that takes a list of numbers as input and returns a new list containing only the even numbers from the original list. Use the
filter()
function and a lambda function to accomplish this.
Click to reveal the solution!
def filter_even_numbers(numbers):
return list(filter(lambda x: x % 2 == 0, numbers))
print(filter_even_numbers([1, 2, 3, 4, 5, 6]))
Write a function that takes a list of numbers as input and returns a new list containing the squares of all the numbers in the original list. Use the
map()
function and a lambda function to accomplish this.
Click to reveal the solution!
def square_numbers(numbers):
return list(map(lambda x: x ** 2, numbers))
print(square_numbers([1, 2, 3, 4, 5]))
Write a function that takes a list of tuples, where each tuple contains a name and an age, and returns a new list of names sorted by age in ascending order. Use the
sorted()
function and a lambda function to accomplish this.
Click to reveal the solution!
def sort_by_age(people):
return [name for name, age in sorted(people, key=lambda person: person[1])]
print(sort_by_age([("Alice", 30), ("Bob", 25), ("Charlie", 35)]))
Write a function that takes a list of numbers as input and returns the maximum number in the list. Do not use the built-in
max()
function.
Click to reveal the solution!
def find_maximum(numbers):
max_num = numbers[0]
for num in numbers:
if num > max_num:
max_num = num
return max_num
print(find_maximum([1, 2, 3, 4, 5]))
Write a function that takes a list of strings as input and returns a new list containing the strings sorted in alphabetical order. Do not use the built-in
sorted()
function.
Click to reveal the solution!
def sort_strings(strings):
for i in range(len(strings)):
for j in range(i + 1, len(strings)):
if strings[i] > strings[j]:
strings[i], strings[j] = strings[j], strings[i]
return strings
print(sort_strings(["banana", "apple", "cherry"]))
Write a function that takes a list of numbers as input and returns the average of all the numbers in the list.
Click to reveal the solution!
def average_of_list(numbers):
return sum(numbers) / len(numbers) if numbers else 0
print(average_of_list([1, 2, 3, 4, 5]))
Classical and Quantum physics exercises:#
Write a function that calculates the kinetic energy of an object given its mass and velocity. The formula for kinetic energy is \(E_{k} = \frac{1}{2} * m * v^{2}\).
Click to reveal the solution!
def kinetic_energy(mass, velocity):
return 0.5 * mass * velocity ** 2
print(kinetic_energy(10, 5))
Write a function that calculates the energy levels of a hydrogen atom given the principal quantum number n. The formula for the energy levels is \(E_{n} = -13.6 eV / n^{2}\).
Click to reveal the solution!
def hydrogen_energy_level(n):
return -13.6 / (n ** 2)
print(hydrogen_energy_level(1))
print(hydrogen_energy_level(2))
Write a function that calculates the potential energy of an object given its mass, height, and gravitational acceleration. The formula for potential energy is \(PE = m * g * h\).
Click to reveal the solution!
def potential_energy(mass, height, gravity=9.81):
return mass * gravity * height
print(potential_energy(10, 5))
Write a function that calculates the energy of a photon given its frequency. The formula for the energy of a photon is \(E = h * f\), where h is Planck’s constant (approximately \(6.626 x 10^-{34}\) Js).
Click to reveal the solution!
def photon_energy(frequency, h=6.626e-34):
return h * frequency
print(photon_energy(5e14))
Write a function that calculates the period of a simple pendulum given its length and gravitational acceleration. The formula for the period is \(T = 2 * \pi * \sqrt(l / g)\). Hint: you might neeed to import the
math
module to access the value of \(\pi\) and the square root function. (Just addimport math
in the first line of your code).
Click to reveal the solution!
import math
def pendulum_period(length, gravity=9.81):
return 2 * math.pi * math.sqrt(length / gravity)
print(pendulum_period(10))
Write a function that calculates the de Broglie wavelength of a particle given its mass and velocity. The formula for the de Broglie wavelength is \(\lambda = h / (m * v)\), where h is Planck’s constant.
Click to reveal the solution!
def de_broglie_wavelength(mass, velocity, h=6.626e-34):
return h / (mass * velocity)
print(de_broglie_wavelength(9.11e-31, 1e6))
Similar exercises with positional and keyword arguments:#
Write a function that calculates the energy of a photon given its frequency and optionally its wavelength. If the wavelength is provided, use it to calculate the frequency using the formula frequency = speed_of_light / wavelength. The speed of light is approximately \(3*10^{8}\) m/s.
Click to reveal the solution!
def photon_energy(frequency=None, wavelength=None, h=6.626e-34, speed_of_light=3e8):
if wavelength is not None:
frequency = speed_of_light / wavelength
if frequency is not None:
return h * frequency
raise ValueError("Either frequency or wavelength must be provided.")
print(photon_energy(frequency=5e14))
print(photon_energy(wavelength=600e-9))
Write a function that calculates the de Broglie wavelength of a particle given its mass and velocity. Allow the user to specify the mass in kilograms or grams using a keyword argument. The formula for the de Broglie wavelength is \(\lambda = h / (m * v)\), where h is Planck’s constant.
Click to reveal the solution!
def de_broglie_wavelength(mass, velocity, mass_unit='kg', h=6.626e-34):
if mass_unit == 'g':
mass /= 1000 # Convert grams to kilograms
return h / (mass * velocity)
print(de_broglie_wavelength(9.11e-31, 1e6))
print(de_broglie_wavelength(9.11e-28, 1e6, mass_unit='g'))
Write a function that calculates the energy levels of a hydrogen atom given the principal quantum number n. Allow the user to specify the energy unit as either electron volts (eV) or joules (J) using a keyword argument. The formula for the energy levels is \(E_{n} = -13.6 eV / n^{2}\).
Click to reveal the solution!
def hydrogen_energy_level(n, energy_unit='eV'):
energy_eV = -13.6 / (n ** 2)
if energy_unit == 'J':
energy_eV *= 1.60218e-19 # Convert eV to Joules
return energy_eV
print(hydrogen_energy_level(1))
print(hydrogen_energy_level(1, energy_unit='J'))
Write a function that calculates the wavelength of a photon given its energy. Allow the user to specify the energy in electron volts (eV) or joules (J) using a keyword argument. The formula for the wavelength is \(\lambda = h * c / E\), where h is Planck’s constant and c is the speed of light.
Click to reveal the solution!
def photon_wavelength(energy, energy_unit='eV', h=6.626e-34, speed_of_light=3e8):
if energy_unit == 'eV':
energy *= 1.60218e-19 # Convert eV to Joules
return h * speed_of_light / energy
print(photon_wavelength(3.313e-19))
print(photon_wavelength(2.179e-18, energy_unit='J'))
Write a function that calculates the momentum of a particle given its mass and velocity. Allow the user to specify the mass in kilograms or grams using a keyword argument. The formula for momentum is \(p = m * v\).
Click to reveal the solution!
def particle_momentum(mass, velocity, mass_unit='kg'):
if mass_unit == 'g':
mass /= 1000 # Convert grams to kilograms
return mass * velocity
print(particle_momentum(9.11e-31, 1e6))
print(particle_momentum(9.11e-28, 1e6, mass_unit='g'))