Python - Passing variables between a class function incorrectly? - python

The program is rather self-explanatory. I've begun playing around with the basics of Python, and am really lost on this one. I am used to C++ and the wonderful ability to pass things by reference. But, in this, the class variable I'm trying to change (Fighter.statHEALTH) won't change, and I've read that it's because integers are immutable and it just creates a new object locally. So how in the world can I make the change apply to the original variable? I've Googled and Googled, but to no avail. I don't want to perform some ugly manuever like making a list and passing it if I don't have to.
#python 3.2.2
# Create a small test project to have combat between two entities. #
# Combat should include 3 different stats: statATK, statDEF, and statHEALTH. #
# The two entities should be of the same class. #
class Fighter:
def __init__(self):
self.statHEALTH = 10
self.statATK = 3
self.statDEF = 3
def attack(self, enemyhealth):
enemyhealth = (enemyhealth - self.statATK)
return enemyhealth
def defend(self):
statDEF += 1
def main():
James = Fighter()
Keaton = Fighter()
while James.statHEALTH > 0:
print("Do you wish to attack or defend?")
print("1. Attack")
print("2. Defend")
choice = input()
if choice == "1":
James.attack(Keaton.statHEALTH)
print("You did", James.statATK, "damage!")
Keaton.attack(James.statHEALTH)
print("Keaton has", Keaton.statHEALTH, "health left.")
print("Keaton did", Keaton.statATK, "damage!")
print("You have", James.statHEALTH, "health left.")
#elif choice == "2":
#James.defend()
#Keaton.attack(James.statHEALTH)
main()

def attack(self, enemyhealth):
enemyhealth = (enemyhealth - self.statATK)
return enemyhealth
This would actually work if you changed your call to be
Keaton.statHEALTH = James.attack(Keaton.statHEALTH)
.. since you return the damage from the attack. Obviously this is ugly; repeating yourself is not good. Instead you can make attack look like:
def attack(self, other):
other.statHEALTH -= self.statATK
And then just do
James.attack(Keaton)
when you call it.

Maybe you could think of it differently. In your example, Fighter.attack() just returns the value of the enemies health after the attack. So really, it should be a method call on the enemy's object. You could add a method that decreases the fighter's health when they get attacked:
def attack(self, enemy):
enemy.getAttacked(self.statATK)
def getAttacked(self, ATK):
self.statHEALTH -= ATK

Try doing:
while James.statHEALTH > 0:
#print statements
if choice == "1":
the_attack = James.attack(Keaton)
Then define your classes as:
class Fighter(object):
def __init__(self):
self.statHEALTH = 10
self.statATK = 3
self.statDEF = 3
def attack(self,target):
attack_details = target.takedamage(self.statATK)
return attack_details
def takedamage(self,dmg):
modified_dmg = dmg-self.statDEF
self.statHEALTH -= modified_dmg
return modified_dmg
This has the added benefit of being easily expandable, e.g. you can add a hit table (for i in random.randint(1,100) if i < 20: #miss; elif 20 <= i < 80: #hit; elif 80<= i: #crit) or resistances for certain elements or add a flag that allows your defender to counterattack in their takedamage function (perhaps calling a new function getcountered to prevent infinite looping).

The problem isn't really that you can't pass things by references in Python. In fact, you always pass values by reference; Python never copies values unless you ask it to.
Or, more accurately, that whole C-based terminology is misleading in Python.
Anyway, when you do this:
James.attack(Keaton.statHEALTH)
This doesn't make a copy of the value in Keaton.statHEALTH, it passes a reference to the exact same value. So, when attack starts, your enemyhealth variable is a name for that value.
And then you do this:
enemyhealth = (enemyhealth - self.statATK)
… the enemyhealth - self.statATK returns a new value, and then you bind that new value to the enemyhealth name. This has no effect on any other names for the old value.
There are two ways to solve this.
First, you don't actually need to mutate anything here. You're already doing return enemyhealth at the end. That means the caller can get the new value that you calculated. So why not just use that?
Keaton.statHEALTH = James.attack(Keaton.statHEALTH)
And then everything works.
Of course in C++, you can mutate integer values. This seems a little silly when you think about it—turning the number 24 into the number 19 would break a lot of math, and probably make the universe stop working, and Python just isn't that powerful.
But you can easily build a type that can be sensibly mutated. In fact, you've already built one: a Fighter holds an integer health value, but can be changed to hold a different one instead. So, you could write this:
def attack(self, enemy):
enemy.health = enemy.health - self.statATK
And then:
James.attack(Keaton)
Just as calling the old method with Keaton.health made enemyhealth into another reference to the same number, calling the new method with Keaton makes enemy into a reference to the same Fighter. If you just reassigned a new value to enemy, that wouldn't have any effect on the old value, the one that Keaton refers to. But if you change the value in-place, obviously Keaton is still referring to that now-changed value.

Related

Function vs if-statement: Function is not working, but the code in the function will work when outside a function?

I was working on building a randomized character generator for Pathfinder 3.5 and got stuck.
I am using the Populate_Skills(Skill_String, Draw, Skill_List, Class_Skill): function to populate a randiomized list of skills with their class based points total, class bonus, and point buy. So modelling the action of a player picking skills for their character.
As an example below, Wizards.
I pick Knowledge_Arcana as a skill and spend one of my skill point pool (Calculated by taking my intelligence modifier +2) on it. So that skill now equals my intelligence modifier(+1 in this case), class skill bonus as a wizard (+3), plus the point I spent(+1) for a total of 5.
The problem is while the function prints the correct result of 5, the outstanding variables do not populate with the final total. To continue our example I'd run the function on Knowledge_Arcana, get a +5, and then check the Knowledge_Arcana after the function call and get just +1. Conversely, if I write out the function as just an if statement it works. Example is next to the function for comparison.
Does anyone know why Im getting the different result?
## Creating the lists and breaking into two separate sections
Int_Mod = 1
Skill_Ranks = 3
Rand_Class = 'Wizard'
Knowledge_Arcana = Int_Mod
Knowledge_Dungeoneering = Int_Mod
Wizard_Class_Top_Skills = ["Knowledge_Arcana"]
Wizard_Class_Less_Skills = ["Knowledge_Dungeoneering"]
Class_Skill = 3
Important_Skills_Weighted = .6
Less_Important_Skills_Weighted = .4
Important_Skills_Total_Weighted = round(Skill_Ranks*Important_Skills_Weighted)
Less_Skill_Total_Weighted = round(Skill_Ranks*Less_Important_Skills_Weighted)
Wiz_Draw =['Knowledge_Arcana', 'Knowledge_Dungeoneering']
def Populate_Skills(Skill_String, Draw, Skill_List, Class_Skill):
if Skill_String in Draw:
Skill_List = Skill_List + Class_Skill + Draw.count(Skill_String)
print(Skill_String, Skill_List)
else:
print('Nuts!')
## Function Calls
Populate_Skills('Knowledge_Arcana', Wiz_Draw, Knowledge_Arcana, Class_Skill)
Populate_Skills('Knowledge_Dungeoneering', Wiz_Draw, Knowledge_Dungeoneering, Class_Skill)
print(Knowledge_Arcana,Knowledge_Dungeoneering)
Edited to be a MRE, I believe. Sorry folks, Im new.
You are passing in a reference to a list and expect the function to modify it; but you are reassigning the variable inside the function which creates a local variable, which is then lost when the function is exited. You want to manipulate the same variable which the caller passed in, instead.
def Populate_Skills(Skill_String, Draw, Skill_List, Class_Skill):
if Skill_String in Draw:
Skill_List.extend(Class_Skill + Draw.count(Skill_String))
print(Skill_String, Skill_List)
else:
print('Nuts!')
Alternatively, have the function return the new value, and mandate for the caller to pick it up and assign it to the variable.
def Populate_Skills(Skill_String, Draw, Skill_List, Class_Skill):
if Skill_String in Draw:
Skill_List = Skill_List + Class_Skill + Draw.count(Skill_String)
print(Skill_String, Skill_List)
else:
print('Nuts!')
return Skill_List
Skill_List = Populate_Skills('Knowledge_Arcana', Wiz_Draw, Knowledge_Arcana, Class_Skill)
# etc
You should probably also rename your variables (capital letters should be used for classes and globals; regular Python functions and variables should use snake_case) and avoid using global variables at all. The entire program looks like you should probably look into refactoring it into objects, but that's far beyond the scope of what you are asking.

How to get rid of the global variables

I don't want to use the global-variable, but how do I make this without them?
(I have more functions and so on, so this isn't all)
Players turn. I use the global-variable to make sure the turns are switching, tried some other sulotion however the turns didn't switch...
#Check if the shot hit any computerships
def player_hit_check():
global players_turn
shot = player_shot()
check_if_hit = set(computer_ships) & set(player_turn_shot)
if(check_if_hit == set()):
print("You missed! Switching turns...")
time.sleep(3)
players_turn = 1
else:
print(f"YOU MADE A BIG BANG ON ",player_turn_shot,"!")
computer_ships.remove(shot)
all_player_hits.append(shot)
Computers turn
#Check if the computers shot hit any player ships
def computer_hit_check():
global computers_turn
computer_random_shot = computer_shot()
check_if_hit = set(player_ships) & set(computer_turn_shot)
if(check_if_hit == set()):
print("Computer missed! It's now your turn.")
time.sleep(3)
computers_turn = 1
else:
print(f"COMPUTER MADE A BIG BANG ON ",computer_turn_shot,"!")
player_ships.remove(computer_random_shot)
all_computer_hits.append(computer_random_shot)
The game it self
#Runs the game
while True:
#Players turn
players_turn = 0
while players_turn < 1:
print('\n'.join('\t'.join(row) for row in board.values()))
print("Playerships:",player_ships)
print("Computerships:",computer_ships)
print("You have shot on",all_player_shots,"and has knocked out these ships:",all_player_hits)
print("The computer has shot at these coordinates",all_computer_shots,"and has knocked out these ships:",all_computer_hits)
player_hit_check()
#Computers turn
computers_turn = 0
while computers_turn < 1:
computer_hit_check()
In your case you don't need global variables at all. Your program is not using them to carry information back and forth.
In your case you just need to get information back from your functions. So in the case of player_hit_check():
def player_hit_check():
shot = player_shot()
check_if_hit = set(computer_ships) & set(player_turn_shot)
if(check_if_hit == set()):
print("You missed! Switching turns...")
time.sleep(3)
return 1
else:
print(f"YOU MADE A BIG BANG ON ",player_turn_shot,"!")
computer_ships.remove(shot)
all_player_hits.append(shot)
return 0
And the call site would be like this:
while True:
#Players turn
players_turn = 0
while players_turn < 1:
# prints elided ...
players_turn = player_hit_check()
The problem with global variables is, that they fly around in an unstructured manner.
There are several approaches to solve global variables
One first step to get rid of global variables is to use singletons (https://en.wikipedia.org/wiki/Singleton_pattern). This will bundle similar values into one class. So the values are more structured and easier to track.
Use classes. If the global value is used in an environment that can be associated to a single responsibility, it can be productive to bundle all functions in a class and make the global variable to a class variable or property of the class
Pass it through. In many programs there is the need of settings class or state class. Put everything in there that defines the current state or starting state of the program and pass it through your code. The downside of passing is, that this classes tend to become almighty classes that appear in every constructor.
There are certainly more approaches how to get rid of global variables, but this are some common strategies.

Is there a way to create a function that will run this code but without passing so many arguments?

I am trying to make code from my project more efficient, shorter but not hard to read/understand.
I see that there are some actions that repeat themselves a couple of times in the code. It is a game so it is required to repeat those.
I thought that maybe I should create a function to call every time instead, but it does not seem friendly to read, because, following a condition, I need to change a couple of variables so I need to pass all of them to the function.
Here is an example:
if nxtTurn == 'player1':
card2, player1 = EgyptionWarHelper.pull_out_card(player1, nxtTurn)
storage.append(card2)
nxtTurn = 'player2'
else:
card2, player2 = EgyptionWarHelper.pull_out_card(player2, nxtTurn)
storage.append(card2)
nxtTurn = 'player1'
I wanted to create a function that does this, but then realized I will need to pass all of the variables to it and then return a tuple of 2 variables at the end of it. I did hear about global variables but I never really used them and I don't know if they are the solution for this.
EDIT: I found out that I didn't have to pass some of the argument so after I edit the function and used temporary variables as well, the code is much more readable.
Additionally, as commented, I didn't have to return player and player2 because python passes lists by reference and not by value.
why not using a temporary variable ?
if nxtTurn == 'player1':
player = player1
nxtTurn = 'player2'
else:
player = player2
nxtTurn = 'player1'
card2, player1 = EgyptionWarHelper.pull_out_card(player, nxtTurn)
storage.append(card2)
it's easily readable and maintainable.

Making a method generate a new number every time it is called?

goal: I'm trying to make it so when self.damage is called it generates a random number between 1 and the argument "damage". However this random number is only being picked the first time, and then used throughout the entire code. My current code looks like this:
import random
class PC(object):
def __init__(self, health, damage):
self.health = health
self.damage = random.randint(1,damage)
Player_character = PC(10,5)
Spider = PC(2,3)
print(Player_character.health)
def fightspider(spiderturn):
rounds = 0
while spiderturn == 1 and rounds < 2:
Spider.damage
Player_character.health = Player_character.health - Spider.damage
rounds += 1
print(Spider.damage)
print(Player_character.health)
def fightplayer(playerturn):
if playerturn == 1:
Spider.damage
fightspider(1)
When I say Spider.damage inside of the while loop I want it to select a new random number, currently it is only using the first one and not picking any new ones.
I'm not sure why this is happening or how to fix, can I get any leads on where to go with this?
This is my first time using classes so I'm still only halfway sure on how exactly they work!
Thanks for any help.
The damage is only be called in the initalizer of the class random.randint(1, damage) (in the __init__). I recommend you only initialize the max_damage in your initalizer then create a new function that will generate the damage for example:
import random
class PC(object):
def __init__(self, health, damage):
self.health = health
self.max_damage = max_damage
def get_damage():
return random.randint(1, self.max_damage)
Then instead of Spider.damage you can use Spider.get_damage() to get the damage generated everytime. Hope this makes sense, good luck on your programming endeavors!
You've tagged this question with "python-3.x", so I presume you're using 3.x
You're usingSpider.damage in two different ways:
You're using it as an expression that returns the current value stored in the damage attribute.
You're also expectng it to act as a function call that will somehow call random.randint() and re-assign a new random number to the damage attribute.
The first usage is ok. The second won't work.
I suggest you write a method called set_damage(), that will internally call
random.randint(), and update the damage attribute.

Specific example: Is it possible to aviod the use of a global variable?

I have the following program and the variable(dictionary) in question is player_info that stores player information (name and goals). In order to solve the error that results currently, I simply need to make player_info a global variable, but I was wondering if stackoverflow experts could suggest or discuss the possibility of alternate ways of solving this problem WITHOUT the use of global variables.
Code
#FOOTBALL COACH app
#The program allows a user to enter a number of players (their names and goals scored) and then search for a player, returning their average goals for the three matches
import sys
def main():
mainmenu()
def mainmenu():
print("=====WELCOME to the MAIN MENU=============")
print("""
1..........Add New Players & Goals
2..........Search by Players
3..........Quit
=========================================
""")
choice=int(input("Enter choice:"))
if choice==1:
addplayers()
elif choice==2:
searchplayer(player_info)
elif choice==3:
sys.exit()
else:
print("You must make a valid choice - 1, 2 or 3")
def addplayers():
player_info= {} #create a dictionary that stores the player name: player goals
num_players = int(input("Please enter number of players you wish to enter:"))
print ("You are entering %s players" %num_players)
player_data = ['Match 1 goals : ', 'Match 2 goals : ', 'Match 3 goals : ']
for i in range(0,num_players):
player_name = input("Enter Player Name :")
player_info[player_name] = {}
for entry in player_data:
player_info[player_name][entry] = int(input(entry)) #storing the marks entered as integers to perform arithmetic operations later on.
mainmenu()
def searchplayer():
print("===============SEARCH by player: Calculate average goals==================")
name = input("Player name : ")
if name in player_info.keys():
#print student_info
print ("Average player goals : ", str(sum(player_info[name].values())/3.0))
else:
print("Please enter a valid player name:")
main()
As mentioned, I am aware that re-writing this in the addplayer() sub would fix the problem:
global player_info
player_info = {} #create a dictionary that stores the player name: player goals
...I am looking for ways to solve the problem WITHOUT the use of global variables.
Update:
One answer below using return player_info is what I would like to go with, but it doesn't quite work yet. Also, I need to return to the main menu each time a player is added, not quite sure how to do this, without a mainmenu call each time. Any suggestions? https://repl.it/JRl5/1
You can use return inside your function to avoid using global variables. A simple example is shown below:
def addplayers():
player_info= {}
name = input("Enter Name: ")
test = int(input("Enter a number: "))
player_info[name] = test
return player_info
player_info = addplayers()
If you then wanted to use this in another function you would just pass in the dictionary as an argument to that function:
def searchplayers(player_info):
print (player_info)
Note: An interesting answer on "Why are global variables evil?"
Edit:
Your addplayers() was calling mainmenu() which itself was being calling within mainmenu(). This is a recursive function and it might be best to avoid these unless there's a good reason for having it. I would put the contents of mainmenu inside a while loop until some condition is met. The complete code is shown below (I have removed the main function as it wasn't really doing anything):
def mainmenu():
stop = False
while stop == False:
print("=====WELCOME to the MAIN MENU=============")
print("""
1..........Add New Players & Goals
2..........Search by Players
3..........Quit
=========================================
""")
choice=int(input("Enter choice:"))
if choice==1:
player_info = addplayers()
elif choice==2:
searchplayer(player_info)
elif choice==3:
print ("Exit the main menu")
stop = True
else:
print("You must make a valid choice - 1, 2 or 3")
def addplayers():
player_info= {} #create a dictionary that stores the player name: player goals
num_players = int(input("Please enter number of players you wish to enter:"))
print ("You are entering %s players" %num_players)
player_data = ['Match 1 goals : ', 'Match 2 goals : ', 'Match 3 goals : ']
for i in range(0,num_players):
player_name = input("Enter Player Name :")
player_info[player_name] = {}
for entry in player_data:
player_info[player_name][entry] = int(input(entry)) #storing the marks entered as integers to perform arithmetic operations later on.
return player_info
def searchplayer(player_info):
print("===============SEARCH by player: Calculate average goals==================")
name = input("Player name : ")
if name in player_info.keys():
#print student_info
print ("Average player goals : ", str(sum(player_info[name].values())/3.0))
else:
print("Please enter a valid player name:")
mainmenu()
Store everything related to the game in a data structure, for example a dictionary, and pass it along in all functions where it can be updated as needed. Write a function "newgame" that creates this structure and initialises it.
In a way, this is object-oriented programming without using Python's syntax for classes and objects. Probably, you will learn these later in your class / tutorial.
Firstly, it is always possible to avoid using global variables. Secondly, global variables are possibly a misnomer in Python; global stores the variable in the local globals, which is typically the local module. That avoids a large part of the problem languages like C have with globals, in that they collide; Python has a namespace per module. For a simple script, where there is only one context, that might be fine.
Another namespace you might use is that of a particular object, using a class. This might look like:
class Game:
def mainmenu(self,...):
self.addplayers()
def addplayers(self):
self.player_info = {}
With that sort of code, whoever instantiates Game can make multiple instances, each passed as self when used. This is in large part syntactic sugar for a similar form of mutable state passing:
def mainmenu():
state={}
addplayers(state)
def addplayers(state):
state['player_info'] = {}
For some forms of programming, immutable state is far preferable (in particular, multithreading where data is shared, or to keep a log where you can undo steps). That's done similarly but you make a new state for each call:
def mainmenu():
state = {}
state = addplayers(state)
def addplayers(oldstate):
newstate = oldstate.copy()
newstate['player_info'] = {}
return newstate
Python isn't designed for this and doesn't really have a mode to keep you from inadvertently modifying mutable types. Some types can be converted to similar types that are immutable, like frozenset or tuple.
One of the weirder hacks we can do is calling a Python function with a different set of globals than it normally has. This can be abused to take your existing functions, global statements and all, and have them use a different variable:
fakeglobals = mainmenu.__globals__.copy()
exec(addplayers.__code__, fakeglobals)
Your original code has calls back and forth between functions, though, and each of those will reset their globals per that __globals__ attribute.
Your code also implements a loop using tail recursion. This is not optimized in Python and will run out of stack space eventually. In languages where tail recursion is optimized, you can pass along state as arguments continously and need not return it.

Categories

Resources