I'm looking for a design pattern suited to Python that will address the following problem:
Assuming a class Company that has a member employees, which is a list that will contain any number of Employees.
What I would like to capture is when one of the Employee's member variables (let's say salary) changes, that change is reflected in the "owning" Company (let's say total_salary). Let's also assume that total_salary is very expensive to calculate and we only want to do that calculation whenever any employee's salary is changed and not whenever we access it as a property.
> c = Company()
> print(c.total_salary)
0
> c.employees.append(Employee())
> print(c.total_salary)
0
c.employees[0].salary = 100 # update total_salary for c now
> print(c.total_salary)
100
The obvious way is by making sure that there is a reference from each Employee back to its owning Company. What I'm curious is whether there is a good way to use a getter, setter or something else in Company to ensure that I can capture a salary change to any element in employees, so that I can immediately update total_salary. Crucially, we should avoid this recalculation when another member is updated (such as name).
class Company:
def __init__(self, ....):
self.employees = []
self.total_salary = 0
def add_employe(self, employe):
self.employees.append(employe)
self.total_salary += employe.salary
def remove_employe(self, employe):
self.employees.remove(employe)
self.total_salary -= employe.salary
def update_employe(self, employe):
for e in self.employees:
if not employe.id == e.id:
continue
e.name = employe.name
# ... update more data here
if employe.salary != e.salary: # salary suffered changes, update it
self.total_salary -= e.salary # subtract the older
self.total_salary += employe.salary # sum up the new
e.salary = employe.salary # update it
class Employee:
_id = itertools.count(start=1)
def __init__(self, ...):
self.id = next(Employee._id)
# ...
You want update total_salary only if the employe.salary suffered any changes. The responsible for this is in update_employe method.
Also an implementation of some sort of id to Employee is useful.
If you're looking for getter/setter control, look into Python property. You can use this pattern to catch when variables are set, in order to implement the auto-update feature.
I think there are a couple of reasonable ways to do what you're asking. One is to have the company dynamically compute its totals each time they're requested. The other approach is for the Employee to know the company it belongs to, and for it to update the company's totals whenever it's own information changes.
I think the first approach is easier, since the Employee instances doesn't need to know anything about the Company, and you don't to do anything special when an Employee is updated. The downside is that if you have a company with many employees and request its totals often, it may be slow, since it needs to iterate over all the employees each time. Here's how I'd implement it, using a property:
class Company:
def __init__(self):
self.employees = []
#property
def total_salary(self):
return sum(e.salary for e in self.employees)
If you go with the second approach, you'd also use a property, but you'd put it in the Employee class, so it can detect changes being made to the salary attribute:
class Company:
def __init__(self):
self.employees = []
self.total_salary = 0
class Employee:
def __init__(self, employer):
self.employer = employer
self._salary = 0
#property
def salary(self):
return self._salary
#salary.setter
def salary(self, value):
self.employer.total_salary += value - self._salary
self._salary = value
To make this work in a more complicated system, you'd probably want a bunch of other methods, like one that adds an Employee with a salary already set to a Company (currently you must add the Employee to the company first, then update it's salary, or the totals will be wrong).
Related
I'm writing a script to find the moving average of different stocks. This script runs continuously, looping through my API call to add the current price to a list before averaging it. This works fine, however I'd like to be able to put this into a function to where the only input I need to give it is the name of the stock. I'd like this script to work for as many stocks as I want to specify, at the same time. That's where I run into issues because for each stock I have I need to have an empty list predefined that can hold the pricing information.
I've been trying to use the name of the stock to then create a related list, but as I now understand it it's not a great idea using one variable name to create another variable, so I'm not sure what to do. I believe the usual solution here would be to use a dictionary, but I'm a beginner to programming in general so I haven't figured out how to fit that into my situation. Any help would be greatly appreciated.
def sma(stock_name):
list_exists = stock_name + "_list" in locals() or stock_name + "_list" in globals()
if list_exists:
print()
else:
stock_name + "_list" = [] # Problem line, I would like for this to create a list called stock_name_list
stock_price = requests.get("myapi.com", params={"stock_name": stock_name, "bla bla": "blah"})
stock_name_list.append(stock_price)
When you have an operation based on a version of the data specific to that operation, that is usually a good time to think about using classes. This particular proposed class will encapsulate the name of a stock, the list of data specific to it, and perform sma on it:
class Stock:
n = 10
def __init__(self, name):
self.name = name
self.data = []
def sma(self):
stock_price = requests.get("myapi.com", params={"stock_name": self.stock_name, "bla bla": "blah"})
self.data.append(stock_price)
window = self.data[-n:]
return sum(window) / len(window)
Now you can maintain a dictionary of these objects. Any time you encounter a new stock, you just add an item to the dictionary:
stocks = {}
def sma(name):
stock = stocks.get(name)
if name is None: # None is what get returns when the key is missing
stock = Stock(name)
stocks[name] = stock
return stock.sma()
The nice thing is that you now have a dictionary of named datasets. If you want to add a different statistic, just add a method to the Stock class that implements it.
I defined a global sma function here that calls the eponymous method on the object it finds in your dictionary. You can carry encapsulation to an exterme by making the method perform the action of the function if called statically with a name instead of an instance. For example:
class Stock:
n = 10
named_stocks = {} # This is a class variable that replaces the global stocks
def __init__(self, name):
self.name = name
self.data = []
def sma(self):
if isinstance(self, str):
self = Stock.get_stock(self)
stock_price = requests.get("myapi.com", params={"stock_name": self.stock_name, "bla bla": "blah"})
self.data.append(stock_price)
window = self.data[-n:]
return sum(window) / len(window)
#classmethod
def get_stock(cls, name):
stock = cls.named_stocks.get(name)
if stock is None:
stock = cls(name)
cls.named_stocks[name] = stock
return stock
Now that there is a check for isinstance(self, str), you can call the sma method in one of two ways. You can all it directly on an instance, which knows its own name:
aapl = Stock('AAPL')
aapl.sma()
OR
Stock.get_stock('AAPL').sma()
Alternatively, you can call it on the class, and pass in a name:
Stock.sma('AAPL')
use defaultdict
from collections import defaultdict
stock_name_to_stock_prices = defaultdict(list)
stock_name_to_stock_prices['STOCK_NAME'].append(123.45)
So I have a group of N persons each having their own unique id. Each person has a randomized opinion of each already existing person ranging from 0 to 100. Upon the addition of a new person, I'd like all existing persons to acquire a randomized opinion of this new person. Upon removal of an existing person, I'd like all remaining persons to remove their opinion of the removed person.
Here's what I have up to now:
import random
persons = {}
class Person():
def __init__(self, idn):
self.idn = idn
self.opinions = {}
for i in persons:
self.opinions[i] = random.randrange(100)
persons[idn] = self
for i in persons:
persons[i].update()
def update(self):
pass
for i in range(20):
person_i = Person(i)
Now clearly the problem here is that only the last created object has opinions of all other persons. I was tinkering with creating a Person.update() function, but I have no clue how to proceed.
I was thinking, perhaps there is already somewhere a framework created to deal with this type of situation? (I would eventually hope to make even more complicated interrelations). The idea is having an object that holds a relationship to every other object in its group, and vice-versa for each other object in the group.
Any help is appreciated, especially resources to learn. I am a beginner at python.
Here for your reference, it is not working for more Person groups, just one Person group. If you need more groups, you have to specified group key for each person. If you want to del person, should person.delete() first.
import random
class Person():
table = {}
def __init__(self):
self.key = self.get_key()
self.opinions = {}
for key in Person.table:
self.opinions[key] = random.randrange(100)
for person in Person.table.values():
person.opinions[self.key] = random.randrange(100)
Person.table[self.key] = self
def get_key(self):
i = 0
while i in Person.table:
i += 1
return i
def delete(self):
del Person.table[self.key]
for person in Person.table.values():
del person.opinions[self.key]
del self
persons = [Person() for i in range(20)]
I am trying to model population growth using individual agents, but have already run into trouble with the basic skeleton of the model.
I have a class to hold individuals
class Individual:
"""These are individuals"""
list = []
def __init__(self, name, sex, dob):
self.name = name
self.sex = sex
self.dob = dob
self.list.append(name)
def __str__(self):
return "name is {}, sex is {}, age is {}" .format(self.name, self.sex, curyear - self.dob)
and I instantiate new individuals through
def birth():
global person
person = Individual((next(name)), randsex(), curyear)
The basic growth loop is
for i in range(totalyears):
curyear = curyear + 1
if curyear - person.dob == 18:
birth()
else:
pass
The problem seems to be that
if curyear - person.dob == 18:
birth()
is only ageing and checking the last instance of Individual that's created.
print (Individual.list) shows that my final population = starting population + total years /18 and print (str(person)) too seems to confirm this.
I think this is because my birth() function basically names each new instance 'person', so whenever I use person.whatever it references the latest instance created. It seems to me that there are two possibilities.
A) Dynamically give each new instance a unique name, and use a list to reference each of these instances' attributes in the growth loop.
B) Add an age instance attribute and figure out how to change this for all members of the class.
I don't know if either is possible, or how to do them. I would appreciate any advice and examples!
For context, I'm working on an inventory system in an RPG, and I'm prototyping it with python code.
What I don't understand is how to make separate variables for each instance of an item without declaring them manually. For a short example:
class Player(object):
def __init__(self):
self.Items = {}
class Item(object):
def __init__(self):
self.Equipped = 0
class Leather_Pants(Item):
def __init__(self):
#What do i place here?
def Pick_Up(self, owner):
owner.Items[self.???] = self #What do i then put at "???"
def Equip(self):
self.Equipped = 1
PC = Player()
#Below this line is what i want to be able to do
Leather_Pants(NPC) #<-Create a unique instance in an NPC's inventory
Leather_Pants(Treasure_Chest5) #Spawn a unique instance of pants in a treasure chest
Leather_Pants1.Pick_Up(PC) #Place a specific instance of pants into player's inventory
PC.Items[Leather_Pants1].Equip() #Make the PC equip his new leather pants.
If I did something silly in the above code, please point it out.
What I want to do if the code doesn't make it clear is that I want to be able to dynamically create variables for all items as I spawn them, so no two items will share the same variable name which will serve as an identifier for me.
I don't mind if I have to use another class or function for it like "Create_Item(Leather_Pants(), Treasure_Chest3)"
What's the best way to go about this, or if you think I'm doing it all wrong, which way would be more right?
As a general rule, you don't want to create dynamic variables, and you want to keep data out of your variable names.
Instead of trying to create variables named pants0, pants1, etc., why not just create, say, a single list of all leather pants? Then you just do pants[0], pants[1], etc. And none of the other parts of your code have to know anything about how the pants are being stored. So all of your problems vanish.
And meanwhile, you probably don't want creating a Leather_Pants to automatically add itself to the global environment. Just assign it normally.
So:
pants = []
pants.append(Leather_Pants(NPC))
pants.append(Leather_Pants(chests[5]))
pants[1].pickup(PC)
The pants don't have to know that they're #1. Whenever you call a method on them, they've got a self argument that they can use. And the player's items don't need to map some arbitrary name to each item; just store the items directly in a list or set. Like this:
class Player(object):
def __init__(self):
self.Items = set()
class Item(object):
def __init__(self):
self.Equipped = 0
class Leather_Pants(Item):
def __init__(self):
pass # there is nothing to do here
def Pick_Up(self, owner):
self.owner.Items.add(self)
def Equip(self):
self.Equipped = 1
Abernat has tackled a few issues, but I thought I weigh in with a few more.
You appear to be using OOP, but are mixing a few issues with your objects. For example, my pants don't care if they are worn or not, I care though for a whole host of reasons. In python terms the Pants class shouldn't track whether it is equipped (only that it is equippable), the Player class should:
class CarryableItem:
isEquipable = False
class Pants(CarryableItem):
isEquipable = True
class Player:
def __init__(self):
self.pants = None # Its chilly in here
self.shirt = None # Better take a jumper
self.inventory = [] # Find some loot
def equip(self,item):
if is.isEquipable:
pass # Find the right slot and equip it,
# put the previously equipped item in inventory, etc...
Also, its very rare that an item will need to know who its owner is, or that its been grabbed, so verbs like that again should go onto the Player:
class Player:
maxCarry = 10
def take(Item):
if len(inventory) < maxCarry:
inventory.append(item)
Lastly, although we've tried to move most verbs on to actors which actually do things, sometimes this isn't always the case. For example, when instantiating a chest:
import random
class StorageItem:
pass
class Chest(StorageItem):
__init__(self):
self.storage = random.randint(5)
self.loot = self.spawnLoot()
def spawnLoot(self):
for i in range(self.storge):
# Lets make some loot
item = MakeAnItem # Scaled according to type level of dungeon, etc.
loot.append(item)
def remove(item):
self.loot[self.loot.index(item)]=None
Now the question about what to do when a Player wants to plunder a chest?
class Player:
def plunder(storage):
for item in storage.loot:
# do some Ui to ask if player wants it.
if item is not None and self.wantsItem(item) or \
(self.name="Jane" and self.wantsItem(item) and self.doesntWantToPay):
self.take(item)
storage.remove(item)
edit: Answering the comment:
If you are curious about calculating armor class, or the like, that again is a factor of the user, not the item. For example:
class Player:
#property
def clothing(self):
return [self.pants,self.top]
#property
def armorClass(self):
ac = self.defence
for item in self.clothing:
def = item.armorClass
if self.race="orc":
if item.material in ["leather","dragonhide"]:
def *= 1.5 # Orcs get a bonus for wearing gruesome dead things
elif item.material in ["wool","silk"]:
def *= 0.5 # Orcs hate the fineries of humans
ac += def
return ac
pants = Pants(material="leather")
grum = Player(race="orc")
grum.equip(pants)
print grum.armorClass
>>> 17 # For example?
Looking at the bottom of the post you can see i have three classes. The code here is pseudo code written on the fly and untested however it adequately shows my problem. If we need the actual classes I can update this question tomorrow when at work. So ignore syntax issues and code that only represents a thought rather than the actual "code" that would do what i describe there.
Question 1
If you look at the Item search class method you can see that when the user does a search i call search on the base class then based on that result return the correct class/object. This works but seems kludgy. Is there a better way to do this?
Question 2
If you look at the KitItem class you can see that I am overriding the list price. If the flag calc_list is set to true then I sum the list price of the components and return that as the list price for the kit. If its not marked as true I want to return the "base" list price. However as far as I know there is no way to access a parent attribute since in a normal setup it would be meaningless but with sqlalchemy and shared table inheritance it could be useful.
TIA
class Item(DeclarativeBase):
__tablename__ = 'items'
item_id = Column(Integer,primary_key=True,autoincrement=True)
sku = Column(Unicode(50),nullable=False,unique=True)
list_price = Column(Float)
cost_price = Column(Float)
item_type = Column(Unicode(1))
__mapper_args__ = {'polymorphic_on': item_type}
__
def __init__(self,sku,list_price,cost_price):
self.sku = sku
self.list_price = list_price
self.cost_price = cost_price
#classmethod
def search(cls):
"""
" search based on sku, description, long description
" return item as proper class
"""
item = DBSession.query(cls).filter(...) #do search stuff here
if item.item_type == 'K': #Better way to do this???
return DBSession.query(KitItem).get(item.item_id)
class KitItem(Item):
__mapper_args__ = {'polymorphic_identity': 'K'}
calc_list = Column(Boolean,nullable=False,default=False)
#property
def list_price(self):
if self.calc_list:
list_price = 0.0
for comp in self.components:
list_price += comp.component.list_price * comp.qty
return list_price
else:
#need help here
item = DBSession.query(Item).get(self.item_id)
return item.list_price
class KitComponent(DeclarativeBase):
__tablename__ = "kit_components"
kit_id = Column(Integer,ForeignKey('items.item_id'),primarykey=True)
component_id = Column(Integer,ForeignKey('items.item_id'),primarykey=True)
qty = Column(Integer,nullable=False, default=1)
kit = relation(KitItem,backref=backref("components"))
component = relation(Item)
Answer-1: in fact you do not need to do anything special here: given that you configured your inheritance hierarchy properly, your query will already return proper class for every row (Item or KitItem). This is the advantage of the ORM part. What you could do though is to configure the query to immediatelly load also the additional columns which do belong to children of Item (from your code this is only calc_list column), which you can do by specifying with_polymorphic('*'):
#classmethod
def search(cls):
item = DBSession.query(cls).with_polymorphic('*').filter(...) #do search stuff here
return item
Read more on this in Basic Control of Which Tables are Queried.
To see the difference, enabled SQL logging, and compare your tests scripts with and without with_polymorphic(...) - you will most probably require less SQL statements being executed.
Answer-2: I would not override one entry attributed with one which is purely computed. Instead I would just create another computed attribute (lets call it final_price), which would look like following for each of two classes:
class Item(Base):
...
#property
def total_price(self):
return self.list_price
class KitItem(Item):
...
#property
def total_price(self):
if self.calc_list:
_price = 0.0
for comp in self.components:
_price += comp.component.list_price * comp.qty
return _price
else:
# #note: again, you do not need to perform any query here at all, as *self* is that you need
return self.list_price
Also in this case, you might think of configuring the relationship KitItem.components to be eagerly loaded, so that the calculation of the total_price will not trigger additional SQL. But you have to decide yourself if this is beneficial for your use cases (again, analyse the SQLs generated in your scenario).