Functions and Objects#
Exercise 1: Dot notation and basic methods#
Use at least two methods (e.g., .append()
, .sort()
) on the provided list. Print the list before and after using the methods.
my_list = [3,1,4,6,1,7]
print("My list:", my_list)
my_list.sort()
print("After sorting:", my_list)
my_list.append(9)
print("After appending 4:", my_list)
My list: [3, 1, 4, 6, 1, 7]
After sorting: [1, 1, 3, 4, 6, 7]
After appending 4: [1, 1, 3, 4, 6, 7, 9]
Exercise 2: Everything is an object#
All data types and structures (like integers or lists) in Python are objects.
Print all methods associated with the previously created list.
Are the methods different from the methods associated with dictionaries?
print(dir(my_list))
my_dict = {}
print(dir(my_dict))
['__add__', '__class__', '__class_getitem__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getstate__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
['__class__', '__class_getitem__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__ior__', '__iter__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__or__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__ror__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'clear', 'copy', 'fromkeys', 'get', 'items', 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values']
Exercise 3: Modules and imports#
Python has a small built-in namespace, which means there is only a limited amount of functions available by default. To access a wide range of functions and tools, we thus need to import additional modules and libraries. Please do the following:
Importing Entire Modules:
Import the entire
math
module.Calculate and print the natural logarithm of 20.
Importing Specific Functions:
Import only the
pow
function from themath
module.Calculate and print 2 raised to the power of 3 (\(2^3\)).
Using Aliases for Modules:
Import the
numpy
module using the aliasnp
.Create a numpy array with the values [1, 4, 9, 16, 25].
Calculate and print the square root of each element in the array using the numpy module.
Hint: The numpy module is the fundamental Python package for scientific computing in Python. It uses arrays to store data. Please refer to the documentation for the third question. Do not worry about the details, we will discuss this package in more detail next week.
# 1. Import module
import math
log_value = math.log(20)
print(f"The natural logarithm of 20 is: {log_value}")
# 2. Import specific function
from math import pow
power_value = pow(2, 3)
print(f"2 raised to the power of 3 is: {power_value}")
# 3. Using alias for module
import numpy as np
array = np.array([1, 4, 9, 16, 25])
sqrt_values = np.sqrt(array)
print(f"The square root of each element in the array is: {sqrt_values}")
The natural logarithm of 20 is: 2.995732273553991
2 raised to the power of 3 is: 8.0
The square root of each element in the array is: [1. 2. 3. 4. 5.]
Exercise 4: Functions#
Write a function classify_reaction_time(rt)
that classifies a given reaction time (in ms) of a psychological experiment and returns the result as a string:
Below 400 ms: “Fast Response”
Between 400 ms and 600 ms: “Normal Response”
Above 600 ms: “Slow Response”
Use flow control statements (if, elif, else) to handle the classification.
Test the function by passing three different response times (fast, normal, slow) to it and printing the result
# Function to classify reaction time
def classify_reaction_time(rt):
if rt < 400:
return "Fast Response"
elif 400 <= rt <= 600:
return "Normal Response"
else:
return "Slow Response"
# Testing the function with different response times
response_times = [350, 450, 700]
for rt in response_times:
result = classify_reaction_time(rt)
print(f"Response Time: {rt} ms - Classification: {result}")
Response Time: 350 ms - Classification: Fast Response
Response Time: 450 ms - Classification: Normal Response
Response Time: 700 ms - Classification: Slow Response
Exercise 5: More functions#
Create a function that takes in the radius of a circle and returns its area.
Run the function by providing a radius and print the resulting area.
def calculate_area(radius):
return math.pi * radius ** 2
# Test the function with a radius of 5
radius = 5
area = calculate_area(radius)
print(f"The area of a circle with radius {radius} is {area}")
The area of a circle with radius 5 is 78.53981633974483
Exercise 6: Classes#
Functions are nice if you need to perform a specific, isolated computation. However, sometimes it is uesful to create encapsulate objects that have different properties (attributes) and actions (methods). For example, look at the provided code snippet.
An instance of Circle
is created by providing a radius: my_circle = Circle(radius=5)
. This radius is then assigned to the self.radius
attribute of the class. The only method in the class is the calculate_area()
method that (if called) calculates the area of the circle, assigns it to a newly created self.area
attribute.
Please do the following:
In
Circle
, implement a second method that calculates the circumference of the circle. Calculate and ürint the circumference.Implement a new class called
Square
that has a single attribute.side
, and two methods for area and circumference. Calculate and print both.
from math import pi
class Circle:
def __init__(self, radius):
self.radius = radius
def calculate_area(self):
self.area = pi * self.radius**2
def calculate_circumference(self):
self.circumference = 2 * pi * self.radius
# Create an instance of the Circle class and calculate the area and circumference
my_circle = Circle(radius=5)
my_circle.calculate_area()
my_circle.calculate_circumference()
print(f"The area of the circle is: : {my_circle.area}")
print(f"The circumference of the circle is: {my_circle.circumference}")
class Square:
def __init__(self, side):
self.side = side
def calculate_area(self):
self.area = self.side**2
def calculate_circumference(self):
self.circumference = 4 * self.side
# Create an instance of the Square class and calculate the area and circumference
my_square = Square(side=5)
my_square.calculate_area()
my_square.calculate_circumference()
print(f"The area of the square is: : {my_square.area}")
print(f"The circumference of the square is: {my_square.circumference}")
The area of the circle is: : 78.53981633974483
The circumference of the circle is: 31.41592653589793
The area of the square is: : 25
The circumference of the square is: 20
Voluntary Exercise 1: Nested functions#
In this exercise, you will create functions that simulate a participant’s decision-making process during a psychological experiment. The goal is to reinforce defining and using functions, handling parameters, and using control flow.
Creating a Decision Function
Write a function
make_decision(response_time, threshold)
that takes two arguments:response_time
: The time (in milliseconds) it takes for the participant to make a decision (an integer).threshold
: The decision threshold (an integer).
The function should:
Print “Decision Made!” if
response_time
is less than or equal to the threshold.Print “Too Slow! No Decision.” if
response_time
is greater than the threshold.
Simulating a Reaction Time Task
Create a function
simulate_reaction_task(mean_rt, variability)
that simulates a participant’s response time:mean_rt
: The average response time (an integer).variability
: The maximum amount by which the response time can vary (an integer).
The function should:
Use a random number generator (from the
random
module) to create a response time by adding a random value (between-variability
and+variability
) tomean_rt
.Return the generated
response_time
.
Running the Simulation
Create a main function
run_experiment()
that:Sets a
mean_rt
of 500 ms and a variability of 100 ms.Sets a
threshold
of 550 ms.Calls
simulate_reaction_task()
three times to generate three different response times.Calls
make_decision()
for each response time to determine if a decision was made.Prints the response times and the decision outcomes.
import random
# 1. Creating the decision function
def make_decision(response_time, threshold):
if response_time <= threshold:
print("Decision Made!")
else:
print("Too Slow! No Decision.")
# 2. Simulating a reaction time task
def simulate_reaction_task(mean_rt, variability):
response_time = mean_rt + random.randint(-variability, variability)
return response_time
# 3. Running the simulation
def run_experiment():
mean_rt = 500
variability = 100
threshold = 550
# Generate three simulated response times and make decisions
for i in range(3):
response_time = simulate_reaction_task(mean_rt, variability)
print(f"Response Time {i+1}: {response_time} ms")
make_decision(response_time, threshold)
# Run the experiment
run_experiment()
Response Time 1: 579 ms
Too Slow! No Decision.
Response Time 2: 548 ms
Decision Made!
Response Time 3: 545 ms
Decision Made!
Voluntrary Exercise 2: Classes and inheritance#
In this exercise, you will create a class that simulates a simple neuron. The neuron will allow energy deposits (representing excitatory input), energy reductions (representing inhibitory input), and checking its activation level.
Create the class
Write a class
Neuron
with the following attributes:neuron_name
: The name of the neuron (e.g., “Neuron A”).activation_level
: The current activation level (default is 0).
Implement the following methods:
excite(amount)
: Adds amount to the activation_level (representing excitatory input).inhibit(amount)
: Subtracts amount from the activation_level (representing inhibitory input). If the activation level becomes negative, set it to 0 and print a message indicating that the neuron has reached its inhibitory limit.check_activation()
: Prints the current activation level.
Test the class
Create an instance of
Neuron
for a neuron named “Neuron A” with an initial activation level of 20. Apply an excitatory input of 30, then apply an inhibitory input of 70. Print the final activation level.
# 1. Creating the class
class Neuron:
def __init__(self, neuron_name, activation_level=0):
self.neuron_name = neuron_name
self.activation_level = activation_level
def excite(self, amount):
self.activation_level += amount
print(f"{self.neuron_name} excited by {amount}. New activation level: {self.activation_level}")
def inhibit(self, amount):
self.activation_level -= amount
if self.activation_level < 0:
self.activation_level = 0
print(f"{self.neuron_name} reached inhibitory limit. Activation level set to 0.")
else:
print(f"{self.neuron_name} inhibited by {amount}. New activation level: {self.activation_level}")
def check_activation(self):
print(f"{self.neuron_name}'s current activation level: {self.activation_level}")
# 2. Testing the class
neuron_a = Neuron("Neuron A", activation_level=20)
neuron_a.excite(30)
neuron_a.inhibit(70)
neuron_a.check_activation()
Neuron A excited by 30. New activation level: 50
Neuron A reached inhibitory limit. Activation level set to 0.
Neuron A's current activation level: 0
In object oriented programming, classes can inherit attributes or methods from an existing class (base or parent class). This allows the subclass to reuse and extend the functionality of the parent class, adding its own specialized behavior. Have a look at an online tutorial to figure out how this can be implemented.
Create a subclass
InhibitoryNeuron
that inherits from Neuron.Override the
inhibit(amount)
method such that it adds an additional 10% inhibition (e.g., if 50 is passed as the amount, it should inhibit by 55).
Test the
InhibitoryNeuron
class by creating an instance, applying excitation and inhibition, and checking the activation level.
class InhibitoryNeuron(Neuron):
def inhibit(self, amount):
# Apply additional 10% inhibition
adjusted_amount = amount * 1.1
super().inhibit(adjusted_amount)
# Test the class
inhibitory_neuron = InhibitoryNeuron("Inhibitory Neuron B", activation_level=50)
inhibitory_neuron.excite(20)
inhibitory_neuron.inhibit(30)
inhibitory_neuron.check_activation()
Inhibitory Neuron B excited by 20. New activation level: 70
Inhibitory Neuron B inhibited by 33.0. New activation level: 37.0
Inhibitory Neuron B's current activation level: 37.0
Voluntary Exercise 3: List comprehensions#
You learned about nested statements in previous sessions. Please carefully read the following code. What is its purpose?
random_stuff = [1, "banana", "apple", 3.14, 7, "hello"]
strings_only = [] # empty list for filtered vaues
# Loop through the list
for elem in random_stuff:
# if the current element is a string...
if isinstance(elem, str):
# ...then append the value to strings_only
strings_only.append(elem)
print(f"Only the string values: {strings_only}")
Only the string values: ['banana', 'apple', 'hello']
Python offers a feature called list comprehension. This allows you to write for-loops in a more compact way. List comprehensions are syntactic sugar, which means they do not add any additional features and are just a way of making the code a bit shorter (and maybe even nicer to read once you get used to them). Take a look at the following example which prints the elements of the random_stuff
list:
p = [print(elem) for elem in random_stuff]
1
banana
apple
3.14
7
hello
Can you implement the previous code block that filters the strings from the list by using a single list comprehension?
strings_only = [elem for elem in random_stuff if isinstance(elem, str)]
print(f"Only the string values: {strings_only}")
Only the string values: ['banana', 'apple', 'hello']