Friday, January 1, 2021

Stack Abuse: How to Use Global and Nonlocal Variables in Python

Introduction

In this article we'll be taking a look at Global and Non-Local Variables in Python and how you to use them to avoid issues when writing code.

We'll be starting off with a brief primer on variable scopes before we launch into the how and why of using global and non-local variables in your own functions.

Scopes in Python

Before we can get started, we first have to touch on scopes. For those of you who are less familiar, "scope" refers to the context in which a variable is defined and how it can be accessed or altered or more specifically - from where it can be accessed.

And in programming, like in life, context is important.

By referencing Python right now, you can infer from context that I am referring to the programming language. In another context however, Python could be a reference to a snake, or a comedic group.

Global and local scopes are how your program understands the context of the variable that you are referencing.

As a rule, variables defined within a function or class (as an instance variable) are local by default, and those outside of functions and classes are global by default.

Local Variables in Python

With that understood, let's see it in action. We'll start by defining a function with it's own local variable inside. In this function we have the variable fruit, which we initialize as a list and print:

def shopping_list():
    fruit = ['apple', 'banana']
    print(fruit)
    
shopping_list()

And as expected, this works like a charm:

['apple', 'banana']

But what happens when we move the print statement outside of the function?

def shopping_list():
    fruit = ['apple', 'banana']
    
shopping_list()
print(fruit)

We get an error"

Traceback (most recent call last):
  File "<string>", line 5, in <module>
NameError: name 'fruit' is not defined

Specifically a NameError, as the fruit was defined locally and therefore remains confined to that context.
In order for our program to understand the variable globally (outside of the function), we need to define it globally.

Global Variables in Python

What if instead of initially defining our variable within the function, we move it outside and initialize it there?

In this case, we can reference it outside of the function and everything works.

But if we try to redefine the fruit variable inside shopping_list, those changes won't update to the original global variable, instead being isolated locally:

fruit = ['apple', 'banana']

def shopping_list():
    fruit = ['apple', 'banana', 'grapes']

shopping_list()
print(fruit)

Output:

['apple', 'banana']

This is because the fruit we've modified in the shopping_list() function is a new, local variable. We've created it, assigned a value to it, and done nothing after that. It's effectively fully redundant code. The print() statement prints the value of the global variable that's in scope for it.

The global Keyword

If we want those changes to be reflected in our global variable, instead of making a new local one, all we have to do is add the global keyword. This allows us to communicate that the fruit variable is indeed a global variable:

fruit = ['pineapple', 'grapes']

def shopping_list():
    global fruit
    fruit = ['pineapple', 'grapes', 'apple', 'banana']

shopping_list()
print(fruit)

And sure enough, the global variable is modified with the new values, so one we call print(fruit), the new values are printed:

['pineapple', 'grapes', 'apple', 'banana']

By defining the context of the fruit variable we're referring to as the global one, we can then redefine and alter it to our hearts content knowing that the changes we make within the function will be carried over.

We could also define a global variable within our function and have it be able to be referenced and accessed anywhere else.

def shopping_list():
    global fruit
    fruit = ['pineapple', 'grapes', 'apple', 'banana']


shopping_list()
print(fruit)

This would output:

['pineapple', 'grapes', 'apple', 'banana']

We could even declare a global variable within one function and access it in another without specifying it as global in the second one:

def shopping_list():
    global fruit
    fruit = ['pineapple', 'grapes', 'apple', 'banana']

def print_list():
    print(fruit)
    
shopping_list()
print(fruit)
print_list()

This results in:

['pineapple', 'grapes', 'apple', 'banana']
['pineapple', 'grapes', 'apple', 'banana']

Caution when Using Global Variables

While being able to modify a global variable locally is a handy little tool to have, you have to treat it with a fair bit of caution. Overzealous rewriting and overriding of scope is a recipe for disaster that ends with bugs and unexpected behavior.

It's always important to make sure that you are manipulating a variable only in the context you need it, and otherwise leaving it alone, this is the main drive behind the principle of encapsulation.

We'll take a quick look at an example of a potential issue before we move on to some of the ways that global variables can be useful in your own code:

fruit = ['pineapple', 'grapes', 'apple', 'banana']

def first_item():
    global fruit
    fruit = fruit[0]
    
def iterate():
    global fruit
    for entry in fruit:
        print(entry)
    
iterate()
print(fruit)
first_item()
print(fruit)

Running the code above, we get the following output:

pineapple
grapes
apple
banana
['pineapple', 'grapes', 'apple', 'banana']
pineapple

In this example we reference the variable in both functions, first_item() and iterate(). Everything seems works fine if we call iterate() and then first_item().

If we reverse that order or attempt to iterate after, we run into a big issue:

first_item()
print(fruit)
iterate()
print(fruit)

This now outputs:

pineapple
p
i
n
e
a
p
p
l
e
pineapple

Namely, fruit is now a string that will be iterated through. What's worse is that this bug won't present itself until it's presumably too late. The first code ran seemingly fine.

Now, this problem is obvious on purpose. We tampered with a global variable directly - lo and behold, it's changed. However, in more complex structures, one might accidentally take global variable modification a step too far and get unexpected results.

The nonlocal Keyword

Just because you need to be cautious doesn't mean that global variables aren't also incredibly useful. Global variables can be helpful whenever you want to update a variable without providing it in the return statement, like a counter. They are also very handy with nested functions.

For those of you using Python 3+, you can make use of nonlocal, a keyword which functions very similarly to global, but primarily takes effect when nested in methods. nonlocal essentially forms an in-between of global and local scope.

Since we've been using shopping lists and fruits for most of our examples, we could think of a checkout function that adds up the total of the purchases:

def shopping_bill(promo=False):
    items_prices = [10, 5, 20, 2, 8]
    pct_off = 0

    def half_off():
        nonlocal pct_off
        pct_off = .50

    if promo:
        half_off()

    total = sum(items_prices) - (sum(items_prices) * pct_off)
    print(total)
    
shopping_bill(True)

Running the code above, we get the output:

22.5

This way the global count variable is still local to the outer function, and will not impair (or exist) at a higher level. This gives you some latitude in adding modifiers to your functions.

You can always confirm this by trying to print pct_off outside of the shopping bill method:

NameError: name 'pct_off' is not defined

If we had used the global keyword instead of the nonlocal keyword, printing pct_off would result in:

0.5

Conclusion

At the end of the day, global (and nonlocal) keywords are a tool, and when used properly can open up a lot of possibilities for your code. I personally use both of these keywords rather frequently in my own code, and with enough practice you'll come to see how powerful and useful they really can be.

As always, thank you so much for reading and Happy Hacking!



from Planet Python
via read more

No comments:

Post a Comment

TestDriven.io: Working with Static and Media Files in Django

This article looks at how to work with static and media files in a Django project, locally and in production. from Planet Python via read...