简体   繁体   中英

Can't change class variable with outside function in Python

So I made a game and I'm trying to create an inventory for the player, and I can't change a variable called "equipped_weapon". I've tried everything and or I get an Exception or when printing the player's inventory it shows the previous weapon. I'm a beginner too so if I'm missing something please tell me.

# Existing items
# Need a funtion to create a random item with random stats and a random name within a certain range.

yes_list = ["yes", "yeah", "sure", "why not"]

add_attack = "Attack:"
add_defense = "Defense:"
add_HP = "HP added:"
rarity = "Rarity"

weapon_rusty_sword = {
    "Name:": "Rusty Sword",
    add_attack: 1,
    add_defense: 0,
    rarity: 1
}

weapon_sword_wooden = {
    "Name:": "Wooden Sword",
    add_attack: 1.5,
    add_defense: 0,
    rarity: 1
}

weapon_sword_iron = {
    "Name:": "Iron Sword",
    add_attack: 2,
    add_defense: 0,
    rarity: 2
}

armor_chest_rusty_mail = {
    "Name:": "Rusty Mail",
    add_attack: 0,
    add_defense: 1,
    rarity: 1
}

armor_legs_adventurers = {
    "Name:": "Adventurer's Leggings",
    add_attack: 0,
    add_defense: 1,
    rarity: 1
}

armor_legs_rash_leggings = {
    "Name:": "Rash Leggings",
    add_attack: 0,
    add_defense: 0.5,
    rarity: 1
}

armor_head_rusty_cap = {
    "Name:": "Rusty Cap",
    add_attack: 0,
    add_defense: 0.5,
    rarity: 1
}
potion_hp = {
    "Name:": "HP Potion,",
    add_HP: 4
}

class Person:

    #global equipped_weapon
    equipped_weapon = weapon_rusty_sword
    ui_equipped_weapon = {"Equipped Weapon: {}".format(weapon_rusty_sword): ""}

    equipped_armor = {
    "Head Protection:": {"Name": armor_head_rusty_cap["Name:"], "Defense": armor_head_rusty_cap[add_defense]},
    "Chest Protection:": {"Name": armor_chest_rusty_mail["Name:"], "Defense": armor_chest_rusty_mail[add_defense]},
    "Legs Protection:": {"Name": armor_legs_rash_leggings["Name:"], "Defense": armor_legs_rash_leggings[add_defense]},
    }

    ATTACK = 1 + equipped_weapon[add_attack]
    DEFENSE = 1
    HP = 20

    gold = 10

    potions = {"Potions: ": [potion_hp["Name:"]]}
    ui_gold = {"Gold: ": gold}

    def __init__(self):
        self.name = "Cavalex"

    inv = [
        equipped_armor,
        ui_equipped_weapon,
        potions,
        ui_gold
    ]

    def see_inventory(self):
        for element in self.inv:
            for k, v in element.items():
                if type(v) == list:
                    print(k, ' '.join(v))
                else:
                    print(k, v)

#    def equip_weapon(self, new_weapon_code):
#        global equipped_weapon
#        eq_val = input(
#            "Do you want to equip this weapon? ->( {} )<-\nNote that your current weapon ( {} )will be discarded.".format(new_weapon_code["Name:"], self.equipped_weapon["Name:"]))
#        if eq_val.lower() in yes_list:
#            #del self.equipped_weapon
#            self.equipped_weapon = new_weapon_code
#            print("The weapon you had was discarded.")
#        else:
#            print("The new weapon was discarded.")





# See total amount of defense.
def defense_points():
    return sum(value["Defense"] for key, value in player.equipped_armor.items())


def add_gold(amount):
    player.gold += amount

# Adding to inventory
def new_potion_to_inv(potion):
    player.potions["Potions: "].append(potion["Name:"])


def equipp_weapon(new_weapon_code):
    # global player.equipped_weapon
    eq_val = input(
        "Do you want to equip this weapon? ->( {} )<-\nNote that your current weapon ( {} )will be discarded.".format(
            new_weapon_code["Name:"], player.equipped_weapon["Name:"]))
    if eq_val.lower() in yes_list:
        del player.equipped_weapon
        player.equipped_weapon = new_weapon_code
        print("The weapon you had was discarded.")
    else:
        print("The new weapon was discarded.")

player = Person()

# game loop
while True:
    print("Your name: ", player.name)
    player.see_inventory() # Can't put print this function, else will print "None" in the end.
    print("\nThis is your total armor defense: {}".format(defense_points()))
    print()
    new_potion_to_inv(potion_hp)
    player.see_inventory()
    print(player.ATTACK)
    print()
    print("Now we are changing your weapon.")
    equipp_weapon(weapon_sword_iron)
    print()
    print(player.ATTACK)
    player.see_inventory()



    break

and this is the output:

C:\Users\mateu\AppData\Local\Programs\Python\Python37-32\python.exe C:/Users/mateu/PycharmProjects/Trabalhos_Python/TESTING_THINGS/testing_armor_without_weapons_and_armor.py
Your name:  Cavalex
Head Protection: {'Name': 'Rusty Cap', 'Defense': 0.5}
Chest Protection: {'Name': 'Rusty Mail', 'Defense': 1}
Legs Protection: {'Name': 'Rash Leggings', 'Defense': 0.5}
Equipped Weapon: {'Name:': 'Rusty Sword', 'Attack:': 1, 'Defense:': 0, 'Rarity': 1} 
Potions:  HP Potion,
Gold:  10

This is your total armor defense: 2.0

Head Protection: {'Name': 'Rusty Cap', 'Defense': 0.5}
Chest Protection: {'Name': 'Rusty Mail', 'Defense': 1}
Legs Protection: {'Name': 'Rash Leggings', 'Defense': 0.5}
Equipped Weapon: {'Name:': 'Rusty Sword', 'Attack:': 1, 'Defense:': 0, 'Rarity': 1} 
Potions:  HP Potion, HP Potion,
Gold:  10
2

Now we are changing your weapon.
Do you want to equip this weapon? ->( Iron Sword )<-
Note that your current weapon ( Rusty Sword )will be discarded.yes
Traceback (most recent call last):
  File "C:/Users/mateu/PycharmProjects/Trabalhos_Python/TESTING_THINGS/testing_armor_without_weapons_and_armor.py", line 158, in <module>
    equipp_weapon(weapon_sword_iron)
  File "C:/Users/mateu/PycharmProjects/Trabalhos_Python/TESTING_THINGS/testing_armor_without_weapons_and_armor.py", line 139, in equipp_weapon
    del player.equipped_weapon
AttributeError: equipped_weapon

Process finished with exit code 1

The problem is that you made equipped_weapon a class attribute, not an instance attribute. del instance.attribute only deletes instance attributes, it doesn't even look for class attributes.

If the goal is just to change the instance, you can just remove the del entirely; the next line will add (if no instance attribute exists, shadowing the class attribute) or replace (if an instance attribute already exists) the instance's specific weapon.

If the goal is to change the class, you need to change the assignment to operate on the class:

type(player).equipped_weapon = new_weapon_code

or explicitly naming the class:

Player.equipped_weapon = new_weapon_code

Either way, the del isn't necessary; assigning the new value replaces the old one, implicitly dropping the reference to the old value in the same way that del does explicitly. It would be perfectly legal to do del type(player).equipped_weapon or del Player.equipped_weapon to delete the class attribute specifically, but it's pointless; whatever the desired behavior, simple assignment to either the class or the instance attribute will discard the old reference anyway.

UPDATE : As you noted, removing the del prevents the exception, but your output (from see_inventory ) never changes. That's because see_inventory is looking at Player.inv , which in turn contains a reference to the original value of Player.ui_equipped_weapon . Problem is, while ui_equipped_weapon and equipped_weapon are initialized based on the same default weapon, they're not synced to one another; changing one has no effect on the other (and therefore changing equipped_weapon , on the class or instance, doesn't affect inv , so see_inventory never changes).

Really, the solution here is to make a sane class. None of your class attributes make sense; they're all logically instance attributes and should be defined as such. In a few cases, they're just convenience-oriented transformations of another attribute; in those cases, they shouldn't be attributes at all, but rather property s, which dynamically recompute their value based on another attribute, preventing them from getting out of sync.

Here's a really quick (not tested, might have small typos) rewrite that moves all your justified attributes to the instance, and converts the rest to read-only properties that derive their value from another instance attribute:

import copy

# Factored out to avoid repetition
def clean_armor(armor):
    return {k.rstrip(':'): v
            for k, v in armor.items() if k in {'Name:', 'Defense:'}}

class Person:
    DEFAULT_WEAPON = weapon_rusty_sword
    DEFAULT_ARMOR = {
        "Head Protection:": clean_armor(armor_head_rusty_cap),
        "Chest Protection:": clean_armor(armor_chest_rusty_mail),
        "Legs Protection:": clean_armor(armor_legs_rash_leggings),
    }

    def __init__(self, name="Cavalex",
                 equipped_weapon=DEFAULT_WEAPON, equipped_armor=DEFAULT_ARMOR,
                 base_attack=1, base_defense=1,
                 hp=20, gold=10,
                 potions=(potion_hp["Name:"],)):
        self.name = name
        # deepcopy is defensive (so changing defaults doesn't retroactively
        # alter existing instance); can be removed if you guarantee defaults
        # won't be changed, or you want changes to defaults to affect
        # Players still wielding default items
        self.equipped_weapon = copy.deepcopy(equipped_weapon)
        self.equipped_armor = copy.deepcopy(equipped_armor)
        self.base_attack = int(base_attack)
        self.base_defense = int(base_defense)
        self.HP = int(hp)
        self.gold = int(gold)
        # potions only used as dictionary, but it's silly to store it as one
        # Just store list of potion names in protected attribute,
        # property can construct the expected dict on demand
        self._potions = list(potions)

    @property
    def ATTACK(self):
        return self.base_attack + self.equipped_weapon[add_attack]

    @property
    def DEFENSE(self):
        return self.base_defense + sum(armor['Defense'] for armor in self.equipped_armor.values())

    @property
    def potions(self):
        return {"Potions: ": self._potions}

    @property
    def ui_gold(self):
        return {"Gold: ": self.gold}

    @property
    def ui_equipped_weapon(self):
        return {"Equipped Weapon: {}".format(self.equipped_weapon): ""}

    @property:
    def inv(self):
        return [
            self.equipped_armor,
            self.ui_equipped_weapon,
            self.potions,
            self.ui_gold,
        ]

    def see_inventory(self):
        for element in self.inv:
            for k, v in element.items():
                if isinstance(v, list):
                    print(k, ' '.join(v))
                else:
                    print(k, v)

This still has a lot of questionable choices (inconsistent attribute/property naming; weird key names in the dict s that seem to have been chosen to make display easier, but make all non-display uses more annoying; using top-level functions for things that only make sense as instance methods, etc.), but I preserved those quirks to keep it a drop-in replacement for your existing class (but with all per-player attributes set on the instance, not the class, and all derived attributes computed via property s rather than set statically, since independent static attributes increase the odds of the base and derived attributes getting out of sync).

I preserved the behavior of allowing you to create a player without providing any arguments, though the name should really be a non-optional argument (unless "Cavalex" is such a common name that you can safely assume most if not all players have that name).

I realize it seems kind of tedious to spell out __init__ and define property s when you can just copy over the data from attribute to derived attribute, but in terms of code that you can actually use in a meaningful way without extreme repetition (and the accompanying risk of typos and logic errors), understand the behavior of, etc., this is the only way that makes sense; a class that is 99% class attributes (and all the methods effectively ignore per-instance state) may as well have just been a bunch of globals and top-level functions, not a class at all.

The reason you make classes is to allow you to create many instances of the class with different attributes, but shared behaviors; making the class a quasi-singleton by using purely class-level state makes the class pointless, at least in Python where the multiparadigm language design means the singleton pattern is rarely necessary, if ever (and using class level state defined at definition time removes the sole advantage of the pattern, deferring initialization of the singleton until it's needed).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM