Changing attributes based on user input - python

Okay, I have a class which has 10 objects, these have the attributes self.planet, self.distance, self.distsquared, self.radius, self.diamater where distance/distsquared/radius/diamater are all integers. And I would like to make a function where the user searches for a planet name, and then changes one of the attributes.
For example, the user should input the name "Jupiter", which would then find the object, and the next row of the function would ask the user to add a certain sum to the attribute self.distance.
Currently the first class is set up as following:
class Planets():
def __init__(self, planetName, dist, radius, diameter):
self.planetName= planetName
self.dist= dist
self.radius= radius
self.diameter= diameter
This is then retrieved through a planetObjects=[Planets(*p) for p in planetList] This is the object-list I would like to turn into a dictionary so the user can search for planetName, and alter the distance
Some users suggested I use a dictionary for this, but I have no idea how to go about doing that. Currently my class turns a list of lists into a list of objects, these objects have these attributes that the user is supposed to be able to change by searching for the Planet name, and then changing one of its attributes.
The class is currently just a simple class which has a constructor and a __str__ function
Meaning, function starts, asks the user something like "Which planet would you like to alter?", the user inputs "Jupiter" and the program asks, "How has the distance to Jupiter changed?" where the user adds for example 450 or so.
The current code I have is a function which opens an infile and turns it into a list of lists. This list is then turned into objects. I turned it into objects to easily be able to sort it and add new values based on previous values. But at this point the user also has to be able to alter values by searching for a planet name and then changing one of the attributes - this is where I am lost and need help!
Is there any way to do this? Thanks in advance!

In psuedocode:
class Planet(object):
# Define your planet class here
# any attributes that you do NOT need the user to be able to edit should start with _
Planets = [Planet('Mercury'.....
#or better
PlanetDict = {'Mercury':Planet(....
which = PromptUserForPlanet()
p = PlanetDict.get(which) # or find and return it if you didn't use a dictionary
for att in dir(p):
if not att.startswith('_'):
input = raw_input('%s: (%s)' % (attr, repr(getattr(p,attr)))
if len(input) > 0:
setattr(p,att,input) # You may wish to do some type conversion first
Since p is a reference to the dictionary entry you will change the main object.

Given your class Planets, this may be solved like this. I'm assuming that planetList is structured like in this code. If it is not, you may have to alter the code a bit.
def increment_dist(planets):
name = raw_input('Please enter planet name')
try:
planets[name].dist += int(raw_input('Increment distance by (integer)'))
except KeyError:
print('No planet called {}'.format(name))
except ValueError:
print('That is not an integer')
planetList = [('Tellus', 1, 2, 4), ('Mars', 1, 3, 9)]
planet_dict = {name: Planets(name, dist, radius, diameter) for
name, dist, radius, diameter in planetList}
increment_dist(planet_dict)

Related

Pythonic way to test single-element lists to avoid IndexErrors

Throughout my codebase there is a design pattern in which we have two lists of objects and try to whittle it down to one object.
Say we have two classes,
class Employee():
def __init__(self, _id, name):
self._id = _id
self.name = name
class Shift():
def __init__(self, employee_id, shift_id):
self.employee_id = employee_id
self.shift_id = shift_id
We have lists of objects of these classes. I want to find the employee who has a shift with their id attached to it. Suppose I have the list employees containing Employee objects, and the list shifts containing Shift objects, then I do:
for shift in shifts:
# Find employee who is assigned to this shift
employee = [e for e in employees if e._id == shift.employee_id]
So far so good. There's no guarantee that the employees_with_shift will contain an employee though. If there is an employee, there's only one. Currently it's being handled like this:
if employee:
employee = employee[0]
... do something
But I don't think this is Pythonic. A simple solution would be to:
for e in employee:
...do something
I don't know if this is unpythonic, but it does handle the case smoothly. Is it wrong to use a for-loop on lists that have either zero or one elements?
The other one is to go by AFNP and do this:
try:
employee = employee[0]
... do something
except IndexError:
pass
I don't like this though because there is quite a lot of coding to do on the employee, and the error handling would get extremely complicated.
Which of my solutions (if any) is the most pythonic?
EDIT:
This question is not answered by the one in the close suggestion, because this question looks for the most pythonic way to handle the element of a list that contains either 0 or 1 elements.
Instead of first creating a list that will contain either 0 or 1 items and then unpacking the list again, I would use next to find the first employee matching the condition. If there is none, this will raise StopIteration, because the iteration of the generator expression passed into next is exhausted:
# Find employee who is assigned to this shift
try:
employee = next(e for e in employees if e._id == shift.employee_id)
except StopIteration:
# no such employee
However, why don't you just have a dictionary mapping employees by their ID?
Then you could simply write:
try:
employee = employees[shift.employee_id]
except KeyError:
# no such employee
And then you should ask yourself how it could happen that a shift was assigned an employee that doesn't exist. Maybe it's because no employee was assigned to the shift and shift.employee_id is None? Then LBYL would in fact be clearer IMO:
if shift.employee_id is None:
# no employee was assigned yet
return # or continue, break, ...
# now this must succeed, if not there is a bug
assert shift.employee_id in employees
employee = employees[shift.employee_id]
If you are searching for a single object in a list, and you expect it to either be in the list or not (and not multiple possible values), then don't create a list in the first place. The Pythonic thing to do would be simply:
for employee in employees:
if e._id == shift.employee_id:
# handle employee
break
else:
# handle the case where no employee is found, else clause not necessery
# if you simply want to pass
Probably the better design overall is to have a dictionary mapping employee id's to employees so you can handle it like this:
try:
employee = employee_dict[shift.employee_id]
except KeyError:
# handle not found case
Borrowing from #mkrieger1's answer, next() takes a default return value, so you can just do
employee = next(e for e in employees if e._id == shift.employee_id,None)
This will default employee to None for example.
A dictionary mapping would indeed also be great:
You could avoid the try..except by using .get():
employee = employees.get(shift.employee_id,None)
You don't say what you want employee to default to if it's absent in the shift list though.
I suspect you're already at the most pythonic point, barring an architecture restructuring, but you could always turn it into one line:
employee = employee[0] if bool(employee) else None
# or this is perhaps more pythonic
employee = employee[0] if employee else None
# I dislike using implicit booleans because of type ambiguity.
It's not much different to what you're already doing, but it would look better, and that might be enough here!
EDIT: I agree with the other answers about using a dictionary. My answer applies if you really can't do that

How to compare user input to class object data?

total beginner here.
I'm trying to understand how to compare user input data to preset class data. Let's say class = Person, with the age, weight, and name.
And I've got two people, johnsmith = Person(50, 200, "John Smith")
tomjones = Person(40, 220, "Tom Jones")
I want to prompt the user to input the name Tom, and have check all "name" attributes for the Person class. So something like,
person = input(print("Insert the person's name: ")) entering "Tom" and then returning a list of all Toms for the user to select, or if there's only one, confirm that the user in fact did mean Tom Jones.
Should I create an array like people = [tomjones, johnsmith, (etc)], and somehow search inside that?
Please point me in the right direction on this, thanks.
Should I create an array-like people = [tomjones, johnsmith, (etc)], and somehow search inside that?
Yes.
Although ihough if you expect a very large number of "people" and frequent queries always on the same field (e.g. you're always searching for a specific person by its name) you may also want to create an index to speed up searches e.g. create a dict (possibly a WeakValueDictionary) which maps whatever your search key is to the proper person.
This is because iterating a list is cheap and efficient but going through the entire list is linear time (O(n)) so if your list is very long and you go through it a lot, it adds up. Building an index is expensive (and requires going through the entire list at least once) and there is more setup to the lookup but setup aside the lookup is constant time (O(1)).
Storing the instances of your Person class in a list would be a way of doing this. You can then loop through the list to match the names.
e.g.
class Person():
def __init__(self, age, weight, name):
self.age = age
self.weight = weight
self.name = name
all_persons = []
all_persons.append(Person(50, 200, "John Smith")) # add your Person instance to a list
all_persons.append(Person(40, 220, "Tom Jones"))
name_input = input("Insert the person's name: ")
for person in all_persons: # loop through list of all Person instances
if name_input.lower() in person.name.lower(): # lower() to handle case
# do something with person

How can i change an objects IfcClassification with python/ifcopenshell, f.ex IfcBuildingElementProxy to IfcWindow?

I have a model with many elements that is classified as ifcbuildingelementproxy (or unclassified as that is the standard of the ifc exporting software aka ifcObject).
I have a code that finds all the elements i want to change the classification of, but i cant seem to find a way to change it.
What i want to do is to get my script to reclassify all elements whos name begins with "whatever" to IfcWindow instead of IfcBuildingElementProxy.
def re_classify():
ifc_loc='thefile.ifc'
ifcfile = ifcopenshell.open(ifc_loc)
create_guid = lambda: ifcopenshell.guid.compress(uuid.uuid1().hex)
owner_history = ifcfile.by_type("IfcOwnerHistory")[0]
element = ifcfile.by_type("IfcElement")
sets = ifcfile.by_type("IfcPropertySet")
search_word_in_name='M_Muntin'
for e in element:
if e.is_a('IfcBuildingElementProxy'):
if e.Name.startswith(search_word_in_name,0,len(search_word_in_name)):
e.is_a()==e.is_a('IfcWindow') #<--- THIS DOES NOTHING
print (e)
print(e.Name,' - ', e.is_a())
re_classify()
I expect that f.ex
# 13505=IfcBuildingElementProxy('3OAbz$kW1DyuZY2KLwUwkk',#41,'M_Muntin Pattern_2x2:M_Muntin Pattern_2x2:346152',$,'M_Muntin Pattern_2x2',#13504,#13499,'346152',$)
will show
# 13505=IfcWindow('3OAbz$kW1DyuZY2KLwUwkk',#41,'M_Muntin Pattern_2x2:M_Muntin Pattern_2x2:346152',$,'M_Muntin Pattern_2x2',#13504,#13499,'346152',$)
On a Unix/Linux shell, you can make the substitution using a one-liner (without Python or IfcOpenShell), as follows:
sed -i '/IFCBUILDINGELEMENTPROXY(.*,.*,.M_Muntin/{s/IFCBUILDINGELEMENTPROXY/IFCWINDOW/}' thefile.ifc
Note that it makes the modifications directly in the initial file.
(N.B.: Tested on Cygwin)
You can not simply change the type. It is not possible to assign a value to a function and is_a is a function and not an attribute (by design for the reason that the type is unmodifiable).
Further IfcBuildingElementProxy and IfcWindow share a subset of their attributes only. That is IfcBuildingElementProxy has one that IfcWindow does not have and vice versa. Fortunately, the additional attributes of IfcWindow are all optional. So you could create a new window entity, copy the common attributes from the proxy, leave the additional attributes unset and remove the proxy.
commonAttrs = list(e.get_Info().values())[2:-1]
window = ifcfile.createIfcWindow(*commonAttrs)
ifcfile.remove(e)
You still would have to look for other entities referencing the proxy and replace the references with references to the window to obtain a valid ifc file.
Thanks to the other answers and some more research, I've put up the following code to accomplish this task.
If the number of attributes that cannot be set in the new type instance is more than 5, the update is not performed.
It seems to work well for now, not entirely sure if it's enough to check the RelatedElements and RelatedObjects or if there are more attributes that needs to be checked.
def change_element_type(
element: entity_instance,
new_type: str,
model: ifcopenshell.file,
) -> None:
"""
Change the element type and pass all the compatible properties.
GlobalId is kept, Element ID is not.
If the new type misses more than 5 of the old attributes,
the change is avoided.
Args:
element: original ifc element
new_type: type to change the element to
model: IFC model containing the element
"""
if new_type == element.is_a():
return
new_element = model.create_entity(new_type)
old_attrs = element.get_info(include_identifier=False)
del old_attrs["type"]
new_attrs = new_element.get_info(include_identifier=False)
missing_attrs = set(old_attrs) - set(new_attrs)
if len(missing_attrs) > 5:
warnings.warn(
f"New type {new_type} for element {element.id} "
f"misses too many attributes:\n {', '.join(missing_attrs)}.\n"
"Type change is cancelled."
)
model.remove(new_element)
return
for name in new_attrs:
if name in old_attrs:
setattr(new_element, name, old_attrs[name])
update_references(element, model, new_element)
model.remove(element)
def update_references(element, model, new_element):
for to_update in model.get_inverse(element):
for attr in ("RelatedElements", "RelatedObjects"):
try:
rel_objs = list(getattr(to_update, attr))
except AttributeError:
continue
try:
rel_objs.remove(element)
except ValueError:
continue
rel_objs.append(new_element)
setattr(to_update, attr, rel_objs)

How do you get python to find specific text in a list and then print it

This is what I have so far:
def lists():
global ClientList, highList, moderateList
ClientList = [ ["NeQua,High"],
["ImKol,Moderate"],
["YoTri,Moderate"],
["RoDen,High"],
["NaThe,Moderate"],
["ReWes,Moderate"],
["BrFre,High"],
["KaDat,High"],
["ViRil,High"],
["TrGeo,High"]]
highList = ["Running", "Swimming", "Aerobics", "Football", "Tennis"]
moderateList = ["Walking", "Hicking", "Cleaning", "Skateboarding", "Basketball"]
checkclient()
def checkclient():
global ClientList, highList, moderateList
answer = input("please enter the client ID: ")
answer2 = next(answer for answer in ClientList)
print(answer)
So I want to input the specific clientID, I want python to then find the client ID in the list, print the clientID with the intensity level (high or moderate) so I can use it later to ask the user how many minutes they spent exercising in the different activities based on whether their intensity was high or moderate.
At the moment the code only prints the first part of the list regardless of what the variable answer is: ["NeQua, High"].
Please can you tell me how to fix this and try to keep it simple as I am relatively new to Python.
Thanks
Cameron
Use a dictionary instead (and there's no need to wrap it in a function that does nothing but create global objects).
ClientList = {"NeQua":"High",
"ImKol":"Moderate",
"YoTri":"Moderate",
"RoDen":"High",
"NaThe":"Moderate",
"ReWes":"Moderate",
"BrFre":"High",
"KaDat":"High",
"ViRil":"High",
"TrGeo":"High"}
You don't need to specify mutable objects like lists or dictionaries as global if all you want to do is mutate them. You only need global if you want local assignments to the same name to also assign to the global name. More importantly, though, next() just returns the next element in an iterable. As a list is an ordered sequence, a generator that you make out of it with answer for answer in ClientList will have the same order, and the next() of that (redundant, I might add) generator will always be the first element of ClientList, because you keep making a new generator. If you want next() to proceed through the whole thing, you'd have to save it first. However, none of that is necessary here. Just access the dictionary. I use get() here to avoid errors if the user tries to access a user that doesn't exist.
def checkclient():
answer = input("please enter the client ID: ")
print(ClientList.get(answer, 'Not found.'))
checkclient()
Also note that a function must be defined before it is called (order matters).
You might change it as follows:
def checkclient():
global ClientList, highList, moderateList
answer = input("please enter the client ID: ") # input: NaThe
try:
answer2 = next(a for a in ClientList if answer in a)
except StopIteration:
checkclient()
else:
print(answer2) # NaThe,Moderate
next returns the first element of an iterable so you always get the first element of ClientList, so you need to filter out the ones containing your ID.

Choose a Class object from a list thereof in a less clumsy way

I have a self defined class, say, Man, (class names etc arbitrary throughout) and a list of these objects men. My goal is to search this list for any object whose age attribute is a certain number, and perform an operation on this object. If no such entry exists, I'd like to make one.
Now, I know I can do this:
for year in range(70):
year_found = False
if year in [m.age for m in men]:
# do something
year_found = True
if not year_found:
men.append(Man(age=year))
but use of year_found to keep a place in the array seems clunky. Does anyone know of a better data structure than a list of classes, or a more pythonic way to approach this?
You can try using next():
man_search = next((man for man in men if man.age == year), None)
if man_search is None:
men.append(Man(age=year))
else:
# do something
You probably want to use sets for this.
ages = set(m.age for m in men)
all_ages = set(range(70))
for age in (all_ages - ages):
men.append(Man(age=age))

Categories