psychopy.org | Reference | Downloads | Github

Functions vs classes

When writing scripts, I often use functions to capture parts of the task. For example, I might define a function called ‘instructions’ and call it several times during the script, passing the appropriate text as an argument.

I see that some people use classes for the very same purpose. I wonder what the (dis)advantages of each are, and when do you (experienced coders) use each for capturing parts of the task.

1 Like

Good question. :slight_smile:

Using functions is a good start in making your code neater. Classes are a neat way of going a little further with that by grouping together functions and variables (aka attributes) that have some logical connection.

Classes essential define a type of object and if it makes sense to think of your thing as an object then you probably want a class. For instance the psychopy Window is a class and it has various methods (functions) like draw() but stores various attributes, like its background color and its size, the id of the window in the operating system etc, all of which it needs to know about to perform its functions. The other great thing with a class is that you can store it in your own file of python tools and reuse it at will. (That’s essentially what the psychopy library is)

How about a concrete example. Well, let’s say you want a fixation point that stands out in front of a photographic image. You could have white dot, sitting on a black dot sitting on a grey dot.

With no function at all you could do:

# top of script
fixWhite = visual.Circle(win, size=5, units='pix', color='grey')
fixBlack = visual.Circle(win, size=7, units='pix', color='black')
fixGrey = visual.Circle(win, size=10, units='pix', color='grey')

# each time you draw fixation
fixGrey.draw()
fixBlack.draw()
fixWhite.draw()

##Using a function
Those three lines of drawing would occur quite often and would be neater as a function so you’d have:

# top of script
fixWhite = visual.Circle(win, size=5, units='pix', color='grey')
fixBlack = visual.Circle(win, size=7, units='pix', color='black')
fixGrey = visual.Circle(win, size=10, units='pix', color='grey')
# create func
def drawFix():
    fixGrey.draw()
    fixBlack.draw()
    fixWhite.draw()

# re-use during script
drawFix()

##Make a class

The class approach allows you to store your ring objects and also handle the drawing method (and potentially others)

class FixationRings:
    def __init__(self, win, color1, color2, color3):
        self.ring1 = visual.Circle(win, size=5, units='pix', color=color1)
        self.ring2 = visual.Circle(win, size=7, units='pix', color=color1)
        self.ring3 = visual.Circle(win, size=10, units='pix', color=color1)
    def draw(self):
        self.ring1.draw()
        self.ring2.draw()
        self.ring3.draw()

#create an instance of that object/class
fix = FixationRings(win, 'white', 'black', 'grey')

fix.draw()  # as needed

Re-use your class

The last step in writing re-usable code is that you move the definition of your class into a separate file that all your experiments can see (added to the python path) so that in your script you just have:

from psychopy import visual
import mytools

win = visual.Window([80,600])
fix = mytools.FixationRings(win, 'white','black','grey')

fix.draw()
win.flip()
7 Likes

Thanks, Jon.

Although the differences in your example between the function and the class seem artificial, no? You could just as well define the visual components inside the function as you did inside a class, and you could pass the colors as arguments to the function.

I see how functions and classes differ but I think I’ll need some more experience to get a good feel for when to use one or the other. Although I suspect my uncertainty might also signal that for the kinds of scripts I’ve been writing so far it doesn’t matter much what you use.

Yes, I was making exactly that point!

You can technically achieve everything either way. So the question is which version looks neater to you when writing the code (I prefer not to have to remember what has to be passed in each time so an object is often neater)

1 Like

To add to @jon’s answer:
Whether to use a function or a class is usually a matter of their size. In general, functions are smaller than classes since the latter can contain the first but not vice versa (a function can only call objects but not define classes explicitly).

Let me give you an example:
I created a class DotProbe(object): that implements a standard Dot Probe paradigm. Of course, this can be done without creating objects.
Additionally, I needed a variation of the Dot Probe (e.g. same pictures used) with flickering stimuli for a steady state visually evoked potential paradigm (ssVEP). After having created the Dot Probe as a class I was able to define class ssVEP(DotProbe): i.e. the class ssVEP is a subclass of the DotProbe class defined earlier and therefore inherits all its functions and attributes.
Now we can define the initialization function of the ssVEP class as def __init__(self, dotProbe=DotProbe()): i.e. you are able to pass a DotProbe object when creating an ssVEP (if you don’t, a default DotProbe object DotProbe() will be initialized and used as a template). Within the initialization function, you can extract all values from the DotProbe that have to be constant in the ssVEP paradigm (e.g. self.interTrialTime = dotProbe.interTrialTime).

The resulting main experiment looks very tidy:
from DotProbe import DotProbe from ssVEP import ssVEP

dotProbe = DotProbe() dotProbe.prepare(cat1Pairs=False, cat2Pairs=True, matchedDistractors=True) #input dialog dotProbe.presentText(dotProbe.welcomeText) #welcome text dotProbe.run()

ssvep = ssVEP(dotProbe) #creates ssVEP according to dotProbe object ssvep.prepare(cat1Pairs=True, cat2Pairs=True, matchedDistractors=True) ssvep.run()

I hope this helped to clarify your question further.

Best regards
Mario

1 Like

Thanks both, very helpful!

That way, the circles would not only be drawn, but also entirely re-created every time you invoked the function. This is typically not what you want!! (performance issues)

Jon’s example demonstrates two principal design patterns in object-oriented programming (OOP): you create objects (instances of classes) that have an internal state that can be retrieved (and possibly modified) at a later time; and we have encapsulation in the sense that all stimulus components (circles) and the corresponding draw() method are grouped together in a single place (the FixationRings class). We do not have to use global variables here, unlike in the example using the function.

Thus, the question whether and when a class would be a better choice than an ordinary function often boils down to the question, do you have to store some states (e.g., variables) specifically belonging to an object, and that object only? If yes, then you will most likely want to use a class.

If eventually, however, you find yourself writing classes that contain empty __init__() methods, you probably don’t want to use classes and should consider implementing everything as simple functions only, as you’re not interested in storing a state inside the object.

2 Likes