Python - Unexpected behaviour of class attributes - python

Edited in simple words
code:
class temp:
attr1 = 0
attr2 = []
t1 = temp()
t2 = temp()
t1.attr1 = 50
t1.attr2.append(50)
print(t1.attr1)
print(t1.attr2)
print(t2.attr1)
print(t2.attr2)
output:
50
[50]
0
[50]
I have called append only on attr2 object t1 but the append changes attr2 of both objects. if attr2 is shared (class attributes) then why does attr1 values are different for t1 and t2. What might have caused this unexpected behaviour ?
old question
I am writing a python code for blackjack. The code I have written is as follows.
from random import randint
from IPython.display import clear_output
deck = ["S","D","C","H"]
class Player:
cards = []
total = 0
amount = 0
def __init__(self,money=0):
self.amount = money
def busted(self):
return self.total > 21
def showCards(self):
for i in self.cards:
print("| {}{} |".format(i%13,deck[i//13]),end = " ")
print()
def hit(self):
no = randint(1,53)
self.cards.append(no)
if no % 13 == 1:
if self.total + 11 > 21:
self.total+=1
else:
self.total+=11
else:
self.total += (no%13 if no%13 <= 10 else 10)
dealer = Player(10000)
p1 = Player(0)
print("Welcome to BlackJack ....")
while True:
try:
p1.amount = int(input("Enter the amount you currrently have for the game"))
except:
print("invalid Value")
continue
else:
break
Game = True
while Game:
print(dealer.cards)
print(p1.cards)
dealer.hit()
print(dealer.cards)
print(p1.cards)
print(dealer.total)
print(p1.total)
Game = False
output of this code is as follows
Welcome to BlackJack ....
Enter the amount you currrently have for the game55
[]
[]
[45]
[45]
6
0
as you can see I had called hit() only once on dealer object but it is appending it to cards attribute of both dealer as well as p1 object. However total attribute is different. Can anyone explain what might have caused this unexpected behaviour ?

When you do t1.attr1 = 50, you're rebinding attr1 to a new value in the t1 object's attribute namespace. It previously let you access the value bound in the class namespace, but when you bind a new value, you hide the one from the class (for that instance only).
In contrast, when you do t1.attr2.append(50), you're mutating the existing list (which is bound in the class namespace, but is visible though all instances) in place, with no rebinding of variables happening at all. This is why you see the change in t2. The variables t1.attr2 and t2.attr2 are both references to the same object (which you can verify using the is operator: t1.attr2 is t2.attr2).
In general, it's usually not a good idea to use lists or other mutable values for class variables if you don't want them to be shared by all instances. It's not forbidden though, because sometimes you do specifically do want the shared behavior.

I got what you are asking. You need to differentiate all cards with player cards. So, instead of naming everything as cards, I would suggest doing this:
class Player:
all_cards = []
total = 0
amount = 0
and update __init__ as :
def __init__(self, money=0):
self.amount = money
self.player_cards = []
while doing append operation, append it to all_cards and to the player_cards. Anyway, you are printing only player cards, you can see different list of cards.
Here is full code :
from random import randint
from IPython.display import clear_output
deck = ["S","D","C","H"]
class Player:
all_cards = []
total = 0
amount = 0
def __init__(self,money=0):
self.player_cards = []
self.amount = money
def busted(self):
return self.total > 21
def showCards(self):
for i in self.player_cards:
print("| {}{} |".format(i%13,deck[i//13]),end = " ")
print()
def hit(self):
no = randint(1,53)
self.player_cards.append(no)
self.all_cards.append(no)
if no % 13 == 1:
if self.total + 11 > 21:
self.total+=1
else:
self.total+=11
else:
self.total += (no%13 if no%13 <= 10 else 10)
dealer = Player(10000)
p1 = Player(0)
print("Welcome to BlackJack ....")
while True:
try:
p1.amount = int(input("Enter the amount you currrently have for the game"))
except:
print("invalid Value")
continue
else:
break
Game = True
while Game:
print(dealer.player_cards)
print(p1.player_cards)
dealer.hit()
print(dealer.player_cards)
print(p1.player_cards)
print(dealer.total)
print(p1.total)
Game = False
This happened because list is a mutable object, and it is created once only when defining the class, that is why it becomes shared when you create two instances. Therefore, to solve this problem, we can use constructor like what I have mentioned above. When we put the list in constructor, whenever the object is instantiated, the new list will also be created.

Related

How do I make my (derived?) python instance attributes "self-update" based on alterations to other same-instance attributes?

I know that I have a misunderstanding of how Python attributes work because I'm here writing this problem, but I don't know exactly what I'm misunderstanding. I'm trying to get
self.card = self.hand[self.card_number].split()
self.card_val = deck.ranks.get(self.card[0])
to attain their values based on self.hand, which I pass to __init__ upon instantiation. Throughout the game I am altering .hand, and I want .card & .card_val to change every time I change .hand, instead of having to tell it to do that elsewhere (outside of the attribute definitions). I know there is a way to do this, or at least I think there is, and by that I mean simply by defining their values as inherited based on whatever .hand is at any given time, without calling an internal or external function.
In the posted code, I have altered it to work as the game instructions require, using...
def get_card_vals(p1, p2):
for player in [p1, p2]:
player.card = player.hand[player.card_number].split()
player.card_val = deck.ranks.get(player.card[0])
print("{a} vs. {b}".format(a = p1.card, b = p2.card))
print("---------------------------")
...but that's what I want to change. I want what that function is doing to be executed more concisely inside of the attribute definitions upon handling of the instance. Basically my question is why can't these two attributes get their values directly from the first attribute that I define via "hand" passed to the init?
Any help would be appreciated, and more importantly, I think more than just solutions, it would help me even more to understand what I am misunderstanding about how attributes, instances, and instantiation and all that works so that I know where my thinking is wrong. Thanks!
import random
from random import shuffle
from collections import deque
class Deck():
def __init__(self):
self.ranks = {"Ace":14, "King":13, "Queen":12, "Jack":11, "10":10, "9":9, "8":8, "7":7, "6":6, "5":5, "4":4, "3":3, "2":2}
self.suites = ["Heart", "Diamond", "Spade", "Club"]
self.cards = []
def create_cards(self):
for suite in self.suites:
for key in self.ranks.keys():
self.cards.append(key + " " + suite)
def shuffle(self):
random.shuffle(deck.cards)
deck = Deck()
deck.create_cards()
deck.shuffle()
class Player():
def __init__(self, hand):
self.name = "name"
self.hand = hand
self.card_number = 1
self.card = self.hand[self.card_number].split()
self.card_val = deck.ranks.get(self.card[0])
def war(bool, p1, p2):
if bool == True:
for player in [p1, p2]:
player.card_number = 4
else:
for player in [p1, p2]:
player.card_number = 0
p2 = Player(deque(deck.cards[::2]))
p1 = Player(deque(deck.cards[1::2]))
p2.name = "The Computer"
def get_card_vals(p1, p2):
for player in [p1, p2]:
player.card = player.hand[player.card_number].split()
player.card_val = deck.ranks.get(player.card[0])
print("{a} vs. {b}".format(a = p1.card, b = p2.card))
print("---------------------------")
def cant_war_lose(winner, loser):
print("{a} doesn't have enough cards to go to war, so {b} wins the Battle!".format(a = loser, b = winner))
def battle_win(winner, loser):
print("{a} has run out of cards, therefore {b} has won via Battle!".format(a = loser, b = winner))
def play_cards(p1, p2):
war(False, p1, p2)
get_card_vals(p1, p2)
if p1.card_val > p2.card_val:
p1.hand.append(p2.hand.popleft())
p1.hand.rotate(-1)
elif p1.card_val == p2.card_val:
if len(p1.hand) < 5 or len(p2.hand) < 5:
if len(p1.hand) > len(p2.hand):
cant_war_lose(p1.name, p2.name)
else:
cant_war_lose(p2.name, p1.name)
return 0
else:
input("War is inititated! Press Enter to continue!")
print("---------------------------")
war(True, p1, p2)
get_card_vals(p1, p2)
if p1.card_val > p2.card_val:
for i in range(0,5):
p1.hand.append(p2.hand.popleft())
p1.hand.rotate(-5)
elif p1.card_val < p2.card_val:
for i in range(0,5):
p2.hand.append(p1.hand.popleft())
p2.hand.rotate(-5)
else:
p1.hand.rotate(-1)
p2.hand.rotate(-1)
elif p1.card_val < p2.card_val:
p2.hand.append(p1.hand.popleft())
p2.hand.rotate(-1)
if len(p1.hand) != 0 and len(p2.hand) != 0:
input("After the last round of Battle, {a} now has {b} cards, and {c} now has {d} cards! Press Enter to continue!".format(a = p1.name, b = len(p1.hand), c = p2.name, d = len(p2.hand)))
print("---------------------------")
else:
if len(p1.hand) > len(p2.hand):
battle_win(p1.name, p2.name)
else:
battle_win(p2.name, p1.name)
return 0
def game_run():
run = 1
p1.name = input("Player 1's name? ")
print("---------------------------")
while run == 1:
if play_cards(p1, p2) == 0:
run = 0
game_run()
You can use the property decorator to create a calculated property
class Player():
def __init__(self, hand):
self.name = "name"
self.hand = hand
self.card_number = 1
#property
def hand(self):
return self._hand
#hand.setter
def hand(self, value):
self._hand = value
self.card = self._hand[self.card_number].split()
self.card_val = deck.ranks.get(self.card[0])
What you misunderstand is variables, not instances. For instance, the attribute card is a scalar variable attached to the instance. Assigning to it with
self.card = <blah>
does not bind it to blah for constant recomputation. This is a value assignment, not a memory mapping. If you want that long-term binding, you must either write the maintenance routine yourself -- which you've already done, in a way, with the consistent recomputation -- or you must assign a mutable reference to self.card, so that card refers to teh same object as the expression you created.
Given that you are consistently rotating and altering the hand, this is not feasible in your design. Instead, simply write an access routine, perhaps get_next_card(hand), which will rotate the hand, extract the card, and return the desired rank and suit.
If you plan to program more card games, you will also find it handy to define a class card and class hand, with appropriate support routines. Maintain the card as a pair of integers; convert to strings only for printing.
Does that get you moving?
For anyone who wanted to compare a before and after of the problem & final solution, below is the working code for my specific issue. All I had to do was convert self.card and self.card_val to a calculated property. By passing in hand, and subsequently handling only hand, self.card & self.card_val are calculated, since every time I handle the instance of the class (by handling hand), these "method attributes" are being called and altered. Thanks for the input, guys!
class Player():
def __init__(self, hand):
self.name = "name"
self.card_number = 1
self.hand = hand
#property
def card(self):
return self.hand[self.card_number].split()
#property
def card_val(self):
return deck.ranks.get(self.card[0])

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
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
I will redesign the app to introduce really powerful python constructs that may help you when getting into OOP.
Your objects are going to represent players, then don't call them Score, call them Player.
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.
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.
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.
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.

Problems with switching players and keeping track of each of their score when making a Cricket game

I want to create a very simple text-based Cricket game. But I am quite stuck.
from random import randint
class Player():
def __init__(self):
pass
def run(self, player, score):
# some code that will take a player and a score and associate that
# score with the player and store it (in a dictionary)
def switch(self, player):
# some code that will take a player name and change the current
# batsmen to the next one, for e.g this should change "a" to "b" or
# vice versa but not "c"
team_players = ["a", "b", "c"]
player = Player()
position = 0
run = randint(0,6)
current_batsman = team_players[position]
if run%2 == 0: # which means run is even
player.run(current_batsman, run) # sending the current player and their run
else: # if the run is odd
player.run(current_batsman, run) # still doing the same stuff as before but...
player.switch(current_batsman) # the current batsman should now be switched
Maybe tweaking position in the Player class somehow might help.
I hope my code explains my problem thoroughly. And by the way, in Cricket scores are called run and if a player (batsman) makes an odd run (1, 3, 5) the next batsman comes to play, and there are only two batsmen in the field until one gets out but I want my game to be really simple, for now. Any help is greatly appreciated. Thanks.
I don't know Cricket's rules but in class Player I would keep player's name and his score. It could have also function run() which adds random value to his score (or create method with parameter - value which you want to add to score)
class Player():
def __init__(self, name):
self.name = name
self.score = 0
def run(self):
self.score += random.randint(0, 6)
def __str__(self):
return "{} (score: {})".format(self.name, self.score)
I also added __str__ to easily display player.
Next I would create class Team which keeps all players, keep information which player is current batsman, switch batsman, and use run() for current batsman
class Team():
def __init__(self, players):
self.players = players
self.current_batsman = 0
self.current_run = 0
def set_next_batsman(self):
self.current_batsman += 1
if self.current_batsman >= len(self.players):
self.current_batsman = 0
def get_current_batsman(self):
return self.players[self.current_batsman]
def run(self):
self.players[self.current_batsman].run()
if self.current_run % 2 != 0:
self.set_next_batsman()
self.current_run += 1
def __str__(self):
return "Player: " + ", ".join(str(p) for p in self.players)
def total_score(self):
return sum(p.score for p in self.players)
And then two teams can play:
team1 = Team( [Player("a"), Player("b"), Player("c")] )
team2 = Team( [Player("x"), Player("y"), Player("z")] )
print('Team1:', team1)
print('Team2:', team2)
for number in range(1, 5):
print('Round:', number)
print('Team1 current batsman:', team1.get_current_batsman())
team1.run()
print('Team2 current batsman:', team2.get_current_batsman())
team2.run()
print('Team1:', team1)
print('Team2:', team2)
print('Team1 total score:', team1.total_score())
print('Team2 total score:', team2.total_score())

Why does this list return an Int rather than an object?

Tcard.attack(self.players[self.opponent])
AttributeError: 'int' object has no attribute 'attack'
This is the error I get from calling attack().
Tcard = self.players[self.turn].returnCard()
Tcard.attack(self.players[self.opponent])
For some odd reason when Tcard.attack() calls with the parameters self.players[self.opponent], the list returns an int ranther than a Player object. Can someone please explain why it is returning an int rather than an object?
Here is the code for the whole file:
class Game():
def __init__(self):
self.players = []
self.turn = 0
self.opponent = 1
def prepare_game(self):
AI_1 = AI("AI_1", 1000)
AI_1.generate_inventory()
AI_2 = AI("AI_2", 1000)
AI_2.generate_inventory()
self.players.append(AI_1)
self.players.append(AI_2)
def start(self):
p1 = self.players[self.turn]
p2 = self.players[self.opponent]
Tcard = self.players[self.turn].returnCard()
print "Battle time"
print "%s attacked %s" % (p1.Name, p2.Name)
Tcard.attack(self.players[self.opponent])
#switch
if self.turn == 0:
self.turn = 1
self.opponent = 0
self.start()
else:
self.turn = 0
self.opponent = 1
self.start()
Here is where the function returnCard is at:
class AI():
def __init__(self, Name, Health):
self.Name = Name
self.Health = Health
self.inventory = []
def generate_inventory(self):
#Generate 6 Cards and 3 Power-ups
#rand_powerups = random.randint(min(len(powerup)), max(len(powerup)))
rand_cards = random.randint(0, 4)
#self.inventory.append(rand_powerups)
while len(self.inventory) != 4:
self.inventory.append(rand_cards)
if len(self.inventory) == 4:
break
def returnCard(self):
return self.inventory[random.randrange(0, 4)]
def returnCard(self):
return self.inventory[random.randrange(0, 4)]
returnCard returns a random item of self.inventory.
And self.inventory is filled by generate_inventory which does this:
# generate a random *INT*
rand_cards = random.randint(0, 4)
while len(self.inventory) != 4:
# append that *INT*
self.inventory.append(rand_cards)
# (note that this keeps adding the same number over and over)
So, of course, returnCard will return an int here:
Tcard = self.players[self.turn].returnCard()
So when you try to call attack you try to call it on an int:
Tcard.attack(self.players[self.opponent])
self.inventory = [] # a list
rand_cards = random.randint(0, 4)
self.inventory.append(rand_cards) # you add ints to the list
# you return an int
return self.inventory[random.randrange(0, 4)]
# you set Tcard equal to an int returned from ^^
Tcard = self.players[self.turn].returnCard()
On another note you should use range to add the random ints and keep calling randint or you will just get the same number added to your list:
def generate_inventory(self):
for _ in range(4):
self.inventory.append(random.randint(0, 4))
If you want to use the methods in your class, create an instance. I could give you an example but I have no idea where attack comes from.
First of all, Tcard becomes a variable due to this line:
Tcard = self.players[self.turn].returnCard()
By assigning Tcard the result of .returnCard() which will always be an integer since you made returnCard() to return an integer with:
return self.inventory[random.randrange(0, 4)]
So since an integer can't have any attributes, that will be an error. Thus the raised error saying that an int has no attribute.
Second, Tcard is not even a function. Only functions can have attributes thus adding more to the error. You need to create a function for Tcard to be able to work. Add something like:
class Tcard:
def attack():
#Do something

Python list of object are sharing variables

class Journey has a list, self.leg_miles
class ManyJourneys has a list of journeys created by
self.journeys = []
for i in range(2):
self.journeys.append(Journey())
from some input I add to the list leg_miles for each Journey kind of like this:
self.journeys[self.count].addLeg(temp)
temp is a number i read from a list. it is always changed to what was inputted right before the above line.
for some reason instead of creating a new list for journey[1] it just adds to the created list.
for example:
if for journey[0] the leg_miles had [4,5,6]
and I moved on to journey[1] to add 4 and 6
it would have :
leg_miles = [4,5,6,4,6]
I do not understand why it is adding on. I have self in it.
I do not understand how to upload code or pictures.
I hope this is enough information to solve the problem
EDIT: Here is the code I have.
class Journey:
def __init__(self,odometer=0,leg_miles=[],leg_gas=[]): #constructor
self.odometer = odometer
self.leg_miles = leg_miles
self.leg_gas = leg_gas
def addLeg(self,miles,gas):
#adds to a list of miles and gas
self.leg_miles.append(miles)
self.leg_gas.append(gas)
def getLists(self):
#returns the list of miles and the list of gas
return self.leg_miles,self.leg_gas
def calculate(self):
#calculate the miles per litre for the journey and return
miles_per_litre = 0
for i in range(len(self.leg_miles)): #calcs miles per litre for each leg and adds to total
miles_per_litre += int(self.leg_miles[i]/self.leg_gas[i])
return miles_per_litre
class ManyJourneys:
def __init__(self,name):
self.myfile = open(name,"r")
self.content = self.myfile.read().split("\n")
self.journeys = []
for i in range(self.content.count("")+1):
self.journeys.append(Journey())
self.count = 0
for i in self.content:
if i == "":
self.count+=1
else:
temp = i.split(" ")
self.journeys[self.count].addLeg(int(temp[0]),int(temp[1]))
def statistics(self):
sum = 0
for i in self.journeys:
sum += i.calculate()
return sum/len(self.journeys)
def main():
filename = input("Please enter a file name. (Eg: test.txt): ")
manyJourneys1 = ManyJourneys(filename)
print("You got {0} miles per litre.".format(manyJourneys1.statistics()))
main()
and a sample text file would be
100 54
340 109
23 4
333 33
4500 678
There is an unobvious feature in Python regarding default values - they represent the same object each time, which causes surprising behavior when using mutable values as defaults:
def add(value, l=[]):
l.append(value)
return l
add(1) # [1]
add(2) # [1, 2]
To not fall into this trap do not use list or any other mutable value this way. If you need to initialize something to an empty list do it in a function/method body:
def __init__(self, odometer=0, leg_miles=None, leg_gas=None):
self.odometer = odometer
self.leg_miles = leg_miles or []
self.leg_gas = leg_gas or []
You screwed up your definition of Journey. Mutable types must be assigned in the initializer or in methods, not in the class or default arguments.

Categories