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:

  1. Importing Entire Modules:

    • Import the entire math module.

    • Calculate and print the natural logarithm of 20.

  2. Importing Specific Functions:

    • Import only the pow function from the math module.

    • Calculate and print 2 raised to the power of 3 (\(2^3\)).

  3. Using Aliases for Modules:

    • Import the numpy module using the alias np.

    • 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#

  1. Create a function that takes in the radius of a circle and returns its area.

  2. 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:

  1. In Circle, implement a second method that calculates the circumference of the circle. Calculate and ürint the circumference.

  2. 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.

  1. 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.

  2. 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) to mean_rt.

      • Return the generated response_time.

  3. 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.

  1. 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.

  2. 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.

  1. 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).

  2. 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']