2

This code is part of a function. When I run the function line by line with the proper arguments, it runs fine, but when I call the function it doesn't seem to work. The following three lines of code and their output are from the function call:

print(locals().keys())
dict_keys(['allpost', 'allpre', 'pre', 'post'])

You can see that 'allpre' exists in my local scope. This line of code, also in the function, works as well:

print(locals()['allpre'])
[15.0, 12.0, 10.0, 6.0, 12.0, 8.0, 5.0, 3.0]

But for some reason this code doesn't work:

print([locals()[k] for k in ['allpre']])
Traceback (most recent call last):
File "prepost.py", line 85, in <module>      parentinfantstats=delete.bystats(mod='parentinfant',assessment=".*pii[1-3]plus",dta=sortFiltbyMod.copy())
File "/mnt/m/safecare/prepost/delete.py", line 37, in bystats
print([locals()[k] for k in ['allpre']])
File "/mnt/m/safecare/prepost/delete.py", line 37, in <listcomp>
print([locals()[k] for k in ['allpre']])
KeyError: 'allpre'

Does anyone have a suggestion for what the problem might be? I would post an example, but can't seem to duplicate the problem.

This is the whole function:

import re
from statistics import mean,median,stdev

def bystats(*,mod,assessment,dta):
    varz=dta[mod]               
    alab=[i for i in varz if re.match(assessment.lower(),i.lower())]
    alab.insert(0,'prepost')
    alab.insert(0,'cact_familycodenormalized')
    alst=[varz[i] for i in alab] # [ID,prepost,assessment]
    bymodprepost=[list(row) for row in zip(*alst) if row[1] in [1,2]] # [ID,prepost,assessment] if prepost 1 or 2
    bymodpost=[i for i in bymodprepost if i[1]==2] # [ID,prepost,assessment] if prepost was 2 (post)
    bymodpre=[i for i in bymodprepost if i[0] in [ids[0] for ids in bymodpost] and i[1]==1] # [ID,prepost,assessment] if ID had a post test

    allpre,allpost,allch,allpctch=[],[],[],[]
    for pre in bymodpre:
        post=[i for i in bymodpost if i[0].upper().strip()==pre[0].upper().strip()][0] # find matching post test
        if any([True for i in pre[2:]+post[2:] if float(i)<0]): continue # cannot have negative number
        sumpre=sum([float(i) for i in pre[2:]]) # sum of pre test assessments
        allpre.append(sumpre)
        sumpost=sum([float(i) for i in post[2:]]) # sum post test assessments
        allpost.append(sumpost)
        ch=sumpost-sumpre       # change from pre to post
        allch.append(ch)
        pctch=round(ch/sumpre*100,1) # percent change from pre to post
        allpctch.append(pctch)
    print(locals().keys())
    print(locals()['allpre'])
    print(locals()[k] for k in ['allpre'])

And this is the function call:

parentinfantstats=delete.bystats(mod='parentinfant',assessment=".*pii[1-3]plus",dta=sortFiltbyMod.copy())
LMc
  • 12,577
  • 3
  • 31
  • 43
  • Post the function. – Bahrom Jun 15 '16 at 20:40
  • I can not find any problem. It should work. It works for me. Check locals() again just before running final print. – Rohanil Jun 15 '16 at 20:45
  • It's still there. These three print statements are in a row. Just posted the whole function. – LMc Jun 15 '16 at 20:48
  • are you sure that above 2 print statements printing the same message which you have mentioned? Use `print([locals()[k] for k in ['allpre']])` for 3rd statement. If still doesn't work try removing second print statement. – Rohanil Jun 15 '16 at 20:58

4 Answers4

2

List comprehensions have their local scope since Python 3.0. This was to prevent scope leak.

https://docs.python.org/3.0/whatsnew/3.0.html

[...] note that list comprehensions have different semantics: they are closer to syntactic sugar for a generator expression inside a list() constructor, and in particular the loop control variables are no longer leaked into the surrounding scope.

Community
  • 1
  • 1
user1238367
  • 505
  • 4
  • 11
1

Calling locals from within a list comprehension won't work the way you expect it to (in Python 3). That's because the main body of the list comprehension gets run within its own local namespace, just like it was a function returning a list. You can see this in your traceback, where <listcomp> shows up as one of the stack levels.

In Python 2, list comprehensions didn't do this (but generator expressions did). A side effect of their old behavior was that the iteration variable (e.g. k) would "leak" out into the surrounding namespace, which was often very unexpected. List comprehensions can also break when they're run at the top level of a class, as the class variables previously defined won't be accessible to the comprehension function (since classes don't have scopes).

Blckknght
  • 100,903
  • 11
  • 120
  • 169
  • I put these print statements in to troubleshoot. The final line of the function is `return {k:[op(eval(k)) for op in [mean,median,min,max,len,stdev]] for k in ['allpre','allpost','allch','allpctch']}`. Is there a way to get this comprehension to access the function's namespace? – LMc Jun 15 '16 at 21:01
  • In general, you shouldn't need to be passing variable names around. But since you need the names to be the keys of the output dictionary, you can't skip them completely. Maybe you could pass the values directly along with their names, rather and not need `eval`? Untested: `return {k: op(v) for op in [[mean,median,min,max,len,stdev]] for k, v in [('allpre', allpre),('allpost', allpost), ('allch', allch), ('allpctch', allpctch)]}` Better might be to return just a sequence of values, where the order implies which value is which. – Blckknght Jun 15 '16 at 21:08
0

Try whitout [] in your print

print(locals()[k] for k in ['allpre'])
Reinier Hernández
  • 428
  • 1
  • 6
  • 22
0

You're on Python 3, where a list comprehension has its own scope with its own local variables. allpre is not one of the comprehension's local variables.

user2357112
  • 260,549
  • 28
  • 431
  • 505