3

When I code games with functions, I often get confused as to which variable to global.

I've heard that globalizing variables isn't a very practice, so I try to minimize the amount by not globalizing any, and only globalize the ones that the error message tells me to. But doing that is quite annoying, and it wastes time.

Can someone tell me the rule of thumb as to when we should global a variable in a function,

and when it is not necessary?

Here is a sample of what I mean (the functions):
import turtle
from random import randint as rd
from time import sleep
delay = 0.1

wn = turtle.Screen()
wn.setup(400,400)
wn.tracer(0)

player = turtle.Turtle('square')
player.penup()
player.goto(0,-170)

rock = turtle.Turtle('circle')
rock.shapesize(0.5,0.5)
rock.penup()
rock.goto(rd(-190,190),200)

rocks = [rock]

pen = turtle.Turtle(visible=False)
pen.penup()
pen.goto(0,150)

def go_left(): # No globalizing here
    if player.xcor() >= -170:
        player.setx(player.xcor()-10)

def go_right(): # No globalizing here
    if player.xcor() <= 170:
        player.setx(player.xcor()+10)

def move_rocks(): # No globalizing here
    for rock in rocks:
        rock.sety(rock.ycor()-rd(0,2))

def loop_rocks():
    global count # rocks not globalized here
    for rock in rocks:
        if rock.ycor() < -200:
            count += 1
            rock.goto(rd(-190,190),200)

def add_rocks():
    global count # rocks not globalized here
    if count == len(rocks) and len(rocks) <= 15:
        rock = turtle.Turtle('circle')
        rock.shapesize(0.5,0.5)
        rock.penup()
        rock.goto(rd(-190,190),200)
        rocks.append(rock)
        count = 0

def write_score():
    global time,score,high_score # pen not globalized here
    time += 1
    if time == 500:
        score += 1
        time = 0
    if score > high_score:
        high_score = score

    pen.clear()
    pen.write(f'Score: {score}   High Score: {high_score}',align='center',font=('Arial',10,'bold'))

def hit(): # No globalizing here
    for rock in rocks:
        if player.distance(rock) < 15:
            return True

def die():
    global score,rocks # player not globalized here
    sleep(1)
    for rock in rocks:
        rock.goto(300,300)
    rocks = rocks[:1]
    rocks[0].goto(rd(-190,190),200)
    player.goto(0,-170)
    score = 0

wn.listen()
wn.onkeypress(go_left,'Left')
wn.onkeypress(go_right,'Right')

score = 0
high_score = 0
count = 0
time = 0

while True:
    if hit():
        die()
    move_rocks()
    loop_rocks()
    add_rocks()
    write_score()
    wn.update()
Ann Zen
  • 17,892
  • 6
  • 20
  • 39
  • 3
    You shouldn't ever use global variables, if you can help it. Sometimes, global "constants" are fine. But for example, `go_left` uses global variables, but it should **instead** take those values are parameters to the function Same with the rest of your functions. – juanpa.arrivillaga May 25 '20 at 20:34
  • I agree with @juanpa.arrivillaga, avoid global variables as much as possible — global _constants_, whose values generally don't vary, are in fact are good thing. You can usually avoid them by passing them as arguments to function or making them attributes of a class that the class' methods can access via the `self` first argument they all receive automatically. – martineau May 25 '20 at 20:45
  • @martineau can you maybe elaborate a little bit on why it is not a good idea? To avoid shadowing variable names? To avoid accidentally changing mutable types? Sorry for the follow-up questions even though I am not OP. Just curious. – mapf May 25 '20 at 21:03
  • 1
    @mapf: [Global Variables Are Bad](http://wiki.c2.com/?GlobalVariablesAreBad) and [Global Variables Considered Harmful](http://wiki.c2.com/?GlobalVariablesConsideredHarmful) do a good job. – martineau May 25 '20 at 21:06
  • @martineau Perfect! Thanks! – mapf May 25 '20 at 21:08
  • Ok, I went a little bit deeper and tried out different things. Although not really an answer, I summarized it in my answer to the best of my knowledge. – mapf May 26 '20 at 10:07
  • 1
    Hi Ann Zen. I admired your amazingly patient work elsewhere. There is no sarcasm in this, my comment and it is unrelated to this, your question. I want to help and protect you. So allow me to I just make sure that you know this https://meta.stackoverflow.com/questions/258206/what-is-a-help-vampire If you read it and then do not know what I am referring to please feel free to ignore my input. – Yunnosch Jun 15 '20 at 19:04

2 Answers2

7

Style rules are not language rules. I.e. you shouldn't use eval(), but there it is, in the language.

tell me the rule of thumb as to when we should global a variable in a function, and when it is not necessary?

The rules for when, and when not, to use global are simple, but even tutorials on the web get it wrong.

  1. The global keyword should not be used to create a global variable.

(Yes, that's partly a style rule.) When you define a top level variable outside a function, Python makes it global. (You don't use the global keyword for this.) When you assign to a variable inside a function, Python assumes it is local to the function. You only need the global keyword when you want change that later assumption so you can reassign (=) a global variable from within a function. You don't need the global declaration to examine a global variable. You don't need it to invoke a method on a global variable that might change its internal state or content:

  1. You only need the global keyword when you want to reassign (=) a global variable within a function.

The global declaration is used in any function where a global variable is reassigned. It is is placed ahead of the first reference to the variable, access or assignment. For simplicity, and style, global statements are put at the beginning of the function.

A statement like, "You should never use global variables", is a style rule and true of most programming languages -- apply it if/when you can. And if you absolutely can't, don't feel bad about it, just:

  1. Comment all globals you do use properly.

Global constants are less an issue:

  1. If global constants are truly constant, they never need the global keyword.

@juanpa.arrivillaga's example of go_left() taking the additional values as parameters instead of global variables, fails to take into account that go_left() is a callback and that the turtle event assignment functions don't provide for additional parameters. (They should, but they don't.) We can get around this using a lambda expression (or partial function from functools), but when used this way, lambda isn't particularly great style either, IMHO.

@martineau's suggestion of "making them attributes of a class that the class' methods can access" (aka class variables) is fine, but what is left unsaid is that it means subclassing Turtle or wrapping a turtle instance with another class.

My personal issue with mutable globals is that they are problematic in a multi-threaded world.

cdlane
  • 33,404
  • 4
  • 23
  • 63
  • 1
    You don't need to use lambda to partially apply arguments even without `partial` you can just use a regular function. While mutable global state can be catastrophic in a mutltihreaded program, it's not a mere *style* rule, it's a fundamental antipattern for designing programs in all languages, even leaving aside multithreading, it easily leads to hard to reason about, hard to test, spaghetti code. Note, you can still use a class without subclassing turtle, indeed, I dou t that's what @martineau had in mind, instead you can do something like `MyObject(state1, state2).callback` – juanpa.arrivillaga May 27 '20 at 02:58
1

Although it is not an answer, I just wanted to point out one more thing to look out for when shadowing names from outer scopes / global variables. cdlane writes in their answer that

You don't need the global declaration to examine a global variable.

I think it goes even further than that, because you cannot use the global keyword that way, as it is a declaration. As cdlane already said, it is used to declare variables in a local scope (such as a function or class) to be of global scope, such that you can assign new values to these variables from a local scope. You can even use the gobal keyword to declare new global variables from a local scope, although again, as cdlane pointed out, this is not a good idea. Here is some code highlighting these behaviours:

a = c = 1  # define global variables 'a' and 'b' and assign both the
# value 1.


class Local:
    def __init__(self):
        print(a)  # if you only want to examine the global variable 'a',
        # you don't need any keywords
        self.declare_variables()

    def declare_variables(self):
        global a, b  # declare 'a' and 'b' as global variables, such that any
        # assignment statements in this scope refer to the global variables
        a = 2  # reassign a new value to the already existing global
        # variable 'a'.
        b = 3  # assign a value to the previously undeclared global variable
        # 'b'. you should avoid this.
        c = 4  # this does not affect the global variable 'c', since the
        # variable 'c' in this scope is assumed to be local by default.


local = Local()
print(a, b, c)  # the vaules of the gobal variables 'a' and 'b' have changed,
# while 'c' remains unaffected.

So far nothing really new. However, when you are shadowing the names from global variables, but are still accessing the global variables elsewhere in the same scope, this becomes a problem.

  • If you declare a variable shadowing the name of a global variable before you try to access that global variable, all references to that variable name following that declaration will refer to the local variable. I think this might be the worse case, since this could go undetected and not produce any errors, but return wrong results.
  • If you try to declare a new local variable, or use the global keyword with the same variable name after you have already referenced that variable name in the same scope, it will result in an UnboundLocalError or SyntaxError, respectively.
def reference_global_before_local_declaration():
    print(a)  # first reference the global variable 'a'. this statement would
    # work on its own if 'a' wouldn't be redeclared to be a local variable 
    # later on.
    a = 5  # redeclare 'a' to be a local variable and assign it the value 5.


reference_global_before_local_declaration()


def reference_global_before_global_declaration():
    print(a)  # first reference the global variable 'a'. this statement would
    # work on its own if 'a' wouldn't be declared to be a global variable
    # again later on.
    global a  # declare 'a' to be a global variable again.


reference_global_before_global_declaration()


def reference_global_after_local_declaration():
    a = 'text'  # redeclare 'a' to be a local variable of type string and
    # assign it the value 'text'.
    b = a + 1  # here, 'a' was supposed to reference the global variable 
    # 'a', but is now referencing the local variable 'a' instead, due to 'a'
    # being declared in the same scope and shadowing the name of the gobal 
    # variable 'a'.


reference_global_after_local_declaration()

The only way that I know of to avoid this, is to use the globals() function, although this really defeats all purpose and I wouldn't recommend it. I would however recommend to read PEP 3104 - Access to Names in Outer Scopes, which discusses these kinds of problems and presents a solution, which was ultimately never implemented though.

def reference_global_before_local_declaration():
    print(globals()['a'])
    a = 5


reference_global_before_local_declaration()


def reference_global_before_global_declaration():
    print(globals()['a'])
    global a


reference_global_before_global_declaration()


def reference_global_after_local_declaration():
    a = 'text'
    b = globals()['a'] + 1


reference_global_after_local_declaration()
mapf
  • 1,327
  • 8
  • 20