Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Questions about pulling object information from subclasses

Tags:

python

my apologies if there are similar questions in regards to mine. I'm rather new to Python, and still getting used to OOP. That being said, if one of the many kind souls in this community could help in regards to my specific situation, or point me in the direction of similar questions that have been asked, -- though I'd like to note that Googling was no help and each question w/ similar errors were so widely different, and spanned so many different projects, I had a hard time grasping the solutions -- it would be greatly appreciated.

Let's say I have a file named 'objects.py' to store most of my class objects for a text based game I'm developing, and in that file I have a class named 'Occupation'. Class 'Occupation' holds variables that are used as modifiers for the 'Player' class that will be described a little more later on.

The idea is that in the root file, namely 'root.py', the player will be able to choose from a list of player occupations, e.g. 'wizard' or 'knight', and have the information from the respective Occupation classes replace the current Player variable information.

'player.py' file:

import objects

class Player(object):
    nextlevel = 30
    # 'nextlevel' is the gold required for a level up.
    def __init__(self):
        self.name = ''
        self.level = 1
        self.health = 1
        self.stamina = 0
        self.mana = 0
        self.skills = {'str': 0, 'def': 0, 'dex': 0, 'int': 0, 'chr': 0, 'lck': 0}
        self.spells = []
        self.inventory = [{'armor': [objects.PrisonerRobes()], 'weapons': [objects.IronDagger()], 'bows': [], 'potions': [], 'misc': []}, objects.Gold(0), objects.Arrow(0)]
        self.victory = False

'objects.py' file:

class Occupation(object):
    # Base class for all occupations / classes, e.g. 'knight' or 'wizard'.
    classlist = ['warrior', 'knight', 'paladin', 'druid', 'warlock', 'wizard']
    def __init__(self, name, hp, sp, mp, strv, defv, dexv, intv, chrv, lckv):
        # The variable names of 'strv', 'defv', etc. stand for 'strength value', 'defence value', etc.
        self.name = name
        self.health = health
        self.stamina = stamina
        self.mana = mana
        self.strv = strv    #skill values
        self.defv = defv    
        self.dexv = dexv
        self.intv = intv
        self.chrv = chrv
        self.lckv = lckv

    def __str__(self):
        details = '{0}: HP: {1}, SP: {2}, MP: {3} // [STR: {4}, DEF: {5}, DEX: {6}, INT: {7}, CHR: {8}, LCK: {9}]'
        return details.format(self.name, self.health, self.stamina, self.mana, self.strv, self.defv, self.dexv, self.intv, self.chrv, self.lckv)

class Warrior(Occupation):
    def __init__(self):
        super(Warrior, self).__init__('Warrior', 75, 50, 25, 12, 8, 12, 5, 8, 5)

'root.py' file:

from player import Player
import objects

def new_game():
    while True:
        print 'Only one class, and it is Warrior!'
        print objects.Warrior()
        classchoice = raw_input('Please type the full name of the class you wish to select.: ').lower()
        playerclass = classchoice.title()
        if classchoice != '':
            characterclass = getattr(objects, playerclass)
            player.health = characterclass.health
            player.stamina = characterclass.stamina
            player.mana = characterclass.mana
            player.skills['str'] = characterclass.strv
            player.skills['def'] = characterclass.defv
            player.skills['dex'] = characterclass.dexv
            player.skills['int'] = characterclass.intv
            player.skills['chr'] = characterclass.chrv
            player.skills['lck'] = characterclass.lckv
            print 'You have chosen {0} as your class.'.format(characterclass.name)
            confirm = raw_input('Is this correct? Type [Y] or [N] for yes/no respectively.: ').lower()
            if confirm == 'y':
                break
            elif confirm == 'n':
                continue
            else:
                print 'Not a valid keystroke!'
                continue

The problem appears when typing 'warrior' when choosing a player class.:

line 48[in my original file]: player.health = characterclass.health
AttributeError: type object 'Warrior' has no attribute 'health'

It seems that I can't reference the variables that I want to, when trying to update pre-existing Player values. The 'Warrior' class, as far as I can tell, has the variable 'health', but I can't seem to access it. Am I misunderstanding how classes and subclasses work in Python? Or is it something else?

Thanks for any help.

like image 538
Mochamethod Avatar asked Aug 23 '26 16:08

Mochamethod


2 Answers

I would like to disagree with Charles Merriam's answer. I fancy the different classes. Yes in the beginning they all look alike and just have different stats. But later on you can extend them to hold multiple functionalities. For example special modifiers such as "rage" for Warrior or "meditation" for Wizard.

If you put all your eggs in a single Character class and just represent different classes as a string attribute of Character class, then you won't be able to do this extending later on. In fact I added the rage method to Warrior at the very end when I came to post this and saw Charles Merriam's answer. And those are the only lines I had to change because I used this inheritance model of yours. (Originally I only wanted to show you composition.)

Composition

Before I showcase what this inheritance thing can do back to what I think you're doing wrong. I think You've created a class inheritance model for your Occupations, but then lost your way and wanted to push it all in the Player class as attributes.

To be more hands on: You defined an Occupation class that holds stats (hp, mana, stamina, dexterity ...). Then you created a Player object and made it exactly the same, except for some extra items, victory... attributes. If you've spent all this effort coding stats as attributes of Occupation classes why would you want for the Player to have specific, independent stats as if Occupations don't exist? You wouldn't.

But what to do exactly? I figure an Occupation can't own items. We haven't coded Occupation like that and it's a bit weird for a system of teachings and ideas to own anything anyhow. So I want to avoid having to inherit Occupations with the Player. A Player is not an extension of Occupation even if a Player can have an Occupation. Therefore, what we want is a composition.

That's when you take an Occupation object, and assign it as an attribute to a Player object.

class CharClass(object):
    #these stats have to be defined for EVERY occupation possible
    #This can be used as a base class for players without defined classes yet
    #by defining the default values of __init__
    def __init__(self, name="Unknown", health=30, stamina=10, mana=10,
                 strv=1, defv=1, dexv=1, intv=0, chrv=0, lckv=0):
        self.name = name
        self.health = health
        self.stamina = stamina
        self.mana = mana
        self.strv = strv    #skill values
        self.defv = defv    
        self.dexv = dexv
        self.intv = intv
        self.chrv = chrv
        self.lckv = lckv

    def __str__(self):
        details = '{0}: HP: {1}, SP: {2}, MP: {3} \n'+\
                  '[STR: {4}, DEF: {5}, DEX: {6}, INT: {7}, CHR: {8}, LCK: {9}]'
        return details.format(self.name, self.health, self.stamina, self.mana, self.strv, 
                              self.defv, self.dexv, self.intv, self.chrv, self.lckv)


class Warrior(CharClass):
    def __init__(self):
        super(Warrior, self).__init__(health=100, stamina=50, dexv=20)

    def __str__(self):
        parentstring = super(Warrior, self).__str__()
        return "You are a Warrior \n"+parentstring

class Player(object):
    def __init__(self, charclass=CharClass(), items=[], spells=[], victory=False):
        self.charclass = charclass #the generic class is the default one
        self.items = items
        self.spells = spells

Now that we used composition, you don't have to set any of the stats for player individually. That makes your code a lot shorter and more readable. You can access those through a Warrior object, which is an attribute of your player class like this:

newplayer = Player()
print newplayer
print newplayer.charclass

print

#graduating to a Warrior
newplayer.charclass = Warrior() 
print newplayer
print newplayer.charclass

print

#there's a snag though. Every time now if you want to print or change Players health
#(or some other attribute) you'd have to spell all of this:

print newplayer.charclass.health
newplayer.charclass.health += 100
print newplayer.charclass.health

#this is because Player object contains a Warrior object in it and what you really change is that
#Warrior object and not the Player.

print type(newplayer.charclass)

Output of the above code would be:

<__main__.Player object at 0x000000000427A550>
Unknown: HP: 30, SP: 10, MP: 10 
[STR: 1, DEF: 1, DEX: 1, INT: 0, CHR: 0, LCK: 0]

<__main__.Player object at 0x000000000427A550>
You are a Warrior 
Unknown: HP: 100, SP: 50, MP: 10 
[STR: 1, DEF: 1, DEX: 20, INT: 0, CHR: 0, LCK: 0]

100
200
<class '__main__.Warrior'>

Seamless Composition

There's a trick/workaround around the snag, albeit it's a bit harder for someone new to get it. Don't fret it, look it back up in a month when the world makes more sense.

Special functions __getattr__ and __setattr__ are called whenever the attribute couldn't be found where it was expected. We override those functions to intercept what the special signs . (in i.e. self.attribute) and = do. Instead of those operators setting or getting the attributes of Player class we make them sometimes set/get the attributes of the composited self.charclass object instead!

class Player(object):
    def __init__(self, charclass=CharClass(), items=[], spells=[], victory=False):
        self.charclass = charclass
        self.items = items
        self.spells = spells

    def __getattr__(self, attr):
            return getattr(self.charclass, attr)

    def __setattr__(self, attr, val):
        if "charclass" in vars(self) and attr in vars(self.charclass):
            self.charclass.__dict__[attr] = val     
        else:
            self.__dict__[attr] = val

With this in place we can do this again (without having to write charclass everywhere:

print newplayer.health
newplayer.health -= 50
print newplayer.health

Output is:

200
150

It's hard to work with __getattr__ and __setattr__ without getting errors. They're not easy functions to overwrite.

It's easier with __getattr__, it will automatically get called whenever there's nothing with the same name in the Player object. So all you have to be careful of is not adding new attributes with the same name in Player.

But __setattr__ will also get called in the __init__ function when Player object doesn't have any attributes yet. If we don't check for that, we will get an error. That's what if "charclass" in vars(self) does; checks if __init__ added charclass attribute to our object.

But if we just check for charclass attribute, what happens when we try to add another? i.e. 2nd one in the row: items? __setattr__ would be called, it would check for charclass in self, it would see it is there and then it would add items to self.charclass and not self! In effect it would add attributes to the Warrior object and not Player object.

How do we add a new attribute to a Player object then? Answer is that we only set already existing Warrior object attributes. All attributes that don't exist in Warrior (self.charclass) are set as attributes of the Player instance (self.__dict__[attr] = val). That's what and attr in vars(self.charclass) means.

Inheritance and composition help extending code later on

To jump back to top and to why I disagree with Charles Merriam's answer. To exntend and change the functionality of your code now, all you have to do additionally is to add methods to Warrior class, for example let's add rageOn and rageOff methods:

class Warrior(CharClass):
    #this class basically just defines different default values than CharClass
    def __init__(self):
        super(Warrior, self).__init__(health=100, stamina=50, dexv=20)

    def __str__(self):
        parentstring = super(Warrior, self).__str__()
        return "You are a Warrior \n"+parentstring

    def rageON(self):
        print "You are in a blood-rage +30 attack modifier"
        self.stamina += 30

    def rageOFF(self):
        print "You have calmed down."
        self.stamina -= 30

And they will be immediately available in your Player class without you having to change the Player at all.

newplayer.rageON()
print newplayer.charclass
print "----------------------------------------------------------------------------------------"
newplayer.rageOFF()
print newplayer.charclass

Output:

You are in a blood-rage +30 attack modifier
You are a Warrior 
Unknown: HP: 150, SP: 80, MP: 10 
[STR: 1, DEF: 1, DEX: 20, INT: 0, CHR: 0, LCK: 0]
----------------------------------------------------------------------------------------
You have calmed down.
You are a Warrior 
Unknown: HP: 150, SP: 50, MP: 10 
[STR: 1, DEF: 1, DEX: 20, INT: 0, CHR: 0, LCK: 0]

If you did it all in Player, or Character, class as suggested by Charles; you would have to add a method rage to the Player class. In it you would have to check if you're actually "Warrior" or not and could only then execute it.

This doesn't sound like that much work until you realize that for every special effect of every class you want to have, you would have to write such a method. And they would all have to be in a single file where the Player class is defined. That does sound bad after all. I don't know why I wrote all this but it's too much invested now to not post. Sorry.

like image 153
ljetibo Avatar answered Aug 25 '26 05:08

ljetibo


characterclass = getattr(objects, playerclass) getattr() returns the value of named attribute of an object other than the object itself. characterclass is not an instance of Warrior, therefore you cannot refer to attribute health

getattr(object, name[, default]) Return the value of the named attribute of object. name must be a string. If the string is the name of one of the object’s attributes, the result is the value of that attribute. For example, getattr(x, 'foobar') is equivalent to x.foobar. If the named attribute does not exist, default is returned if provided, otherwise AttributeError is raised.

You need to create warrior instance as:

warrior = objects.Warrior()
warrior.health
like image 28
Haifeng Zhang Avatar answered Aug 25 '26 06:08

Haifeng Zhang



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!