Calling a repeated routine from an imported script

Looking at the auto-generated code from an experiment designed in Builder, it is clear that certain chunks are repeated. For example, a single screen of static text (which waits for a button press before commencing the next block) occurs 10 times in my current experiment, with only the actual text stimulus changing, and so something very similar to the following code is repeated 10 times in the script:

# ------Prepare to start Routine "staticText"-------
t = 0
staticTextClock.reset()  # clock
frameN = -1
continueRoutine = True
# update component parameters for each repeat
respStaticText = event.BuilderKeyResponse()
textStaticText.setText(u'I am the first occurrence of static text')
# keep track of which components have finished
staticTextComponents = [isiStaticText, textStaticText, respStaticText]
for thisComponent in staticTextComponents:
    if hasattr(thisComponent, 'status'):
        thisComponent.status = NOT_STARTED

# -------Start Routine "staticText"-------
while continueRoutine:
    # get current time
    t = staticTextClock.getTime()
    frameN = frameN + 1  # number of completed frames (so 0 is the first frame)
    # update/draw components on each frame
    
    # *isiStaticText* updates
    if t >= 0.0 and isiStaticText.status == NOT_STARTED:
        # keep track of start time/frame for later
        isiStaticText.tStart = t
        isiStaticText.frameNStart = frameN  # exact frame index
        isiStaticText.setAutoDraw(True)
    frameRemains = 0.0 + 0.8- win.monitorFramePeriod * 0.75  # most of one frame period left
    if isiStaticText.status == STARTED and t >= frameRemains:
        isiStaticText.setAutoDraw(False)
    
    # *textStaticText* updates
    if t >= 0.8 and textStaticText.status == NOT_STARTED:
        # keep track of start time/frame for later
        textStaticText.tStart = t
        textStaticText.frameNStart = frameN  # exact frame index
        textStaticText.setAutoDraw(True)
    
    # *respStaticText* updates
    if t >= 1.8 and respStaticText.status == NOT_STARTED:
        # keep track of start time/frame for later
        respStaticText.tStart = t
        respStaticText.frameNStart = frameN  # exact frame index
        respStaticText.status = STARTED
        # keyboard checking is just starting
        event.clearEvents(eventType='keyboard')
    if respStaticText.status == STARTED:
        theseKeys = event.getKeys()
        
        # check for quit:
        if "escape" in theseKeys:
            endExpNow = True
        if len(theseKeys) > 0:  # at least one key was pressed
            # a response ends the routine
            continueRoutine = False
    
    # check if all components have finished
    if not continueRoutine:  # a component has requested a forced-end of Routine
        break
    continueRoutine = False  # will revert to True if at least one component still running
    for thisComponent in staticTextComponents:
        if hasattr(thisComponent, "status") and thisComponent.status != FINISHED:
            continueRoutine = True
            break  # at least one component has not yet finished
    
    # check for quit (the Esc key)
    if endExpNow or event.getKeys(keyList=["escape"]):
        core.quit()
    
    # refresh the screen
    if continueRoutine:  # don't flip if this routine is over or we'll get a blank screen
        win.flip()

# -------Ending Routine "staticText"-------
for thisComponent in staticTextComponents:
    if hasattr(thisComponent, "setAutoDraw"):
        thisComponent.setAutoDraw(False)
# the Routine "staticText" was not non-slip safe, so reset the non-slip timer
routineTimer.reset()

Obviously it would be nice to only repeat this once. My first thought was to put it all into a separate .py script, with the only changes being defining it as a function and passing the actual text to this function as an option, like so:

def staticTextFunc(theText):
    # ------Prepare to start Routine "staticText"-------
    t = 0
    staticTextClock.reset()  # clock
    frameN = -1
    continueRoutine = True
    # update component parameters for each repeat
    respStaticText = event.BuilderKeyResponse()
    textStaticText.setText(theText)
    # keep track of which components have finished
    staticTextComponents = [isiStaticText, textStaticText, respStaticText]
    for thisComponent in staticTextComponents:
        if hasattr(thisComponent, 'status'):
            thisComponent.status = NOT_STARTED # (rest of routine follows)

which I saved in the same folder as the main script, calling it staticTextFunc.py. But then, when I call the following in place of the usual routine code in the main script:

import staticTextFunc
staticTextFunc.staticTextFunc(u'Hi')

It gives the error global name staticTextClock is not defined. Note that staticTextClock = core.Clock() occurs earlier in the main script, as usual in the Builder-generated component initialization part. Moving the component initialization to the staticTextFunc.py script does not solve the problem, it just starts a cascade of errors (name 'core' is not defined; name 'win' is not defined) whose solutions would seem to involve duplicating the entire original script inside the helper script!

I am probably missing something basic; I read that global variables in a script are not available to functions called within separate scripts in Python, but I don’t really understand how this works (coming from an intermediate R background and no coding otherwise). I might add that there is no problem running the experiment from Spyder before these changes are made. I am new to the coder, so please forgive my ignorance!

It sounds like you need to do a little studying up on “variable scope” in python.

Since staticTextFunc() is in another file (which is common), it won’t know about any variables or imports that were declared in the main file.

For functions, the general idea is to avoid global variables as much as possible, and there are a lot of good reasons for this, which I’ll leave to you to research. But what this means practically is that if you want to interact with an object that exists outside of the function (as in, you create it before you call the function, not from within the function), general practice is to pass it to the function in an argument. So quickly adapting what you have here:

# All of the arguments after "theText" are objects that were created and exist outside 
# of the function
def staticTextFunc(theText, textStaticText, staticTextClock, staticTextComponents):

    # Any variables that you declare in this function will die when
    # this function ends. Again, this is usually desirable.

    # ------Prepare to start Routine "staticText"-------
    t = 0
    staticTextClock.reset()  # clock
    frameN = -1
    continueRoutine = True
    # update component parameters for each repeat
    respStaticText = event.BuilderKeyResponse()
    textStaticText.setText(theText)
    # keep track of which components have finished

    # This list was passed as an argument, so removing this line
    #staticTextComponents = [isiStaticText, textStaticText, respStaticText]
    for thisComponent in staticTextComponents:
        if hasattr(thisComponent, 'status'):
            thisComponent.status = NOT_STARTED # (rest of routine follows)

There’s a lot more that could be said about this, but I think I’ll leave this here for now. Try to find some tutorials or articles on variables scope with examples, and hopefully you’ll get another step further in your understanding. Talk to you later!

Dan

P.S. Also keep your eye open to understanding the difference between “mutable” and “immutable” types in python, since this affects how items behave when passed to a function. It’s a little bit more of an advanced topic, but keep those terms in the back of your head for when confusion arises later.

1 Like