I've been working on my snake game using tkinter for gui. The problem is in one of the functions.
The function is supposed to draw body fragments and fruit using Canvas().create_rectangle and Canvas().create_oval respectively. So instead of writing separate code for each case, I decided to write it once and just modify it with 'shape' argument, which would be either 'rectangle' or 'oval'. Function must also return the id of drawn element for its own purposes. Originally that part of the code looked like this:
exec(
"""
segment_id = self.grid.create_{}(coords,
[coord + self.pxSize for coord in coords],
fill=color, tag=tag, width=0)
""".format(shape))
return segment_id
And instead of getting a common NameError: name 'segment_id' is not defined
, I got NameError: name 'self' is not defined
.
After googling I only found this:
ldict = {}
exec('var = something', globals(), ldict)
var = ldict['var']
Which solves NameError: name 'segment_id' is not defined
, but doesn't solve the other one. So with scientific poke method I fixed it by passing locals() to its 'globals' parameter. It works and now I'm confused even more.
Here's the code:
class Game(Tk):
def __init__(self):
...
# ...
def drawSegment(self, position, color, tag, shape, id_=None):
coords = self.field.from_1D_to_2D(position)
coords = [i * self.pxSize for i in coords]
# id > 1, otherwise deletes background
if id_ and id_ > 1:
self.grid.delete(id_)
# ???
ldict = {}
exec(
"""
segment_id = self.grid.create_{}(coords,
[coord + self.pxSize for coord in coords],
fill=color, tag=tag, width=0)
""".format(shape), locals(), ldict)
segment_id = ldict['segment_id']
return segment_id
# ...
What I need is the answer on why did it work and what's going on even.
Using exec like this is unnecessary, and as you've run into, quite confusing; so I've got two different answers for you.
What's going on?
When you pass globals=globals(), locals=ldict
to exec
, it will execute the code in a scope that can only see globals
and ldict
; so in particular, it will not see any variables that are local to the drawSegment
method local scope. Since self
is only defined in this local scope, in order to reference self
from inside your exec
call, you need to pass in locals()
, not just globals()
.
What should you do instead?
Rather than dynamically executing the whole block of code based on the value of shape
, you can just dynamically look up the desired create_* method based on shape
:
creator = getattr(self.grid, 'create_{}'.format(shape))
segment_id = creator(coords, [coord + self.pxSize for coord in coords],
fill=color, tag=tag, width=0)
If you know there are only two possibilities for shape, depending on personal taste you might make this even more obvious:
creator = self.grid.create_oval if shape == 'oval' else self.grid.create_rectangle