3

I often define variables that will never change, i.e. constants, at the top of a script/module. But recently I've been wondering if it makes sense to define them at the function scope level if they are only used by a single method.

LIST_OF_THINGS = [('a', 1), ('b', 2), ('c', 3)]
    
def some_op():
    for thing in LIST_OF_THINGS:
        do_something_with(thing)

While Python has no notion of constants, in this case LIST_OF_THINGS is never modified and only called by a single method, ever. If LIST_OF_THINGS was ever modified, it would be a hardcoded modification in a new release. Now while this is a simple case, I recently had the need for a data structure that later references other methods:

LIST_OF_OPS = {'foo': _call_foo, 'bar': _call_bar}  # Python throws a fit here
    
def _call_foo(): pass
    
def _call_bar(): pass
    
def some_op():
    for op in LIST_OF_OPS:
        LIST_OF_OPS[op]()

So I had two options:

  1. Lower the location of the "constant"' below the referenced methods
  2. Place the "constant" inside some_op

Again, a simple case, but when the structure of a constant or the number of constants is large, they can make the body of a function larger than it should be; this is really the only merit I see in having them defined at the function scope level.

JimmyJames
  • 24,682
  • 2
  • 50
  • 92
pstatix
  • 1,017
  • 10
  • 16

3 Answers3

3

You’re well on your way to making a global.

The principle of data hiding tells us to limit exposure of details. Constants are details. The wider the scope the wider the exposure. The correct scope for variables and constants is the narrowest scope that works. Having them visible to code that doesn’t need them harms readability.

If there is a need to define them elsewhere the answer isn’t a wider scope. It’s passing them to where they are needed. This used to just be called reference passing but the fancy new term for it is pure dependency injection. It lets you separate use from construction. It works for constants just as well.

But if it can be defined where it is used I don’t see any reason to make it visible outside of that. It’s far easier to read the code when you limit scope.

candied_orange
  • 102,279
  • 24
  • 197
  • 315
  • I disagree with the "pass everywhere", unless creating an object that has dependencies. If there are a list of functions that all need access to a particular variable (say a hex constant), and these functions are exposed as a public API, defining them internally to each function is wasteful and making it visible at the global scope makes sense. Again, that's only because they are used in multiple places. – pstatix Feb 25 '21 at 18:33
  • 1
    I am not advocating creating copies everywhere. I’m denouncing using globals to solve this problem. You should not be reaching out to find things. When you do you create hidden dependencies that make your code brittle. – candied_orange Feb 25 '21 at 18:37
  • So 4 functions all use the same variable internally; each has its own responsibility and exposed via an API. Why would declaring the variable, say `ROOT_ID = 0x0115`, be creating a hidden dependency? I would agree that create a global *variable* would be bad, but a constant should be perfectly fine. – pstatix Feb 25 '21 at 18:47
  • I'm not sure if you know this or not but `global` is a keyword and has a specific meaning in Python that is different from the general term 'global'. I think you mean it in the general programming sense here but it's not clear. Everything declared at the module scope is `global` in Python. It's impossible to avoid using `global` declarations in Python. – JimmyJames Feb 25 '21 at 19:14
  • When you put it that way the answer is an object that declares the functions publicly and the constant privately. I know they aren’t keywords in python. Just use an underscore and be an adult. – candied_orange Feb 25 '21 at 19:41
  • @JimmyJames Yes I am aware, `global` enables altering a variable defined at the global scope. None of the variables in question are ever altered, only accessed for read; the `global` keyword is therefore not used. – pstatix Feb 25 '21 at 19:55
  • 2
    @madeslurpy the shared mutable state of a global variable is indeed worse than a shared constant. However, a global/widely-scoped constant is still harmful to readability. A limited scope gives me confidence that I know what I’m impacting when I redefine the constant. Please limit what I have to think about when I code. – candied_orange Feb 25 '21 at 20:21
  • @madeslurpy That comment wasn't meant to be addressed to you. But the point is all module level declarations are called globals in Python regardless of whether that keyword is used. This is distinct from the more general programming concept of 'global'. – JimmyJames Feb 25 '21 at 20:35
  • @JimmyJames they aren’t if you stick an underscore at the start of a name. Even in Java private is an agreement not a security measure thanks to reflection. Is there something I could edit into my answer to make this clearer? – candied_orange Feb 25 '21 at 20:45
  • @candied_orange I'm not sure I follow you and that is my only concern. See the definition of the built-in [`globals()`](https://docs.python.org/3/library/functions.html#globals). As you point out, a Python `global` isn't necessarily global in scope. I just would clarify whether you mean global in the general sense. I kind of want to call it big-G Global but that wouldn't mean anything to anyone. – JimmyJames Feb 25 '21 at 20:52
  • @JimmyJames I mean global in the evil sense. That thing that is known by god knows what that you can't touch because you've no idea what it will break when you do. The thing that forces you to keep reading file after file desperately hoping that you finally understand all the possible implications. You know, that global. – candied_orange Feb 25 '21 at 23:56
1

Your second example is a little weird. It's not clear why you define this as a dict when you only show using the values. If you need the key and the value, I would generally prefer the for key, value in dict.items() idiom.

That aside, there's a third option here: don't declare a variable at all. You could simply do this:

def some_op():
    for op in [_call_foo, _call_bar]:
        op()

If you come from a 'curly-brace' background as I do, this might seem grotesquely wrong but as I have spent more time writing Python, I've actually started to question the idea that all 'constants' need to be given a name. I think it actually hurt readability in a lot of cases, especially if they are always declared at the top of the file. Why force the person reading your code to pause, scroll somewhere else and then come back to where they were. There's really no sense in that and I think it's just one of those things that's done because that's what you are 'supposed' to do.

Of course, this might be a trivial example and if you have a longer list, you would want to give it a name because of that. I think that really comes down to why you are giving something a name. Is it just to organize the code or does the name have some meaning? For example if you name a list of numbers, FIRST_TEN_PRIMES is far more interesting and informative than NUMBERS. In the latter case, I don't see much point in declaring it at all.

As far as the scope goes, this is a little more tricky. In general a tighter scope is preferred. If you only need it in the one method, I would generally declare it there. However, if you think this might be more generally useful or needed in other parts of the code, it might be worth putting it at the module scope. You can also suggest that it is 'private' with an underscore prefix. There has been at least one time where I had a bug because I didn't realize that my local declaration of a name was shadowing a module declaration. Something to think about. Again, I would stick to the tightest scoping possible in most cases.

JimmyJames
  • 24,682
  • 2
  • 50
  • 92
  • [Meaningless intermediate names](https://softwareengineering.stackexchange.com/a/366370/131624) are wrong in any language. Please don't blame the curly-brace languages for the stupid things people do with them. – candied_orange Feb 25 '21 at 23:22
  • @candied_orange Please don't make (incorrect) assumptions about my meaning. I didn't blame the language at all. I blame the people who pushed ridiculous 'rules' like single-return, Hungarian warts, interfaces for every class, etc. I also blame the people who blindly followed those rules without question. It's about culture. – JimmyJames Feb 26 '21 at 16:12
  • The correct response to an incorrect assumption is to edit your answer and remove the ambiguity that caused it. I agree with your assessment of the “ridiculous rules” with the exception of single return. That rule is language dependent. It’s valid in languages that don’t have finally, destructors, or some other method of unifying cleanup. This is important even when there is no cleanup to do now because it may need to be added later. Which is why newer languages added ways to do that cleanly. Anyway, names are very important but putting them where they're not needed is bad in any language. – candied_orange Feb 26 '21 at 18:14
  • There is no ambiguity, your interpretation was presumptive. – JimmyJames Feb 26 '21 at 19:32
  • @candied_orange Frankly, I don't get why you are telling me to 'remove the ambiguity' when your answer is literally using the term global in an ambiguous way (i.e. differently than the question uses it) and have not fixed that. – JimmyJames Feb 26 '21 at 19:42
  • The point of these comments is to encourage improvements to the posts above them. All I seem to be doing is antagonizing you. For that I am sorry. I do agree with much of what you said. I’ll give you my upvote now. Let me know if you need anything else. – candied_orange Feb 26 '21 at 21:18
0

My "policy" is:

  • For small, simple things, inside some_op,
  • For more complex things, directly above some_op,
  • For things that I treat as ad-hoc configuration, even if they are used only in one function (e.g. switch for a library version), top of file.
Pedja
  • 1