Can I keep an expression with side effects in the Watch window from being reevaluated?


I notice that if I have an item in the Watch window such as N = N + 1, then every time I continue the program and then break, this expression is re-evaluated.

When the debug script evaluates the expression the first time, is there a way it can tell the IDE that there were side effects (or possible side effects), so that the expression will not be evaluated again without some specific instruction from the user?


pminaev wrote Jan 10 at 7:13 PM

Right now, we basically treat all expressions in Watch as side-effect-free - there are no flags and such internally to discriminate, much less any way to set those flags.

Ideally, we'd want to figure out if something is side-effect-free or not more or less automatically. For example, this particular code obviously isn't, and we can tell that by parsing it and seeing that it has an assignment.

In fact, arguably, this particular code should not be valid in Watch at all, because assignment is a statement rather than expression. Currently, we just hand it over to exec as is without trying to validate it in any way, which makes it possible to use all kinds of weird stuff like assignments and loops there (and potentially break the debugger if you enter something like while True: pass as a watch). Statements don't really make sense for Watch anyway because they always return None; for cases where you want to run them just for their side effects, Immediate window and Debug Interactive are better options. So the immediate action item is that we should validate expressions in Watch, and reject those that cannot be parsed as expressions.

It still leaves a question of what to do with bona fide expressions that have side effects (e.g. mutating function calls). We can't reasonably detect those in a generic fashion, but we can make some assumptions and expose some options. We do in fact already have a set of options for the Debug Interactive that deal with the same issue - it uses eval to drive code completion, and so the issue of what to do with side effects came up there as well. We currently expose the following options in Tools -> Options -> Python Tools -> Debug Interactive Window:
  • Never evaluate expressions
  • Never evaluate expressions containing calls <-- default
  • Always evaluate expressions
We could add a new property page in Options with similar choices for Watch, with the same default setting (it seems reasonable to assume that simple attribute access and indexing won't mutate). Only instead of "never evaluate", it would read "explicitly evaluate". So e.g. if you have the "expressions containing calls" option selected, and add a watch like x.add(123), we would show that Watch item as grayed out, with a refresh icon where the value should be and text explaining that it must be explicitly evaluated to produce the results. Basically, copy the C# debugger behavior for this.

We could also add some knowledge of commonly used builtins that are side-effect-free, like len(), so that we are not unnecessarily pessimistic when determining whether the expression has side effects or not. This is a bit trickier though as we'd then need to figure out if that len() actually refers to a built-in len(), or to something overriding it in the current scope. We could still do it by evaling len itself and inspecting the object which it points to, and making a decision based on its __name__ and __module__ (or __objclass__ for methods), and store that list in a documented place such that it can be customized.

pminaev wrote Jan 15 at 10:15 PM

slishak wrote May 27 at 4:33 PM

(I've come here from the related discussion https://pytools.codeplex.com/discussions/545059)

I think that the option of making the user "explicitly evaluate" expressions would be the best solution. The functionality is already there for iterators, and it puts the onus on the user to decide whether or not a function call has side effects. I'd personally rather click one more button to see the result of an expression than have to maintain and rely on a list of side-effect-free functions.

pminaev wrote May 27 at 5:10 PM

It's slightly different for iterators, because there we eval on expand, whereas here we'd need to use a refresh button on the expression itself (which VS provides, I just need to figure out how to make it appear there).

Generally speaking, we should revisit the whole deferred execution thing, with iterators as well. Right now, when you expand child nodes in a tree of objects, we re-evaluate the entire object chain that brought you there. So e.g. if you have a.b.c, and you expand a, then expand b, then expand c, children of a are evaluated on every step. For normal objects it's just not a big deal, but for iterators this mean that if you do something like it[0].x, you won't actually be able to expand x - because it would require re-evaluating it[0], and it is already exhausted. It will also become a bigger problem once we add this feature, since then it's not clear how the expansion of children of an expression with side effects would work - do we re-run the expression on every such expansion (and is it obvious?), or do we just block it?

What it should rather do is only eval the expression once, and cache the object for reuse so long as it still shows up in one of the debugger windows, until something triggers a refresh (i.e. a step, or user explicitly clicking the "Refresh" button). Then everything works as it should.