Managing Items in an Object Oriented game - python

Every once in a while I like to take a break from my other projects to try to make a classic adventure text-based-game (in Python, this time) as a fun project, but I always have design issues implementing the item system.
I'd like for the items in the game to descend from one base Item class, containing some attributes that every item has, such as damage and weight. My problems begin when I try to add some functionality to these items. When an item's damage gets past a threshold, it should be destroyed. And there lies my problem: I don't really know how to accomplish that.
Since del self won't work for a million different reasons, (Edit: I am intentionally providing the use of 'del' as something that I know is wrong. I know what garbage collection is, and how it is not what I want.) how should I do this (And other similar tasks)? Should each item contain some kind of reference to it's container (The player, I guess) and 'ask' for itself to be deleted?
The first thing that comes to mind is a big dictionary containing every item in the game, and each object would have a reference to this list, and both have and know it's own unique ID. I don't like this solution at all and I don't think that it's the right way to go at all. Does anybody have any suggestions?
EDIT: I'm seeing a lot of people thinking that I'm worried about garbage collection. What I'm talking about is not garbage collection, but actually removing the object from gameplay. I'm not sure about what objects should initiate the removal, etc.

I would have your object keep a reference to all of its parents. Then, when it should be destroyed, it would notify its parents. If you're already using an event system, this should integrate nicely with the rest of the game.
A nice way to avoid forcing your parent to explicitly notify the object whenever the reference is dropped or added is to use some sort of proxy. Python supports properties that will allow for code like self.weapon = Weapon() to actually hand off the duty of setting the weapon attribute to the new weapon to a user defined function.
Here's some example code using properties:
class Weapon(object):
def __init__(self, name):
self.name = name
self.parent = None
def destroy(self):
if self.parent:
self.parent.weaponDestroyed()
def WeaponRef():
def getWeapon(self):
return self._weapon
def setWeapon(self, newWeapon):
if newWeapon == None: #ensure that this is a valid weapon
delWeapon(self)
return
if hasattr(self, "weapon"): #remove old weapon's reference to us
self._weapon.parent = None
self._weapon = newWeapon
newWeapon.parent = self
def delWeapon(self):
if hasattr(self, "weapon"):
self._weapon.parent = None
del self._weapon
return property(getWeapon, setWeapon, delWeapon)
class Parent(object):
weapon = WeaponRef()
def __init__(self, name, weapon=None):
self.name = name
self.weapon = weapon
def weaponDestroyed(self):
print "%s deleting reference to %s" %(self.name, self.weapon.name)
del self.weapon
w1 = Weapon("weapon 1")
w2 = Weapon("weapon 2")
w3 = Weapon("weapon 3")
p1 = Parent("parent 1", w1)
p2 = Parent("parent 2")
w1.destroy()
p2.weapon = w2
w2.destroy()
p2.weapon = w3
w3.destroy()
Now if you're doing some sort of inventory system, where a player can have more than 1 weapon and any one of them can be destroyed at any time, then you're going to have to write your own collection class.
For something like that, just keep in mind that x[2] calls x.__getitem__(2), x[2] = 5 calls x.__setitem__(2, 5) and del x[2] calls x.__delitem__(2)

You're conflating two meanings of the "destroying" idea. The Item should get destroyed in a "gameplay" sense. Let the garbage collector worry about when to destroy it as an object.
Who has a reference to the Item? Perhaps the player has it in his inventory, or it is in a room in the game. In either case your Inventory or Room objects know about the Item. Tell them the Item has been destroyed (in a gameplay sense) and let them handle that. Perhaps they'll now keep a reference to a "broken" Item. Perhaps they'll keep track of it, but not display it to the user. Perhaps they'll delete all references to it, in which case the object in memory will soon be deleted.
The beauty of object-oriented programming is that you can abstract these processes away from the Item itself: pass the messages to whoever needs to know, and let them implement in their own way what it means for the Item to be destroyed.

One option would be to use a signal system
Firstly, we have a reusable class that lets you define a signal
class Signal(object):
def __init__(self):
self._handlers = []
def connect(self, handler):
self._handlers.append(handler)
def fire(self, *args):
for handler in self._handlers:
handler(*args)
Your item class uses this signal to create a destroyed signal that other classes can listen for.
class Item(object):
def __init__(self):
self.destroyed = Signal()
def destroy(self):
self.destroyed.fire(self)
And inventory listens to the signals from the items and updates its internal state accordingly
class Inventory(object):
def __init__(self):
self._items = []
def add(self, item):
item.destroyed.connect(self.on_destroyed)
self._items.add(item)
def on_destroyed(self, item):
self._items.remove(item)

Assuming you call a method when the item is used, you could always return a boolean value indicating whether it's broken.

How about:
from collections import defaultdict
_items = defaultdict(set)
_owner = {}
class CanHaveItems(object):
#property
def items(self):
return iter(_items[self])
def take(self, item):
item.change_owner(self)
def lose(self, item):
""" local cleanup """
class _nobody(CanHaveItems):
def __repr__(self):
return '_nobody'
_nobody = _nobody()
class Destroyed(object):
def __repr__(self):
return 'This is an ex-item!'
class Item(object):
def __new__(cls, *a, **k):
self = object.__new__(cls)
_owner[self] = _nobody
_items[_nobody].add(self)
self._damage = .0
return self
def destroy(self):
self.change_owner(_nobody)
self.__class__ = Destroyed
#property
def damage(self):
return self._damage
#damage.setter
def damage(self, value):
self._damage = value
if self._damage >= 1.:
self.destroy()
def change_owner(self, new_owner):
old_owner = _owner[self]
old_owner.lose(self)
_items[old_owner].discard(self)
_owner[self] = new_owner
_items[new_owner].add(self)
class Ball(Item):
def __init__(self, color):
self.color = color
def __repr__(self):
return 'Ball(%s)' % self.color
class Player(CanHaveItems):
def __init__(self, name):
self.name = name
def __repr__(self):
return 'Player(%s)' % self.name
ball = Ball('red')
ball = Ball('blue')
joe = Player('joe')
jim = Player('jim')
print list(joe.items), ':', list(jim.items)
joe.take(ball)
print list(joe.items), ':', list(jim.items)
jim.take(ball)
print list(joe.items), ':', list(jim.items)
print ball, ':', _owner[ball], ':', list(jim.items)
ball.damage += 2
print ball, ':', _owner[ball], ':', list(jim.items)
print _items, ':', _owner

at first: i don't have any python experience, so think about this in a more general way
your item should neither know or care ... your Item should have an interface that says it is something destroyable. containers and other objects that care about things that can be destroyed, can make use of that interface
that destroyable interface could have some option for consuming objects to register a callback or event, triggered when the item gets destroyed

Related

Is it possible to create an object that actualize its attributes when modified?

In this example, what should be done so that print(left_hand.number_of_fingers) returns 4 and not 5?
class Hand:
def __init__(self, fingers:list):
self.fingers = fingers
self.number_of_fingers = len(fingers)
left_hand = Hand(["thumb", "index", "middle", "ring", "pinkie"])
left_hand.fingers.pop()
print(left_hand.number_of_fingers) # I want this to actualize and be 4, not 5
I found a solution using #property
class Hand:
def __init__(self, fingers:list):
self.fingers = fingers
#property
def number_of_fingers(self):
return len(self.fingers)
But I'm not satisfied because of a computational power issue, if computing number_of_fingers was expensive we would only want to compute it whenever fingers is modified, not every time the user asks for the attribute number_of_fingers.
Now I found a not elegant solution to solve the issue with computational power:
class Hand:
def __init__(self, fingers:list):
self.fingers = fingers
self.old_fingers = fingers
self.number_of_fingers = len(fingers)
def get_number_of_fingers(self):
if self.fingers != self.old_fingers:
self.old_fingers = self.fingers
self.number_of_fingers = len(self.fingers)
return self.number_of_fingers
The problem is that the underlying list in your Hand class, i.e. self.fingers, is not sufficiently encapsulated so that any user can be modifying it, for example by calling left_hand.fingers.pop() or even by assigning to it a new list. Therefore, you cannot assume that it has not been modified between calls to number_of_fingers and therefore you have no choice but to compute its length in that call.
The solution is to control what clients of your class can and cannot do. The easiest way to do this is by using name mangling. That is, you prefix your attribute names with two leading underscore characters. This makes it difficult (although not impossible) for clients of your class to access these attributes from outside of the class (we assume that your users are not intentionally malicious). And therefore we have to provide now a pop method:
class Hand:
def __init__(self, fingers:list):
self.__fingers = fingers
self.__number_of_fingers = len(fingers)
def pop(self):
assert(self.__fingers)
self.__number_of_fingers -= 1
return self.__fingers.pop()
#property
def number_of_fingers(self):
return self.__number_of_fingers
left_hand = Hand(["thumb", "index", "middle", "ring", "pinkie"])
print(left_hand.pop())
print(left_hand.number_of_fingers)
Prints:
pinkie
4
I am not suggesting that you actually do the following, but if you wanted to you can get more elaborate by creating special class decorators #Private and #Public that will wrap your class in a new class and check access to your attributes ensuring that you are not accessing those attributes defined to be private. You use either the #Private decorator to define those attributes/methods that are private (everything else is considered public) or the #Public decorator to define those attributes/methods that are public (everything else is considered private), but not both. You would typically name your private attributes with a leading single underscore, which is the convention that tells users that the attribute/method is to be considered private.
This is meant more to catch inadvertent access of attributes that are meant to be private. If you execute the code with the -O Python flag, then no runtime checks will be made.
def accessControl(failIf):
def onDecorator(aClass):
if not __debug__:
return aClass
else:
class onInstance:
def __init__(self, *args, **kargs):
self.__wrapped = aClass(*args, **kargs)
def __getattr__(self, attr):
if failIf(attr):
raise TypeError('private attribute fetch: ' + attr)
else:
return getattr(self.__wrapped, attr)
def __setattr__(self, attr, value):
if attr == '_onInstance__wrapped':
self.__dict__[attr] = value
elif failIf(attr):
raise TypeError('private attribute change: ' + attr)
else:
setattr(self.__wrapped, attr, value)
return onInstance
return onDecorator
def Private(*attributes):
return accessControl(failIf=(lambda attr: attr in attributes))
def Public(*attributes):
return accessControl(failIf=(lambda attr: attr not in attributes))
#Private('_fingers', '_number_of_fingers')
class Hand:
def __init__(self, fingers:list):
self._fingers = fingers
self._number_of_fingers = len(fingers)
def pop(self):
assert(self._fingers)
self._number_of_fingers -= 1
return self._fingers.pop()
#property
def number_of_fingers(self):
return self._number_of_fingers
left_hand = Hand(["thumb", "index", "middle", "ring", "pinkie"])
print(left_hand.pop())
print(left_hand.number_of_fingers)
# Thsis will throw an exception:
print(left_hand._fingers)
Prints:
pinkie
4
Traceback (most recent call last):
File "C:\Booboo\test\test.py", line 50, in <module>
print(left_hand._fingers)
File "C:\Booboo\test\test.py", line 9, in __getattr__
raise TypeError('private attribute fetch: ' + attr)
TypeError: private attribute fetch: _fingers
Update
This is the OP's approach using a cache:
class Hand:
def __init__(self, fingers:list):
self._cache = {}
self.fingers = fingers
def get_number_of_fingers(self):
fingers = tuple(self.fingers) # can be key of a dictionary
fingers_length = self._cache.get(fingers)
if fingers_length:
print(self.fingers, 'in cache')
return fingers_length
fingers_length = len(fingers)
self._cache[fingers] = fingers_length
return fingers_length
left_hand_fingers = ["thumb", "index", "middle", "ring", "pinkie"]
right_hand_fingers = ["thumb", "middle", "ring", "pinkie"]
hand = Hand(left_hand_fingers)
print(hand.get_number_of_fingers())
hand.fingers = right_hand_fingers
print(hand.get_number_of_fingers())
hand.fingers = left_hand_fingers
print(hand.get_number_of_fingers())
hand.fingers = right_hand_fingers
print(hand.get_number_of_fingers())
hand.fingers = left_hand_fingers
print(hand.get_number_of_fingers())
Prints:
5
4
['thumb', 'index', 'middle', 'ring', 'pinkie'] in cache
5
['thumb', 'middle', 'ring', 'pinkie'] in cache
4
['thumb', 'index', 'middle', 'ring', 'pinkie'] in cache
5
So here in the first code (without using #property), you will get the output as 5 and not 4, because you are simply assigning the value of len(fingers) to number_of_fingers attribute while initialising a Hand object, and number_of_fingers attribute is not getting linked to fingers.
So even if left_hand.fingers is modified in between the code, it will have no effect on the value of number_of_fingers. One cannot change this behaviour.
Also you don't need that #property, I tested and found that there will be no error if it is not written.
And finally coming to
But I'm not satisfied, because if computing number_of_fingers was expensive we would only want to compute it whenever fingers is modified, not every time the user asks for the attribute number_of_fingers.
Where do you need so much computing power?

Where did I mess up in this program for tracking faction alliances?

I have a program that models kingdoms and other groups (called 'factions' in my code).
class Faction:
def __init__(self, name, allies=[]):
self.name = name
self.allies = allies
def is_ally_of(self, other_faction):
if self in other_faction.allies:
return True
else:
return False
def become_ally(self, other_faction, both_ally=True):
""" If both_ally is false, this does *not* also
add self to other_faction's ally list """
if self.is_ally_of(other_faction):
print("They're already allies!")
else:
self.allies.append(other_faction)
if both_ally == True:
other_faction.become_ally(self, False)
RezlaGovt = Faction("Kingdom of Rezla")
AzosGovt = Faction("Azos Ascendancy")
I want to be able to call a factions become_ally() method to add factions to the ally lists, like this:
RezlaGovt.become_ally(AzosGovt) # Now AzosGovt should be in RezlaGovt.allies,
# and RezlaGovt in AzosGovt.allies
What actually happens is this:
RezlaGovt.become_ally(AzosGovt)
# prints "They're already allies!"
# now AzosGovt is in the allies list of both AzosGovt and RezlaGovt,
# but RezlaGovt isn't in any allies list at all.
Whenever I try to call become_ally(), the code should check to make sure they aren't already allies. This is the part that isn't working. Every time I call become_ally(), it prints "They're already allies!", regardless of if they actually are.
I also tried to use if self in other_faction.allies:, but that had the same problem.
I strongly suspect that the problem is with my use of self, but I don't know what terms to Google for more information.
You can't use mutable arguments as the default argument to a function.
def __init__(self, name, allies=[]):
When the default is used, it's the same list each time, so they have the same allies; mutating one changes the other because they're actually the same thing.
Change to:
def __init__(self, name, allies=None):
if allies is None:
allies = []
Alternatively, copy the allies argument unconditionally (so you're not worried about a reference to it surviving outside the class and getting mutated under the class):
def __init__(self, name, allies=[]):
self.allies = list(allies) # Which also guarantees a tuple argument becomes list
# and non-iterable args are rejected
Change this function.
def is_ally_of(self, other_faction):
if other_faction in self.allies:
return True
else:
return False
Check your own data not that of the passed in object.
Also
def __init__(self, name, allies=[]):
Is a bug waiting to happen. Your allies list will be a static list shared between all instances. Instead use
def __init__(self, name, allies=None):
self.name = name
self.allies = allies or []

Chaining class decorators to extend game item's abilities

I'm coding a little game for my python course, and I want to integrate an inventory and item system. The possibilities offered by the item are variables (weapons, quest item, consumable or not, an so on).
I have read this tutorial (in French) about the pattern decorator (google translated in english) and I came with this:
(I am using python3)
class Item(object):
def __init__(self, caracts=None, inventory=None):
self.caracts = {}
if caracts:
self.caracts = caracts
self.inventory = inventory
class ItemDecorator(Item):
def __init__(self, item):
super().__init__()
self.item = item
self.caracts = item.caracts
class Consumable(ItemDecorator):
def __init__(self, item, amount=1):
super().__init__(item)
self._amount = 0
self.amount = amount
#property
def amount(self):
return self._amount
#amount.setter
def amount(self, value):
self._amount = max(0, value)
if self._amount == 0 and self.item.inventory:
self.item.inventory.remove(self)
#amount.deleter
def amount(self):
del self._amount
class Usable(ItemDecorator):
def __init__(self, item, action=None, consumable=None):
if not action:
action = lambda *args, **kwargs: None
self._use = action
self.consumable = consumable
def use(self, *args, **kwargs):
if self.consumable and self.consumable.amount <= 0:
raise CantBeUsedException("There is no consumable")
else:
if self.consumable:
self.consumable.amount -= 1
self._use(*args, **kwargs)
My idea is to be able to do this:
potion = Usable(Consumable(Item(), 3), use_potion)
print("isinstance(potion.item, Consumable): {}".format(
isinstance(potion.item, Consumable)))
potion.consumable = potion.item
for dummy in range(4):
try:
potion.use()
except CantBeUsedException as e:
print("Expected exception: {}".format(e))
But here comes my issue, line 4. The consumable used by the usable potion should be potion itself. But potion lost its consumable ability and only potion.item has it. It's even worst, because the order in which I call my decorator matters. potion = Consumable(Usable(Item(), use_potion), 3) leads me to do potion.item.use(), always this item that annoys me.
How can I simplify this? Knowing that a usable doesn't necessarily consume itself, or even something. In fact, I would like to be able to do this, no matter which decorator was called first:
potion = Consumable(Usable(Item(), use_potion), 3)
potion.consumable = potion
potion.use()
I don't manage to found a clean solution for my issue. Here is all the questions that come to my mind:
* Is this Decorator pattern adapted? (It looks so to my mind, but I can be wrong)
* If it's not the case, to your mind, wouldn't be an interface system (thus, with multiple heritage) a better solution?
* What did I do wrong to get stuck here?
How can I make this system really simple while still being extensible. For this, I think about this solution:
class ItemDecorator(Item):
def __init__(self, item):
super().__init__()
self.item = item
self.caracts = item.caracts
if hasattr(item, "amount"):
self.amount = item.amount
if hasattr(item, "use"):
self.use = item.use
But by doing so, don't I lose all the extensibility of the Decorator pattern? Indeed, I would need to update ItemDecorator each time I want to create a quite complex decorator. Thus, wouldn't I lose all the advantage of the decorator pattern?
Thank you very much for your help
Right now your classes layout is rather twisted, and Usable(Consumable(Item(), 3), use_potion) doesn't look pythonic.
I'd slightly redesign the system:
class ItemCapability:
pass
class Consumable(ItemCapability):
def __init__(self, amount):
super().__init__()
self.amount = amount
class Usable(ItemCapability):
pass
class Item:
def __init__(self, name):
self.name = name
self.capabilities = {}
def make(self, capability):
assert isinstance(capability, ItemCapability)
assert capability.__class__ not in self.capabilities
self.capabilities[capability.__class__] = capability
def has(self, capability_cls):
try:
return self.capabilities[capability_cls]
except KeyError:
return False
potion = Item('potion')
potion.make(Usable())
potion.make(Consumable(amount=10))
print(potion.has(Usable))
print(potion.has(Consumable))
This way you have a very simple to understand class system, and easy way to query your items for capabilities.

Python: Preventing duplication of data when using dictionaries and lists

Hello Stack Overflow!
I am executing a simple command in a program that compiles a report of all the books contained in a library. The library contains a list of shelves, each shelves contains a dictionary of books. However, despite my best efforts, I am always duplicating all my books and placing them on every shelf, instead of the shelf I've instructed the program to place the book on.
I expect I have missed out on some kind of fundamental rule with object creation and organization.
I believe the culprits are the enshelf and unshelf methods in the book class.
Thank you so much for your time,
Jake
Code below:
class book():
shelf_number = None
def __init__(self, title, author):
super(book, self).__init__()
self.title = title
self.author = author
def enshelf(self, shelf_number):
self.shelf_number = shelf_number
SPL.shelves[self.shelf_number].books[hash(self)] = self
def unshelf(self):
del SPL.shelves[self.shelf_number].books[hash(self)]
return self
def get_title(self):
return self.title
def get_author(self):
return self.author
class shelf():
books = {}
def __init__(self):
super(shelf, self).__init__()
def get_books(self):
temp_list = []
for k in self.books.keys():
temp_list.append(self.books[k].get_title())
return temp_list
class library():
shelves = []
def __init__(self, name):
super(library, self).__init__()
self.name = name
def make_shelf(self):
temp = shelf()
self.shelves.append(temp)
def remove_shelf(shelf_number):
del shelves[shelf_number]
def report_all_books(self):
temp_list = []
for x in range(0,len(self.shelves)):
temp_list.append(self.shelves[x].get_books())
print(temp_list)
#---------------------------------------------------------------------------------------
#----------------------SEATTLE PUBLIC LIBARARY -----------------------------------------
#---------------------------------------------------------------------------------------
SPL = library("Seattle Public Library")
for x in range(0,3):
SPL.make_shelf()
b1 = book("matterhorn","karl marlantes")
b2 = book("my life","bill clinton")
b3 = book("decision points","george bush")
b1.enshelf(0)
b2.enshelf(1)
b3.enshelf(2)
print(SPL.report_all_books())
b1.unshelf()
b2.unshelf()
b3.unshelf()
OUTPUT:
[['decision points', 'my life', 'matterhorn'], ['decision points', 'my life', 'matterhorn'], ['decision points', 'my life', 'matterhorn']]
None
[Finished in 0.1s]
..instead of [["decision points"],["my life"],["matterhorn"]]
Use dict.pop() instead of del.
Add self.books = {} to shelf's __init__. Don't declare books outside of the __init__, because if you do so, all of the instances of that class are going to refer to the same thing. Instead, this makes each instance have its own dictionary, which is of course what you want since a book can't be in two shelves at once.
Do the same for library and its shelves and book and its shelf_number.
Pass a library instance as an argument to enshelf and unshelf. When you refer to SPL from within your objects' methods, Python finds that there is no local SPL defined, so it searches for one outside of the local scope; but if you were to try to assign something to SPL or do some other sort of mutative business, you would get an UnboundLocalError.
Bonuses:
class book(object), class shelf(object), and class library(object). (Won't fix your problem, but you should do that anyway.)
You don't need to hash the keys before using them, they will be hashed (if they are hashable, but if you're hashing them, then they are).
There is no need to call super() unless you are inheriting from something, in which case you can delegate a method call to a parent or sibling using it - but you aren't doing that.
get_books() can be implemented as nothing more than return [self.books[k].get_title() for k in self.books.iterkeys()]
Likewise for report_all_books(): return [shlf.get_books() for shlf in self.shelves]. Note that I am not iterating over the indices, but rather over the elements themselves. Try for c in "foobar": print(c) in the interactive shell if you want to see for yourself.

Instantiating a unique object every time when using object composition?

As an example, just a couple of dummy objects that will be used together. FWIW this is using Python 2.7.2.
class Student(object):
def __init__(self, tool):
self.tool = tool
def draw(self):
if self.tool.broken != True:
print "I used my tool. Sweet."
else:
print "My tool is broken. Wah."
class Tool(object):
def __init__(self, name):
self.name = name
self.broken = False
def break(self):
print "The %s busted." % self.name
self.broken = True
Hammer = Tool(hammer)
Billy = Student(Hammer)
Tommy = Student(Hammer)
That's probably enough code, you see where I'm going with this. If I call Hammer.break(), I'm calling it on the same instance of the object; if Billy's hammer is broken, so is Tommy's (it's really the same Hammer after all).
Now obviously if the program were limited to just Billy and Tommy as instances of Students, the fix would be obvious - instantiate more Hammers. But clearly I'm asking because it isn't that simple, heh. I would like to know if it's possible to create objects which show up as unique instances of themselves for every time they're called into being.
EDIT: The kind of answers I'm getting lead me to believe that I have a gaping hole in my understanding of instantiation. If I have something like this:
class Foo(object):
pass
class Moo(Foo):
pass
class Guy(object):
def __init__(self, thing):
self.thing = thing
Bill = Guy(Moo())
Steve = Guy(Moo())
Each time I use Moo(), is that a separate instance, or do they both reference the same object? If they're separate, then my whole question can be withdrawn, because it'll ahve to make way for my mind getting blown.
You have to create new instances of the Tool for each Student.
class Student(object):
def __init__(self, tool):
self.tool = tool
def draw(self):
if self.tool.broken != True:
print "I used my tool. Sweet."
else:
print "My tool is broken. Wah."
class Tool(object):
def __init__(self, name):
self.name = name
self.broken = False
def break(self):
print "The %s busted." % self.name
self.broken = True
# Instead of instance, make it a callable that returns a new one
def Hammer():
return Tool('hammer')
# Pass a new object, instead of the type
Billy = Student(Hammer())
Tommy = Student(Hammer())
I'll try to be brief. Well.. I always try to be brief, but my level of success is pretty much random.randint(0, never). So yeah.
Lol. You even failed to be brief about announcing that you will try to be brief.
First, we need to be clear about what "called into being" means. Presumably you want a new hammer every time self.tool = object happens. You don't want a new instance every time, for example, you access the tool attribute, or you'd always a get a new, presumably unbroken, hammer every time you check self.tool.broken.
A couple approaches.
One, give Tool a copy method that produces a new object that should equal the original object, but be a different instance. For example:
class Tool:
def __init__(self, kind):
self.kind = kind
self.broken = False
def copy(self):
result = Tool(self.kind)
result.broken = self.broken
return result
Then in Student's init you say
self.tool = tool.copy()
Option two, use a factory function.
def makehammer():
return Tool(hammer)
class Student:
def __init__(self, factory):
self.tool = factory()
Billy = Student(makehammer)
I can't think any way in Python that you can write the line self.tool = object and have object automagically make a copy, and I don't think you want to. One thing I like about Python is WYSIWYG. If you want magic use C++. I think it makes code hard to understand when you not only can't tell what a line of code is doing, you can't even tell it's doing anything special.
Note you can get even fancier with a factory object. For example:
class RealisticFactory:
def __init__(self, kind, failurerate):
self.kind = kind
self.failurerate = failurerate
def make(self):
result = Tool(self.kind)
if random.random() < self.failurerate:
result.broken = True
if (self.failurerate < 0.01):
self.failurerate += 0.0001
return result
factory = RealisticFactory(hammer, 0.0007)
Billy = Student(factory.make)
Tommy = Student(factory.make) # Tommy's tool is slightly more likely to be broken
You could change your lines like this:
Billy = Student(Tool('hammer'))
Tommy = Student(Tool('hammer'))
That'll produce a distinct instance of your Tool class for each instance of the Student class. the trouble with your posted example code is that you haven't "called the Tool into being" (to use your words) more than once.
Just call Tool('hammer') every time you want to create a new tool.
h1 = Tool('hammer')
h2 = Tool('hammer')
Billy = Student(h1)
Tommy = Student(h2)
Oh wait, I forgot, Python does have magic.
class Student:
def __setattr__(self, attr, value):
if attr == 'tool':
self.__dict__[attr] = value.copy()
else:
self.__dict__[attr] = value
But I still say you should use magic sparingly.
After seeing the tenor of the answers here and remembering the Zen of Python, I'm going to answer my own dang question by saying, "I probably should have just thought harder about it."
I will restate my own question as the answer. Suppose I have this tiny program:
class Item(object):
def __init__(self):
self.broken = False
def smash(self):
print "This object broke."
self.broken = True
class Person(object):
def __init__(self, holding):
self.holding = holding
def using(self):
if self.holding.broken != True:
print "Pass."
else:
print "Fail."
Foo = Person(Item())
Bar = Person(Item())
Foo.holding.smash()
Foo.using()
Bar.using()
The program will return "Fail" for Foo.using() and "Pass" for Bar.using(). Upon actually thinking about what I'm doing, "Foo.holding = Item()" and "Bar.holding = Item()" are clearly different instances. I even ran this dumpy program to prove it worked as I surmised it did, and no surprises to you pros, it does. So I withdraw my question on the basis that I wasn't actually using my brain when I asked it. The funny thing is, with the program I've been working on, I was already doing it this way but assuming it was the wrong way to do it. So thanks for humoring me.

Categories

Resources