Class Instance Variable - ex45

I have been trying to find a way to make the below code work for a few days now. I have googled inheritance, class vs instance, etc and I still cannot find a solution… I’m trying to call an instance variable from one class (Starter_Cabin) to another class (Mystery_Forrest).

I’ve tried inheritance, super and simply calling the variable. I think that every time I try and call ‘self.weapon’ it simply re-loops through the if statement in the Starter_Cabin() - maybe the if statement needs to be rewritten?.. Any help is much appreciated.

from random import randint
from textwrap import dedent
from sys import exit
from operator import itemgetter

class Starter_Cabin(object):

    def Enter(self):

        print("You wake up and find yourself in a cabin... Pick a weapon.")

        self.armory = [
        "sword",
        "axe",
        "bow"
        ]

        self.action = input("1. Sword\n2. Axe\n3. Bow\n\n> ")

        if self.action == "1" or self.action.lower() == "sword":
            self.weapon = itemgetter(0)(self.armory)
            print("You reach for the", self.weapon, "and are amazed of how light it is.")
            Mystery_Forrest().Enter()
        elif self.action == "2" or self.action.lower  () == "axe":
            self.weapon = itemgetter(1)(self.armory)
            print("You reach for the", self.weapon, "and find that it is more maneuverable than it looks.")
            Mystery_Forrest().Enter()
        elif self.action == "3" or self.action.lower() == "bow":
            self.weapon = itemgetter(2)(self.armory)
            print("You reach for the", self.weapon, "and find that it is as light as a feather.")
            Mystery_Forrest().Enter()
        else:
            print("Please enter a valid option.\n")
            Starter_Cabin().Enter()

class Mystery_Forrest(object):

    def Enter(self):
        SC = Starter_Cabin()
        weapon = self.weapon
### The print line below is trying to call 'self.weapon' from Starter_cabin. I have also tried inheritance but the instance variable has to be initialised and when called reloops through the if statement above.
        print("\nOnce you pick up your", SC.Enter(self.weapon), "you leave the cabin and find\nyourself in a forrest... This is no ordinary forrest.")

        direction = [
        "North",
        "East",
        "South",
        "West"
        ]

        action = input("Now that you can defend yourself, it's up to you\nwhere you want to go.\n1. North\n2. East\n3. South\n4. West\n> ")

        if action == "1" or action.lower() == "north":
            walk_way = itemgetter(0)(direction)
            Mystery_Forrest_North().Enter()
        elif action == "2" or action.lower() == "east":
            walk_way = itemgetter(1)(direction)
            Mystery_Forrest_East().Enter()
        elif action == "3" or action.lower() == "south":
            walk_way = itemgetter(2)(direction)
            Mystery_Forest_South().Enter()
        elif action == "4" or action.lower() == "west":
            walk_way = itemgetter(3)(direction)
            Mystery_Forest_West().Enter()
        else:
            print("Please enter a valid option.")
            Mystery_Forrest().Enter()

class Mystery_Forrest_North(Mystery_Forrest):
    pass

class Mystery_Forrest_East(Mystery_Forrest):
    pass

class Mystery_Forrest_South(Mystery_Forrest):
    pass

class Mystery_Forrest_West(Mystery_Forrest):
    pass

Starter_Cabin().Enter()

Welcome to the forum @michaelaguilera!

I think what may not be quite clear to you is how you interact with class members. In your code you instantiate StarterCabin once at the bottom. This instance gets an instance attribute weapon during the execution of the enter method. You could access this attribute with dot access anywhere in the code where there’s a reference to this particular instance:

cabin = StarterCabin()
cabin.enter()
# now inside cabin there's an attribute self.weapon
# access it from the outside world with cabin.weapon
print(cabin.weapon)

But if you make another instance and don’t call enter on that one, it won’t have a weapon attribute:

cabin2 = StarterCabin()
print(cabin2.weapon) # error: cabin2 doesn't have the weapon attribute

This is what you are doing in MysteryForrest.enter: you make a new instance of StarterCabin and you never call enter on that instance so it never gets a weapon attribute.


So much for the theory. Now the problem is, if you want to store the weapon as an instance variable, you’ll have to find a way to pass around the instance to the next scenes. It’s possible, but convoluted, and conceptually not quite fitting I guess, because the weapon doesn’t really belong to the starting point. It would rather belong to a player object which you don’t have. But that’s fine.

Here’s another solution that’s quite close to your original approach, but it works without instance attributes:

class StarterCabin(object):
    def enter(self):
        choice = input("Choose your weapon:\n1. Sword\n2. Axe\n> ")
        if choice == "1" or choice.lower() == "sword":
            weapon = "sword"
        elif choice == "2" or choice.lower() == "axe":
            weapon = "axe"
        else:
            print("Please enter a valid option.")
            self.enter()

        print(f"You pick up the {weapon}.")
        # Pass the weapon to the next scene when you call it.
        MysteryForrest().enter(weapon)

class MysteryForrest(object):
    # enter takes a parameter weapon.
    def enter(self, weapon):
        print(f"You leave the cabin with your {weapon}.")
        choice = input("Where do you want to go?\n1. North\n2. South\n> ")
        # ...

StarterCabin().enter()

There are many other ways to solve this.

I’m intrigued why you use the above instead of

walk_way = direction[0]

To access the index of the list. What’s the benefit of this?

Thanks for clarifying, this really helped!

1 Like

I’ve read that itemgetter has slightly better performance. The script is too small to see any difference so it was just a fun way to spice things up a bit.

Oh! Really? That’s interesting. I guess this matters if you need to access the exact nth element of varying sequences like a billion times. Then it’s probably faster to have one curried function of one argument get_nth(seq) instead of passing two arguments to get(seq, n) where n is constant anyway.

But I’d say, don’t bother. If you ever need to handle that amount of data that fast you’ll probably not be using Python anyway.

Thanks for bringing it up. I didn’t even know these existed. :slight_smile:

Yeah likewise. I just read up on it a bit. Kinda sacrificing readability for performance it always good to know multiple ways to implement things.

Cheers @michaelaguilera

So, this comes up a lot but I think it’s because I didn’t really cover it since it’s really obvious to me as someone who’s been doing this forever. I should probably have said:

If you want one object to tell another object to do something you have three choices:

  1. Pass a variable to the other classes __init__.
  2. Set a variable in the other object with just: otherobject.the_var = “foo”
  3. Call a function on the other object to make it do stuff. This is usually what you want.

So, in StarterCabin, when you want the MysterForrest to get the weapon, do this:

the_forrest.has_weapn(weapon)

BUT, this brings up a couple of interesting things:

  1. Why not have Player object? Each room has stuff, and players have stuff?
  2. How do you get at the_mystery_forrest?

Answering #2, easiest is just set them up at the top:

the_mystery_forrest = MysteryForrest()
the_starter_cabin = StarterCabin()

Then call them at will inside your code. Totally allowed in something this small. How about #1? Just make a player class:

class Player(object):
    def __init__(self, health, weapon):
        self.health = health
        self.weapon = weapon

   def take_weapon(self, weapon):
        self.weapon = weapon

    def drop_weapon(self):
        self.weapon = None

Now, you pass the player to each room when you call enter, like:

the_mystery_forrest.enter(player)

And then you can do what you want to all the rooms and the player. Add functions to remove weapons from the rooms, add them to the player, and you’re set. You can also then make a monster:

class Monster(self):
    def __init__(self, name, health, damage):
        self.name = name
        self.health = health
        self.damage = damage

    def attack(self, player):
        # do some kind of random number attack on the player
        # using self.damage as a limit, and random numbers (there's a module)
    
    def take_hit(self, amount):
         self.health -= amount

I think what happens is everyone thinks only rooms can be objects, but everything can be objects. The weapon can be an object. The chairs in the room that the player break. This is where “objects” come from. I mean, we literally call things in the real world “objects” for this reason. OOP is just modeling things in the real world, so if you get stuck, just think “what attributes does a sword have and what can I do with it?” Then, self.x makes the x attribute, and def do_thing makes a thing you can do with the sword.