Awesome Finn
Awesome Finn

Reputation: 13

Is there a way to fix Name Error due to scope?

I have a function that creates a player object but when referencing the object, I get a NameError. I think it is happening due to local scope but global should fix it...

I just started out OOP and this code is working in the python shell but it is not working in script mode.

endl = lambda a: print("\n"*a)

class Score:
    _tie = 0
    def __init__(self):
        self._name = ""
        self._wins = 0
        self._loses = 0

    def get_name(self):
        print
        self._name = input().upper()

    def inc_score(self, wlt):
        if wlt=="w": self._wins += 1
        elif wlt=="l": self._loses += 1
        elif wlt=="t": _tie += 1
        else: raise ValueError("Bad Input")

def player_num(): #Gets number of players
    while True:
        clear()
        endl(10)
        print("1 player or 2 players?")
        endl(5)
        pnum = input('Enter 1 or 2: '.rjust(55))
        try:
            assert int(pnum) == 1 or int(pnum) == 2
            clear()
            return int(pnum)
        except:
            print("\n\nPlease enter 1 or 2.")

def create_player():  #Creates players
    global p1
    p1 = Score()
    yield 0          #stops here if there is only 1 player
    global p2
    p2 = Score()

def pr_():          #testing object
    input(p1._wins)
    input(p2._wins)


for i in range(player_num()):
    create_player()
    input(p1)
input(p1._wins())
pr_()

wherever I reference p1 I should get the required object attributes but I'm getting this error

Traceback (most recent call last):
  File "G:/Python/TicTacTwo.py", line 83, in <module>
    input(p1)
NameError: name 'p1' is not defined

Upvotes: 1

Views: 119

Answers (2)

Adirio
Adirio

Reputation: 5286

I will redesign the app to introduce really powerful python constructs that may help you when getting into OOP.

  1. Your objects are going to represent players, then don't call them Score, call them Player.
  2. Using _tie like that makes it a class variable, so the value is shared for all the players. With only two participants this may be true but this will come to hurt you when you try to extend to more players. Keep it as a instance variable.
  3. I am a fan of __slots__. It is a class special variable that tells the instance variables what attributes they can have. This will prevent to insert new attributes by mistake and also improve the memory needed for each instance, you can remove this line and it will work but I suggest you leave it. __slots__ is any kind of iterable. Using tuples as they are inmutable is my recomendation.
  4. Properties are also a really nice feature. They will act as instance attribute but allow you to specify how they behave when you get the value (a = instance.property), assign them a value (instance.property = value), or delete the value (del instance.property). Name seems to be a really nice fit for a property. The getter will just return the value stored in _name, the setter will remove the leading and trailing spaces and will capitalize the first letter of each word, and the deletter will set the default name again.
  5. Using a single function to compute a result is not very descriptive. Let's do it with 3 functions.

The code could look like this:

# DEFAULT_NAME is a contant so that we only have to modify it here if we want another
# default name instead of having to change it in several places
DEFAULT_NAME = "Unknown"


class Player:

    # ( and ) are not needed but I'll keep them for clarity
    __slots__ = ("_name", "_wins", "_loses", "_ties")

    # We give a default name in case none is provided when the instance is built
    def __init__(self, name=DEFAULT_NAME):
        self._name = name
        self._wins = 0
        self._loses = 0
        self._ties = 0

    # This is part of the name property, more specifically the getter and the documentation
    @property
    def name(self):
        """ The name of the player """
        return self._name

    # This is the setter of the name property, it removes spaces with .strip() and
    # capitalizes first letters of each word with .title()
    @name.setter
    def name(self, name):
        self._name = name.strip().title()

    # This is the last part, the deleter, that assigns the default name again
    @name.deleter
    def name(self):
        self._name = DEFAULT_NAME

    def won(self):
        self._wins += 1

    def lost(self):
        self._loses += 1

    def tied(self):
        self._ties += 1

Now that's all we need for the player itself. The game should have a different class where the players are created.

class Game:

    _min_players = 1
    _max_players = 2

    def __init__(self, players):
        # Check that the number of players is correct
        if not(self._min_players <= players <= self._max_players):
            raise ValueError("Number of players is invalid")
        self._players = []
        for i in range(1, players+1):
            self._players.append(Player(input("Insert player {}'s name: ".format(i))))

    @property
    def players(self):
        # We return a copy of the list to avoid mutating the inner list
        return self._players.copy()

Now the game would be created as follows:

def new_game():
    return Game(int(input("How many players? ")))

After that you would create new methods for the game like playing matches that will call the players won, lost or tied method, etc.

I hope that some of the concepts introduced here are useful for you, like properties, slots, delegating object creation to the owner object, etc.

Upvotes: 0

Mike Scotty
Mike Scotty

Reputation: 10782

Your issue is not with global but with the yield in create_player(), which turns the function into a generator.

What you could do:

Actually run through the generator, by executing list(create_player()) (not nice, but works).

But I suggest you re-design your code instead, e.g. by calling the method with the number of players:

def create_player(num):  #Creates players
    if num >= 1:
        global p1
        p1 = Score()
    if num >= 2:
        global p2
        p2 = Score()

If you fix this issue, the next issues will be

1) input(p1) will print the string representation of p1 and the input will be lost, you probably want p1.get_name() instead.

2) input(p1._wins()) will raise TypeError: 'int' object is not callable

Upvotes: 1

Related Questions