Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to avoid using global variables?

I use global variables but I've read that they aren't a good practice or pythonic. I often use functions that give as a result many yes/no variables that I need to use in the main function. For example, how can I write the following code without using global variables?

def secondary_function():
    global alfa_is_higher_than_12
    global beta_is_higher_than_12

    alfa = 12
    beta = 5

    if alfa > 10:
        alfa_is_higher_than_12 = "yes"
    else:
        alfa_is_higher_than_12 = "no"

    if beta > 10:
        beta_is_higher_than_12 = "yes"
    else:
        beta_is_higher_than_12 = "no"

def main_function():
    global alfa_is_higher_than_12
    global beta_is_higher_than_12

    secondary_function()

    if alfa_is_higher_than_12=="yes":
        print("alfa is higher than 12")
    else:
        print("alfa isn't higher than 12")

    if beta_is_higher_than_12=="yes":
        print("beta is higher than 12")
    else:
        print("beta isn't higher thant 12")

main_function()
like image 842
Carl Avatar asked Nov 29 '22 12:11

Carl


2 Answers

One could ask what reasons you might have to structure your code like this, but assuming you have your reasons, you could just return the values from your secondary function:

def secondary_function():

  alfa = 12
  beta = 5

  if alfa > 10:
      alfa_is_higher_than_12 = "yes"
  else:
      alfa_is_higher_than_12 = "no"

  if beta > 10:
      beta_is_higher_than_12 = "yes"
  else:
      beta_is_higher_than_12 = "no"

  return alfa_is_higher_than_12, beta_is_higher_than_12


def main_function():

  alfa_is_higher_than_12, beta_is_higher_than_12 = secondary_function()

  if alfa_is_higher_than_12=="yes":
      print("alfa is higher than 12")
  else:
      print("alfa isn't higher than 12")

  if beta_is_higher_than_12=="yes":
      print("beta is higher than 12")
  else:
      print("beta isn't higher thant 12")
like image 27
sjc Avatar answered Dec 06 '22 04:12

sjc


The term "Pythonic" doesn't apply to this topic--using globals like this is poor practice in any programming language and paradigm and isn't something specific to Python.

The global keyword is the tool that Python provides for you to opt out of encapsulation and break the natural scope of a variable. Encapsulation means that each of your components is a logical, self-contained unit that should work as a black box and performs one thing (note: this one thing is conceptual and may consist of many, possibly non-trivial, sub-steps) without mutating global state or producing side effects. The reason is modularity: if something goes wrong in a program (and it will), having strong encapsulation makes it very easy to determine where the failing component is.

Encapsulsation makes code easier to refactor, maintain and expand upon. If you need a component to behave differently, it should be easy to remove it or adjust it without these modifications causing a domino effect of changes across other components in the system.

Basic tools for enforcing encapsulation include classes, functions, parameters and the return keyword. Languages often provide modules, namespaces and closures to similar effect, but the end goal is always to limit scope and allow the programmer to create loosely-coupled abstractions.

Functions take in input through parameters and produce output through return values. You can assign the return value to variables in the calling scope. You can think of parameters as "knobs" that adjust the function's behavior. Inside the function, variables are just temporary storage used by the function needed to generate its one return value then disappear.

Ideally, functions are written to be pure and idempotent; that is, they don't modify global state and produce the same result when called multiple times. Python is a little less strict about this than other languages and it's natural to use certain in-place functions like sort and random.shuffle. These are exceptions that prove the rule (and if you know a bit about sorting and shuffling, they make sense in these contexts due to the algorithms used and the need for efficiency).

An in-place algorithm is impure and non-idempotent, but if the state that it modifies is limited to its parameter(s) and its documentation and return value (usually None) support this, the behavior is predictable and comprehensible.

So what does all this look like in code? Unfortunately, your example seems contrived and unclear as to its purpose/goal, so there's no direct way to transform it that makes the advantages of encapsulation obvious.

Here's a list of some of the problems in these functions beyond modifying global state:

  • using "yes" and "no" string literals instead of True/False boolean values.
  • hardcoding values in functions, making them entirely single-purpose (they may as well be inlined).
  • printing in functions (see side effects remark above--prefer to return values and let the calling scope print if they desire to do so).
  • generic variable names like secondary_function (I'm assuming this is equivalent to foo/bar for the example, but it still doesn't justify their reason for existence, making it difficult to modify as a pedagogical example).

But here's my shot anyway:

if __name__ == "__main__":
    alpha = 42
    beta = 6
    print("alpha %s higher than 12" % ("is" if alpha > 12 else "isn't"))
    print("beta %s higher than 12" % ("is" if beta > 12 else "isn't"))

We can see there's no need for all of the functions--just write alpha > 12 wherever you need to make a comparison and call print when you need to print. One drawback of functions is that they can serve to hide important logic, so if their names and "contract" (defined by the name, docstring and parameters/return value) aren't clear, they'll only serve to confuse the client of the function (yourself, generally).

For sake of illustration, say you're calling this formatter often. Then, there's reason to abstract; the calling code would become cumbersome and repetitive. You can move the formatting code to a helper function and pass any dynamic data to inject into the template:

def fmt_higher(name, n, cutoff=12):
    verb = "is" if n > cutoff else "isn't"
    return f"{name} {verb} higher than {cutoff}"

if __name__ == "__main__":
    print(fmt_higher("alpha", 42))
    print(fmt_higher("beta", 6))
    print(fmt_higher("epsilon", 0))
    print(fmt_higher(name="delta", n=2, cutoff=-5))

We can go a step further and pretend that n > cutoff was a much more complicated test with many small steps that would breach single-responsibility if left in fmt_higher. Maybe the complicated test is used elsewhere in the code and could be generalized to support both use cases.

In this situation, you can still use parameters and return values instead of global and perform the same sort of abstraction to the predicate as you did with the formatter:

def complex_predicate(n, cutoff):
    # pretend this function is much more 
    # complex and/or used in many places...
    return n > cutoff

def fmt_higher(name, n, cutoff=12):
    verb = "is" if complex_predicate(n, cutoff) else "isn't"
    return f"{name} {verb} higher than {cutoff}"

if __name__ == "__main__":
    print(fmt_higher("alpha", 42))
    print(fmt_higher("beta", 6))
    print(fmt_higher("epsilon", 0))
    print(fmt_higher(name="delta", n=2, cutoff=-5))

Only abstract when there is sufficient reason to abstract (the calling code becomes clogged or when you're repeating similar blocks of code multiple times are classic rules-of-thumb). And when you do abstract, do it properly.

like image 122
ggorlen Avatar answered Dec 06 '22 02:12

ggorlen