
On Sat, Nov 28, 2020 at 12:57:12PM +1100, Cameron Simpson wrote:
Got a nice example of somewhere where shadowing would be useful and hard to do some task otherwise?
Mocking. For example:
https://docs.python.org/3/library/unittest.mock.html#patch-builtins
Monkey-patching. One nice trick I've used is if I have a function that is excessively verbose, printing oodles of unwanted output, I can monkey-patch the owner's module, without affecting other calls to print:
import library library.print = lambda *args, **kw: None result = library.noisy_function()
There are other ways to get the same result, of course (change sys.stdout, etc) but I find this works well.
Another technique I've used occassionally:
I need diagnostics to debug a function, but I don't want to insert debugging code into it. (Because I invariably forget to take it out again.) But I can sometimes monkey-patch a built-in to collect the diagnostics. I don't have to remove my debugging code from the source because it isn't in the source.
I've even shadowed entire modules:
- copy module A to a new directory; - modify the copy; - cd into the directory; - run your code as normal, with the sole difference that "import A" will give you the patched (experimental) A rather than the regular A.
But most important of all: we must be able to shadow module level globals with locals with the same name, otherwise the addition of a global "x" will suddenly and without warning break any function that happens to also use "x".
The problem is, if your inner block scope must not reuse variable names in the outer function scope, well, what's the advantage to making them seperate scopes?
Lifetime, but in terms of referencing resources and also accidental (mis)use of something beyond where it was supposed to be used. I used to do short {...} scopes in C all the time for this purpose.
I keep hearing about "accidental misuse" of variables. Are people in the habit of coding by closing their eyes and mashing the keyboard until the code compiles?
*wink*
Seriously, how big and complex are your functions that you can't tell whether you are re-using a variable within a single function and are at risk of doing so *accidentally*?
Martin Fowler suggests that (Ruby) functions with more than, say, half a dozen lines are a code smell:
https://www.martinfowler.com/bliki/FunctionLength.html
75% of his functions are four lines or less. Even if you think this is a tad excessive, surely we're not in the habit of writing functions with hundreds of lines and dozens of variables, and consequently keep accidentally re-using variables?
This seems especially ... odd ... in a language with declarations like C, where you ought to easily be able to tell what variables are in use by looking at the declarations.
Unless of course you are in the habit of scattering your declarations all through the function, an anti-pattern that block-scopes encourages. "Only block-scopes can save us from the incomprehensible mess of code that uses block-scopes!" *wink*
Analogy: I think most of us would consider it *really weird* if this code was prohibited:
a = None
def func(): a = 1 # Local with the same name as the global prohibited.
Yes, but a function is a clean new scope to my mind. (Yes, closures.) As opposed to this:
f = None # define a lifespan for "f" using the fictitious "as new" syntax. with open("foo") as new f: process file f # f is None again
I'd be all for forbidding that, and instead requiring a different name for the with-statement "f".
This implies that in your mind, with statements are *not* "clean new scopes" like functions are. If they were, you would be quite comfortable with the with statement introducing a separate variable that happened to be called f but was distinct from the local variable "f" or global variable "f" outside of the block.
I think that's telling. Functions make naturally distinct scopes, which is why we're comfortable with locals and globals with the same name.
Likewise comprehensions: they look and feel self-contained, which is why people were surprised that they leaked. Nobody said that we ought to prohibit the comprehension from using the same names as the surrounding function.
But blocks don't make natural scopes. We can force them to be separate scopes, but even (some) people who want this feature don't actually think of them as naturally distinct scopes like functions with independent sets of names. If a function and a block use the same name, they see that as a clash, rather than two distinct, independent, non-clashing scopes like functions or comprehensions.
One possible advantage, I guess, is that if your language only runs the garbage collector when leaving a scope, adding extra scopes helps to encourage the timely collection of garbage. I don't think that's a big advantage to CPython with it's reference counting gc.
I'm not concerned directly with garbage collection so much as preventing use of a name beyond its intended range. Which is the next bit:
[...]
Why do you care about the *name* "i" rather than whatever value is bound
Because of intent. Why do we encapsulate things in functions? It isn't just releasing resources and having convenient code reuse, but also to say "all these names inside the function are only relevant to it, and are _not_ of use after the function completes".
That cannot possibly be true, because we use the same *names* after the function completes all the time. That's why functions introduce a new scope, so that distinct scopes can use the same names without stomping on each other!
If we cared about the *names*, as you insist, we would insist that names in functions have to be globally unique. But we don't.
Functions are their own distinct scopes so that we can use names inside a function without caring whether those names are used elsewhere.
But I can't do the same for block scopes if names are forced to be unique. Instead of being independent, the scopes are strongly coupled: every time I use a name inside a block, I have to care whether it is already used outside of that block.
So blocks are not actually independent scopes under the Java semantics.
(They are independent in C.)
[...]
But I don't get why I might care about the lifetime of a *name*.
Maybe not. But I very much do.
I think you actually care about the value bound to the name, but mistake it for the name itself. Consider:
with open(...) as new f: # new scope, the *name* f is only allowed in this block process(f) g = f # smuggle the *value* of f outside the block do_something_with(g)
If you care about the *name* f, then you will be fine with that. Nobody is reusing f, so it's all good!