Back to the main index

An introduction to Python

Part of the introductory series to using Python for Vision Research brought to you by the GestaltReVision group (KU Leuven, Belgium).

In the first two parts you will learn the basics of Python just enough to build a real working experiment.

Authors: Maarten Demeyer, Jonas Kubilius
Year: 2014
Copyright: Public Domain as in CC0

Contents

Code advancement 0

The goal of the first two classes is to build a Change Blindness experiment. We will work with pairs of images that only differ on a tiny detail, for example, a green traffic light might turn into red in a typical street scene. We will continuously switch between both images until the participant observes a difference between the two. In addition, half of the image pairs will be presented upright, whereas half will be inverted. To make the changes more difficult to detect, we will have bubbles of various sizes superimposed on the images, which will be repositioned to new random locations upon each image switch. When the participant detects the change, she should hit spacebar. Otherwise, after 30 seconds the experiment advances to the next stimulus. At any time, the participant can press escape to quit the experiment.

If this is not very clear, find the full code of the experiment in script_final.py in the Part2 directory, and try running it yourself first.

Question: How would you go about creating this experiment? We understand you don't know how to program yet, but simply try to list all steps that such experiment should perform. The answer is below but you will benefit massively from trying to produce it yourself.

In [5]:
%load script_0.py
In [ ]:
#==============================================
# Settings that we might want to tweak later on
#==============================================

# directory to save data in                          data
# directory where images can be found                image
# image names without the suffixes                   1,2,3,4,5,6
# suffix for the first image                         a.jpg
# suffix for the second image                        b.jpg
# screen size in pixels                              1200x800
# image freezing time in seconds                     30
# image changing time in seconds                     0.5
# number of bubbles overlayed on the image           40


#==========================================
# Store info about the experiment session
#==========================================

# Get subject name, gender, age, handedness through a dialog box
# If 'Cancel' is pressed, quit
# Get date and time
# Store this information as general session info
# Create a unique filename for the experiment data


#========================
# Prepare condition lists
#========================

# Check if all images exist
# Randomize the image order

# Create the orientations list: half upright, half inverted
# Randomize the orientation order


#===============================
# Creation of window and stimuli
#===============================

# Open a window
# Define trial start text
# Define the bitmap stimuli (contents can still change)
# Define a bubble (position and size can still change)


#==========================
# Define the trial sequence
#==========================

# Define a list of trials with their properties:
#   - Which image (without the suffix)
#   - Which orientation


#=====================
# Start the experiment
#=====================

# Run through the trials. On each trial:
#   - Display trial start text
#   - Wait for a spacebar press to start the trial, or escape to quit
#   - Set the images, set the orientation
#   - Switch the image every 0.5s, and:
#        - Draw bubbles of increasing radius at random positions
#        - Listen for a spacebar or escape press
#   - Stop trial if spacebar or escape has been pressed, or if 30s have passed
#   - Analyze the keypress
#        - Escape press = quit the experiment
#        - Spacebar press = correct change detection; register response time
#        - No press = failed change detection; maximal response time
#   - Advance to the next trial


#======================
# End of the experiment
#======================

# Save all data to a file
# Quit the experiment

Literals, expressions, and variables

We will need our program to hold and handle many pieces of information. For instance, the reaction time recorded on a trial, or the duration of the image presentation.

As a starting point, we distinguish between three useful types of values:

  • Integer: whole numbers, for instance '5'
  • Float: decimal numbers, for instance '5.3'
  • Boolean: binary numbers, coded as True or False

These value representations are called literals, since you would literally put into your program which value you want to be there.

More commonly however, values are implied in expressions, which evaluate (e-value-ate) to a value. For instance, '5+3' does not represent the number 8 literally, but if you work it out, it does evaluate to 8. Therefore, an expression is any piece of code which represents a value.

Many values change during the execution of a program while they retain the same meaning; or, they might recur so often throughout the program that it is more convenient to represent them by a symbolic name instead. For this, we have variables which have a fixed name, and a value that is not fixed. To assign a value to a variable, use the following syntax:

name = value

To then see the current value of a variable, do:

print name

Since we are for now considering three types of values (integer, float, boolean), we also need three types of variables. Unlike other programming languages, Python does not require you to define the type of a variable yourself; Python will figure it out from the values you assign. To inspect the type of any variable once it has been assigned, do:

print type(name)

In [ ]:
# Assign values to the three variable types
# Here, we simply assign a literal
my_int = 5
print my_int
print type(my_int)

my_float = 5.0
print my_float
print type(my_float)

my_boolean = False
print my_boolean
print type(my_boolean)
In [ ]:
first_number = 5
second_number = 5.0

## Now we assign an expression to a variable
## Since an expression evaluates to a value, this is equivalent

# Simple expression
summed_numbers = first_number + second_number
print summed_numbers, type(summed_numbers)

# Re-assign a variable using an expression involving its own value
first_number = first_number * 3
print first_number

# A more complex expression
result = ((first_number-3)*second_number)/1.3
print result

You might note that a literal is itself actually a special case of an expression, since a literal is also a piece of code which represents a value.

Statements and program flow

A program typically consists of a sequence of short lines, each of which does something. One such line of code which does something is called a statement.

Above, we have used assignment statements to assign values to variables, and print statements to print variable values to the screen.

But, if we want our program to be more than a glorified calculator, we need automated program flow. Two kinds of statements are of fundamental importance then: conditional statements and iterative statements.

The basic conditional statement is the if statement, which only executes the next block of statements if it is followed by an expression which evaluates to True. For instance:

In [ ]:
a = 1
b = 2

if True:
    print a  

if False:
    print b

Note the colon : at the end of the if-statement, which must be present. Note also that the block of code conditional on the if-statement must be indented, preferably by four spaces.

Of course, this code is pretty useless, since we know the conditional value will always be True. We could also replace the condition by a variable which is equal to, and therefore evaluates to a boolean:

In [ ]:
a = 1
my_boolean = True

if my_boolean:
    print a 

This can sometimes be useful. However more commonly, we will not represent the True or False value of the if-condition by a literal or a variable, but by a more complex expression. For instance, a > 0 would evaluate to True. The resulting value of the expression is only used in this line of code, so there is no actual need to store it in a variable first, we can just insert the expression itself.

In [ ]:
a = 1

# This expression evaluates to a boolean
my_boolean = a > 0
print my_boolean

# We can assign to a variable and use that
if my_boolean:
    print a

# Or we could use the full expression directly
if a > 0:
    print a

We can specify alternate options if the condition is not satisfied, and evaluate integers or floats to booleans in various operations.

In [ ]:
a = 1
b = 2
c = 3

if a > 0:
    print a
    
elif b == 2: 
    # Execute if b equals 2
    # ...but ONLY if the first condition was False
    # Note the difference with the assignment symbol =
    print b  
    
elif c <= 3:  
    # Execute if c is smaller than or equal to 3
    # ...but ONLY if the first two conditions were False
    print c 
    
else:      
    # Execute in any case
    # But ONLY if all others were False
    print 4      

    
if b >= 0:    
    # Execute if b is greater than or equal to 0
    # A new if-statement, evaluated independent of previous conditions
    print b   
    
    
# This statement is not indented
# ...and will therefore always execute
print 5
In [ ]:
a = 1
b = 2
c = 3

# We can logically combine booleans to create another boolean
# using 'and', 'or' and 'not'
my_boolean = a > 0 and b == 2
print my_boolean

# Instead of assigning to a variable, again we can use
# the full expression in the conditional statement
if a > 0 and b == 2:
    print a, b   
elif b == 3 or c <= 3:
    print b, c
elif not a < 0 and c == 3:
    print a, c

Now we have really started programming! Next, we need an iterative statement which can re-execute statements without us having to keep entering them literally in the program.

The most basic type is the while statement, which repeatedly executes its code block as long as its conditional expression evaluates to True.

In [ ]:
# Instead of this:
a = 1
print a
a = a + 1
print a
a = a + 1
print a
a = a + 1
print a
a = a + 1
print a

# (copy-paste as many times as you need...)
In [ ]:
# We can write much shorter and far more flexibly:
a = 1
maximum_number = 5

while a <= maximum_number:
    print a
    a = a + 1

To stop your while-loop at any time, use the break statement.

In [ ]:
a = 1
maximum_number = 5

while True:
    print a
    a = a + 1
    if a > maximum_number:
        break

With what you have just learned, you could in principle write any program imaginable!

However, they would often be long, repetitive and difficult to understand. All the concepts we will next learn are therefore meant to make the solution more simple when the problem you are trying to solve is more complex.

More data types

We now introduce more complex variable types. The first three (string, tuple and list) are ordered series of values, whereas the fourth (dictionary) connects pairs of values to one another, in no particular order.

All four have a way to display their length, len()

In [ ]:
# A string represents an ordered series of characters
my_string = "a line of text"
my_string = 'a line of text'

print my_string
print type(my_string)
print len(my_string)
In [ ]:
# A tuple is an ordered series of values
# ... but these values may consist of different data types

my_tuple = (1, 2.0, False)

print my_tuple
print type(my_tuple)
print len(my_tuple)
In [ ]:
# A list is similar (see below for the difference)
# Note that tuples and lists may even have complex data types as elements

my_list = [True, "a string!", (1,'tuple','in', 1, 'list')] 

print my_list
print type(my_list)
print len(my_list)
In [ ]:
# A dictionary connects values to one another
# It is unordered; the order of elements does not matter
# Again, all variable types may be used

my_dictionary = {1:[1,1,1], 3:'three', False:2.0}
print my_dictionary

my_dictionary = {False:2.0, 1:[1,1,1], 3:'three'}
print my_dictionary

print type(my_dictionary)
print len(my_dictionary)

To retrieve a particular element, use square brackets. In case of an ordered series, this will retrieve the n-th element, where the index count starts at 0.

Note that strings and tuples cannot be modified once assigned, unless re-assigned, whereas this is possible for lists and dictionaries.

In [ ]:
a = "kumbaya, milord"
print a

# Fetch and print the first element of a string
print a[0]

# Fetch the first until, but not including, the fifth element
# This is called 'slicing'
print a[0:4]

# -1 is the index of the last element
# -2 of the second-to-last, etc
print a[-2]
print a[-5:-1]
print a[-5:]

# Reverse the order or change step size
# Syntax: [start:end:step]
print a[-1:-10:-1]
print a[0::3]
In [ ]:
# The same applies to the other ordered data types
b = [1, 2.0, True]
print b

# In lists, you can change existing elements
# Change the second element in a list
b[1] = 'foo'
print b

# Print the elements in reverse
print b[::-1]
In [ ]:
# '+' performs concatenation on strings, tuples and lists
# Since we do not re-assign the result to a variable however
# ...nothing changes in the original ordered series

a = "kumbaya, milord"
print a
print a + '!!!'
print a

# Similarly, '*' performs replication of the entire string
print a * 2
In [ ]:
# Same goes for a tuple (or a list)
b = (1, 2.0, True)
print b + (5,)
print b * 2
print b

# By re-assigning, we do change the tuple
b = b + (5,)
print b

In case of a dictionary, square brackets will retrieve the second value of each pair through the first value.

In [ ]:
# Retrieve the value corresponding to 1 in the dictionary
# Note that this retrieval is one-way only
# ...therefore the first values (the 'keys') need to be unique

d = {1:'one', 2:'two', 3:'three'}
print d[1]

# Add a value to the dictionary
d[4] = 'four'
print d

We won't discuss all possible options when working with strings, tuples, lists and dictionaries.

A useful one however is the in keyword, which evaluates to a boolean whether a given element is present or not.

In [ ]:
# String
a = 'example'
result = 'i' in a
print result

# Tuple
b = (1,2,3)
print 0 in b

# List
c = [1,2,3]
print 5 in c

# In case of a dictionary, this pertains only to the keys
d = {1:'one', 2:'two', 3:'three'}
print 2 in d
print 'two' in d

Another handy fact is that an empty string, list, tuple, or dictionary will by itself evaluate to False, if a boolean is required. There is no need to check whether the length is equal to zero. Play around with the variable a to check that we're telling you no lies.

In [ ]:
a = []

# We could do:
if len(a) == 0:
    print 'This variable is empty!'
else:
    print 'There is something in here!'

# But this is shorter:
if not a:
    print 'This variable is empty!'
else:
    print 'There is something in here!'

More program flow

The next basic ingredient we will add here is a new iterative statement, the for-statement. 'for' allows us to run through each element of an ordered series, and execute statements on them.

In [ ]:
# Without the for-syntax, we could do it like this
a = 'hi!'
n = 0
while n < len(a):
    print a[n]
    n = n + 1
In [ ]:
# With the for-syntax, this becomes much more intuitive 
a = 'hi!'
for letter in a:
    print letter

A final, brutal instrument in program flow control is the exception. Also known as: an error. Errors are not just annoying, as a programmer they are actually useful, since they allow you to exit the program at any point by raising an exception, and telling the user what went wrong.

In [ ]:
number_of_paws = -1

if number_of_paws < 0:
    raise Exception('Your dog cannot have a negative number of paws!')

print 'My dog has', number_of_paws, 'paw(s).'

Debugging

Unfortunately you do not always need to raise errors yourselves. You will make mistakes (called 'bugs') and errors will occur. This is normal.

An important skill for any programmer is therefore to debug his program efficiently. Sometimes, the error message itself is informative enough.

In [ ]:
aseries = (0,2,3)
a_series[0] = 1

More often however the cause of the error is not as readily apparent, or there might be no error at all, just unexpected behavior. For instance, debug this:

In [ ]:
my_string = 'Hi!'
my_list = []

# We try to turn a string into a list
# of individual characters
n = 0
while True:
    my_list + [my_string[n]]
    n = n + 1
    if n > len(my_string):
        break
print my_list

The most basic form of debugging, is to add print statements at every iteration, so you can see what is happening. Do this now.

More advanced options:

  • In Spyder, inspect variable values in the Variable Explorer tab at any time.
  • To stop and debug a script during its execution, use the built-in pdb module. There is a nice introducion to pdb on our wiki. In Spyder, pdb breakpoints can be set visually by double clicking on the relevant line number. A red dot will appear, where the script will stop if you run it in debug mode (CTRL-F5, then CTRL-F12). In combination with Spyder's Variable Explorer, this makes for a very powerful debugging method.

Intermediate summary

Concepts

  • literals
  • expressions
  • variables
  • statements

Data types

  • int
  • float
  • bool
  • str (ordered, immutable, only characters)
  • tuple (ordered, immutable, all types)
  • list (ordered, mutable, all types)
  • dictionary (unordered, mutable, all types, unique keys)

Statements

  • =
  • print
  • if elif else
  • while
  • break
  • for
  • raise

Also useful

  • #
  • type()
  • len()
  • + - * / ** %
  • < > <= >= ==
  • and or not
  • [ ]
  • in
  • len()==0 is unnecessary

Code advancement 1

Using what we learned so far, try to subsitute as much of pseudocode from Code advancement 0 with the real code. The answer is below but please put an effort into working it out by yourself.

In [4]:
%load script_1.py
In [ ]:
#==============================================
# Settings that we might want to tweak later on
#==============================================

datapath = 'data'                   # directory to save data in
impath = 'images'                   # directory where images can be found
imlist = ['1','2','3','4','5','6']  # image names without the suffixes
asfx = 'a.jpg'                      # suffix for the first image
bsfx = 'b.jpg'                      # suffix for the second image
scrsize = (1200,800)                # screen size in pixels
timelimit = 30                      # image freezing time in seconds
changetime = .5                     # image changing time in seconds
n_bubbles = 40                      # number of bubbles overlayed on the image


#========================================
# Store info about the experiment session
#========================================

exp_name = 'Change Detection'
exp_info = {}

# Get subject name, gender, age, handedness through a dialog box
# If 'Cancel' is pressed, quit
# Get date and time
# Store this information as general session info

# Create a unique filename for the experiment data
data_fname = exp_info['participant'] + '_' + exp_info['date']


#========================
# Prepare condition lists
#========================

# Check if all images exist
# Randomize the image order

# Create the orientations list: half upright, half inverted
orilist = [0,1]*(len(imlist)/2)

# Randomize the orientation order


#===============================
# Creation of window and stimuli
#===============================

# Open a window

# Define trial start text
text = "Press spacebar to start the trial"

# Define the bitmap stimuli (contents can still change)
# Define a bubble (position and size can still change)


#==========================
# Define the trial sequence
#==========================

# Define a list of trials with their properties:
#   - Which image (without the suffix)
#   - Which orientation


#=====================
# Start the experiment
#=====================

for trial in trials:
    
    # Display trial start text
    
    # Wait for a spacebar press to start the trial, or escape to quit
    
    # Set the image filename, set the orientation    
    
    # Start the trial
    # Stop trial if spacebar or escape has been pressed, or if 30s have passed
    while not response and time < timelimit:
        
        # Switch the image
        
        # Draw bubbles of increasing radius at random positions

        # For the duration of 'changetime',
        # Listen for a spacebar or escape press
        while time < changetime:
            if response:
                break

    # Analyze the keypress
    if response:
        if escape_pressed:
            # Escape press = quit the experiment
            break
        elif spacebar_pressed:
            # Spacebar press = correct change detection; register response time
    else:
        # No press = failed change detection; maximal response time

    # Advance to the next trial


#======================
# End of the experiment
#======================

# Save all data to a file
# Quit the experiment

Functions

We have learned how to represent values or even series of values by a short variable name, so that we can re-use it easily.

Often however we wish to quickly repeat many lines of code. For instance, the formula for a sine is complicated, we would not want to copy-paste it into the code for every angle for which we want to compute a sine. We would rather refer to the relevant code by a short name, such as sin.

Functions allow this. To define a function, use a def statement:

In [ ]:
def print_something():
    print "preparing to print"
    print "the function is printing"
    print "done printing"

print_something()
print_something()

To add more flexibility, we can pass variables to the function, called the function arguments, as well as return variables back to the main block of code.

In [ ]:
# Function definition
def print_and_sum(a, b):
    print 'The first number is', a
    print 'The second number is', b
    return a + b

# Assign function output to a variable
c = print_and_sum(3,5)
print 'Their sum is', c

# Print function output directly
print print_and_sum(10,20)

Note that our function call is by itself an expression, since it returns and therefore evaluates to a value. Multiple return values, separated by a comma, are returned as a tuple. Functions without a return argument (e.g. the first example) return None.

Defining functions is often important when you write long, repetitive programs. However even more important is that others have defined common functions for you, which you then do not need to re-invent, or even understand how they work!

Actually, when you used len() or type() above, you were doing exactly this - you were using built-in functions. We could also write our own len() function, but why bother, when it exists already?

In [ ]:
def my_len(inp):
    x = 0
    for el in inp:
        x = x + 1
    return x

a = [1, True, 'T']

print len(a)
print my_len(a)

Modules

Suppose we have a collection of functions that, loosely or not, belong together. For instance, a bunch of trigonometric functions.

We can then put this code in one .py file, whereas we can put unrelated code in a separate .py file. Such a .py file is then called a module, which we can load again in other Python programs, and use its functions.

For instance, try to save this code to a file called multiplications.py:

In [ ]:
def times_two(x):
    print x*2
    
def times_three(x):
    print x*3

Now, if put in the same directory as your current Python script, we can import the module using simply the filename (without the .py extension). Its functions are accessed using the dot operator .

In [ ]:
import multiplications

a = 5
multiplications.times_two(a)
multiplications.times_three(a)

To address a module in a shorter way, we can specify our own name for the module, or even just import some of the functions directly, in which case we don't even need to use the module name anymore.

In [ ]:
import multiplications as mt

mt.times_two(5)
In [ ]:
from multiplications import times_two

times_two(5)

In larger software libraries, such as PsychoPy, you can encounter modules-within-modules, to organize the wealth of code in a logical, hierarchical way. The overarching module-containing-modules is then called a package.

Objects

In many situations, certain variables and functions are very closely connected to one another. Take for instance a list variable, and the function append_element() which appends a new element to it.

In [ ]:
a = [1,2,3]
print a

def append_element(old_list, new_element):
    return old_list + [new_element]

a = append_element(a,4)
print a

The function needs a list, and the list has great use for the function. They belong together. This is why we can group variables and functions into an object.

It is not within the scope of this tutorial to define our own objects. However, since they are encountered everywhere in Python, you do need to understand how to use those that were made for you by others. Often you will not address the member variables of an object directly after their assignment, but you will use member functions that use and/or affect these member variables.

Actually, you have already been using objects. All variables in Python are objects, even a simple integer. This becomes more apparent with the more complex data types, such as a list, that have convenient member functions. Through the . (dot) operator, you can use these member functions.

As you will see, many of the common operations which you would want to perform on a list have already been implemented for you... even the append function!

In [ ]:
# Do the same thing 
# ...using the append member function of the list object
a = [1,2,3]
a.append(4)
print a
In [ ]:
# reverse() reverses the list
a.reverse()
print a

# remove() removes its first encounter of the specified element
a.remove(2)
print a

# sort() sorts the list from low to high
a.sort()
print a

# pop() returns AND removes the last element
print a.pop()
print a

In the above cases, the actual contents of the list are stored in some member variable of the list object, which represents its current value. The member functions then use this member variable to perform their operations on, and replace it by their output, so that the contents of the list change by executing the member function.

We call this in-place functions; they modify the object's contents, and typically do not return anything (i.e., None).

In [ ]:
print a
print a.reverse()
print a

Other objects, like a string, are however immutable, as you may remember; you cannot change their contents. The in-place list functions will not work on string.

Instead, strings often have member functions which take the contents of the string object, process it, and return a new object. If the returned object is a modified version of the string, you may then choose to assign that string to the same variable name, in which case the old string will be replaced by the new one, to the same result as an in-place function.

In [ ]:
a = 'kumbaya, milord'

# Split at spaces, and return a list of strings
print a.split(' ')

# To print the second word in a sentence, you could then do
print a.split(' ')[1]
In [ ]:
# Check whether a string starts or ends with certain characters
# Returns a boolean
print a.startswith('kumba')
print a.endswith('ard')
In [ ]:
# Remove the first/last part of a string
# ...note that the original string is not changed
# A new, different string is returned instead
print a.lstrip('kumba')
print a.rstrip('ord')
print a
In [ ]:
# Replace part of a string
# Again, a new string is returned
print a.replace('lord', 'lard')

# Here we assign the result to the original string variable
# To the same effect as an in-place function
a = a.replace('u','x')
print a

Example: The PsychoPy TrialHandler

psychopy defines a new, useful type of objects (much like float is the type of '5.0') called TrialHandler. As we will see, it does not allow you to do anything you couldn't do without using objects, but it does make your life simpler.

Remember our experiment - defining our trial are two pieces of information: the image to be presented, and the orientation of the image. After the trial, we want to obtain the accuracy and the reaction time. We could have handled this as follows:

In [ ]:
im_order = [5, 6, 3, 2, 1, 4]
ori_order = [0, 1, 1, 0, 1, 0]
trials_n = [1, 2, 3, 4, 5, 6]
data_acc = []
data_rt = []

for trn in trials_n:
    current_image = im_order[trn-1]
    current_orientation = ori_order[trn-1]
    
    # Present the correct stimuli
    # ...then gather the responses as acc and rt
    # e.g.,
    acc = 1
    rt = 0.32
    
    data_acc.append(acc)
    data_rt.append(rt)

print data_acc
print data_rt

# Find some function that turns all these lists into a text file
# Preferably easily loadable by your statistics program

In practice, these lists belong together, and you'll always want to perform the same actions on them. Define your conditions, loop over your trials, add responses to each trial, and save everything to a file. In addition you might want to add error checking, for instance to make sure that all lists are of equal length.

PsychoPy bundles all this and more into a convenient object for you, so that you don't need to repeatedly code the same things over and over.

--

The TrialHandler is set up using a constructor, which much like a function takes certain input arguments. Here, the condition values you want to use, and some settings.

This information is then stored inside the TrialHandler object, but we won't care exactly how or where. We just use the object's functions.

Then, looping over the TrialHandler returns one trial at a time, containing the values for that specific trial in the form of a dictionary.

Adding responses is done through the addData() member function. Since TrialHandler remembers what the current trial is, you don't need to specify that.

Finally, the SaveAsWideText() function saves both the condition values and the response values to a text file for you.

In [ ]:
# import the TrialHandler
from psychopy.data import TrialHandler

# Define conditions values as a list of dicts
stim_order = [{'im':5,'ori':0},
              {'im':6,'ori':1},
              {'im':3,'ori':1},
              {'im':2,'ori':0},
              {'im':1,'ori':1},
              {'im':4,'ori':0}]

# Construct the TrialHandler object
#     nReps = number of repeats
#     method = randomization method (here: no randomization)
trials = TrialHandler(stim_order, nReps=1, method='sequential')

# Loop over the trials
for trial in trials:
    
    print trial['im'], trial['ori']
    
    # Present the correct stimuli
    # ...then gather the responses as acc and rt
    # e.g.,
    acc = 1
    rt = 0.32
    
    trials.addData('acc',acc)
    trials.addData('rt',rt)

# And save everything to a file
trials.saveAsWideText('test.csv', delim=',')

The main practical difference between a module and an object (for now) is that you can easily create several instances of a given object type (many different strings, several TrialHandlers, ...), whereas you should import a module only once in a script. Module functions typically act on data you provide in the individual function arguments, whereas object functions act on variables that are present within the object.

Intermediate summary

Functions

  • Function definition (def)
  • Input arguments
  • Output arguments
  • Function call

Modules

  • Filename = module name
  • import ...
  • import ... as ...
  • from ... import ...
  • . (dot) operator

Objects

  • Member variables (can be hidden)
  • Member functions
  • . (dot) operator
  • In-place functions

Code advancement 2

You have now learned a couple more useful functions which should help you further advance with the code from Code advancement 2. The answer is below.

In [3]:
%load script_2.py
In [ ]:
#===============
# Import modules
#===============

from psychopy import data

#==============================================
# Settings that we might want to tweak later on
#==============================================

datapath = 'data'                   # directory to save data in
impath = 'images'                   # directory where images can be found
imlist = ['1','2','3','4','5','6']  # image names without the suffixes
asfx = 'a.jpg'                      # suffix for the first image
bsfx = 'b.jpg'                      # suffix for the second image
scrsize = (1200,800)                # screen size in pixels
timelimit = 30                      # image freezing time in seconds
changetime = .5                     # image changing time in seconds
n_bubbles = 40                      # number of bubbles overlayed on the image


#========================================
# Store info about the experiment session
#========================================

exp_name = 'Change Detection'
exp_info = {}

# Get subject name, gender, age, handedness through a dialog box
# If 'Cancel' is pressed, quit
# Get date and time
# Store this information as general session info

# Create a unique filename for the experiment data
data_fname = exp_info['participant'] + '_' + exp_info['date']


#========================
# Prepare condition lists
#========================

# Check if all images exist
# Randomize the image order

# Create the orientations list: half upright, half inverted
orilist = [0,1]*(len(imlist)/2)

# Randomize the orientation order


#===============================
# Creation of window and stimuli
#===============================

# Open a window

# Define trial start text
text = "Press spacebar to start the trial"

# Define the bitmap stimuli (contents can still change)
# Define a bubble (position and size can still change)


#==========================
# Define the trial sequence
#==========================

# Define a list of trials with their properties:
#   - Which image (without the suffix)
#   - Which orientation

# We need a list of dictionaries, e.g. {im:'6',ori:1}
# Leave blank, we will soon learn how to fill this with imlist and orilist
stim_order = [{},{},{},{},{},{}]

trials = data.TrialHandler(stim_order, nReps=1, extraInfo=exp_info,
                           method='sequential', originPath=datapath)


#=====================
# Start the experiment
#=====================

for trial in trials:
    
    # Display trial start text
    
    # Wait for a spacebar press to start the trial, or escape to quit
    
    # Set the images, set the orientation   
    trial['im']
    trial['ori']
    
    # Start the trial
    # Stop trial if spacebar or escape has been pressed, or if 30s have passed
    while not response and time < timelimit:
        
        # Switch the image
        
        # Draw bubbles of increasing radius at random positions

        # For the duration of 'changetime',
        # Listen for a spacebar or escape press
        while time < changetime:
            if response:
                break

    # Analyze the keypress
    if response:
        if escape_pressed:
            # Escape press = quit the experiment
            break
        elif spacebar_pressed:
            # Spacebar press = correct change detection; register response time
            acc = 1
            rt =
    else:
        # No press = failed change detection; maximal response time
        acc = 0
        rt = timelimit
    
    # Add the current trial's data to the TrialHandler
    trials.addData('rt', rt)
    trials.addData('acc', acc)

    # Advance to the next trial


#======================
# End of the experiment
#======================

# Save all data to a file
trials.saveAsWideText(data_fname + '.csv', delim=',')

# Quit the experiment

Useful modules and functions

Congratulations, you have now gained the programming knowledge you need to write an experiment.

All you need to learn about now, are useful existing packages, modules and functions which can help you program the experiment. We will review the ones we will need here - but there are many more!

Global built-in functions

Global built-in functions are not part of any package, module or object, but can just be used directly. Apart from those we've seen, like len(), the following ones are useful.

In [ ]:
# zip() takes two ordered series of equal length
# ...and creates a list of tuple pairs from their elements
print zip('haha', 'hihi')
print zip([1,2,4],[4,5,6])
In [ ]:
# range() creates a sequence of integers
# ...by default, starting from 0 and not including the specified integer
print range(10)

# The (included) starting value can however be specified
print range(1,10)

# As can the step size
print range(1,10,2)

os

The os package, built into Python, contains functions that will help you handle directories and files on your computer.

In [ ]:
import os

my_file = 'multiplications'
my_ext = '.py'

# Get the current working directory
# Returns a string
my_dir = os.getcwd()
print my_dir

# Check whether a directory exists
# Returns a boolean
print os.path.isdir(my_dir)

# Creates a directory
if not os.path.isdir('temp'):
    os.makedirs('temp')

# Joins together different parts of a file path
# Returns a string
my_full_path = os.path.join(my_dir, my_file+my_ext)
print my_full_path

# Check whether a file exists
# Returns a boolean
print os.path.exists(my_full_path)

numpy.random

numpy is a package that is not by default part of Python, but installed as a user package. It contains many functions useful for science, similar to MATLAB, but here will only use its random module, which contains randomization functions.

In [ ]:
import numpy.random as rnd

a =  [1, 2, 3, 4, 5]
print a

# Shuffle the order of a
rnd.shuffle(a)
print a

# Generate 5 random floats between 0 and 1
b = rnd.random(5)
print b

# Generate 5 integers up to but not including 10
# Also notice how we specify the 'size' argument here
# It is an OPTIONAL argument; specified by name rather than order
c = rnd.randint(10, size=5)
print c

Code advancement 3

This is you final chance to update the code from Code advancement 2 before we start on PsychoPy!

In [2]:
%load script_3.py
In [ ]:
#===============
# Import modules
#===============

import os                           # for file/folder operations
import numpy.random as rnd          # for random number generators
from psychopy import visual, event, core, gui, data


#==============================================
# Settings that we might want to tweak later on
#==============================================

datapath = 'data'                   # directory to save data in
impath = 'images'                   # directory where images can be found
imlist = ['1','2','3','4','5','6']  # image names without the suffixes
asfx = 'a.jpg'                      # suffix for the first image
bsfx = 'b.jpg'                      # suffix for the second image
scrsize = (1200,800)                # screen size in pixels
timelimit = 30                      # image freezing time in seconds
changetime = .5                     # image changing time in seconds
n_bubbles = 40                      # number of bubbles overlayed on the image


#========================================
# Store info about the experiment session
#========================================

exp_name = 'Change Detection'
exp_info = {}

# Get subject name, gender, age, handedness through a dialog box
# If 'Cancel' is pressed, quit
# Get date and time
# Store this information as general session info

# Create a unique filename for the experiment data
if not os.path.isdir(datapath):
    os.makedirs(datapath)
data_fname = exp_info['participant'] + '_' + exp_info['date']
data_fname = os.path.join(datapath, data_fname)


#=========================
# Prepare conditions lists
#=========================

# Check if all images exist
for im in imlist:
    if (not os.path.exists(os.path.join(impath, im+asfx)) or
        not os.path.exists(os.path.join(impath, im+bsfx))):
        raise Exception('Image files not found in image folder: ' + str(im)) 
        
# Randomize the image order
rnd.shuffle(imlist)

# Create the orientations list: half upright, half inverted
orilist = [0,1]*(len(imlist)/2)

# Randomize the orientation order
rnd.shuffle(orilist)


#===============================
# Creation of window and stimuli
#===============================

# Open a window

# Define trial start text
text = "Press spacebar to start the trial"

# Define the bitmap stimuli (contents can still change)
# Define a bubble (position and size can still change)


#==========================
# Define the trial sequence
#==========================

# Define a list of trials with their properties:
#   - Which image (without the suffix)
#   - Which orientation
stim_order = []
for im, ori in zip(imlist, orilist):
    stim_order.append({'im': im, 'ori': ori})

trials = data.TrialHandler(stim_order, nReps=1, extraInfo=exp_info,
                           method='sequential', originPath=datapath)


#=====================
# Start the experiment
#=====================

for trial in trials:
    
    # Display trial start text
    
    # Wait for a spacebar press to start the trial, or escape to quit
    
    # Set the images, set the orientation
    im_fname = os.path.join(impath, trial['im'])
    trial['ori']
    
    # Empty the keypresses list
    keys = []  
    
    # Start the trial
    # Stop trial if spacebar or escape has been pressed, or if 30s have passed
    while not response and time < timelimit:
        
        # Switch the image
        
        # Draw bubbles of increasing radius at random positions
        for radius in range(n_bubbles):  
            radius/2.
            pos = ((rnd.random()-.5) * scrsize[0],
                   (rnd.random()-.5) * scrsize[1] )

        # For the duration of 'changetime',
        # Listen for a spacebar or escape press
        while time < changetime:
            if response:
                break

    # Analyze the keypress
    if response:
        if escape_pressed:
            # Escape press = quit the experiment
            break
        elif spacebar_pressed:
            # Spacebar press = correct change detection; register response time
            acc = 1
            rt
    else:
        # No press = failed change detection; maximal response time
        acc = 0
        rt = timelimit
    
    # Add the current trial's data to the TrialHandler
    trials.addData('rt', rt)
    trials.addData('acc', acc)

    # Advance to the next trial


#======================
# End of the experiment
#======================

# Save all data to a file
trials.saveAsWideText(data_fname + '.csv', delim=',')

# Quit the experiment