LMc LMc - 1 month ago 4
Python Question

Why is my call to a variable that exists in the local scope not working in Python?

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())

Answer

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

Comments