Group objects with an identical list attribute - python

I have two classes:
class A:
def __init__(self, name, li):
self.b_list = li
class B:
def __init__(self, i):
self.i = i
class A contains a list of objects of type class B.
Assuming I have a list of class A objects, how can I group the class A objects that have an identical b_list together?
For example:
a_list = []
li = [B(1), B(2), B(3)]
a_list.append(A(li))
li = [B(2), B(3)]
a_list.append(A(li))
li = [B(1), B(2), B(3)]
a_list.append(A(li))
After processing this should give us two lists, one list with the first and third A, and another list with only the second A. Or in a nutshell:
result = [
[ A([B(1),B(2),B(3)]), A([B(1),B(2),B(3)]) ],
[ A([B(2),B(3)] ]
]

For starters, I've removed the name parameter from class A, since the rest of your details omitted it.
To group your class A objects together, you're going to need to define exactly what is meant when two A objects are equal. I've created a __cmp__ method that will let us sort on A objects by comparing them.
Now, since your A objects are composed of B objects, you're going to need something to define what is meant by two B objects being equal. I've created a __eq__ method in class B that does that.
Next, I've sorted the A instances to make grouping them easier, and added a __str__ method to class A, and a __repr__ method to class B so you can verify that they are being grouped together correctly.
I haven't added any error checking anywhere, so this code is a little fragile.
class A:
def __init__(self, li):
self.b_list = li
def __cmp__(self, other):
return cmp([elem.i for elem in self.b_list],
[elem.i for elem in other.b_list])
def __str__(self):
return "A({})".format(self.b_list)
class B:
def __init__(self, i):
self.i = i
def __eq__(self, other):
return self.i == other.i
def __repr__(self):
return "B({})".format(self.i)
def main():
a_list = []
li = [B(1), B(2), B(3)]
a_list.append(A(li))
li = [B(2), B(3)]
a_list.append(A(li))
li = [B(1), B(2), B(3)]
a_list.append(A(li))
result = []
last_item = None
for item in sorted(a_list):
if last_item and item == last_item:
result[-1] = result[-1] + [item]
else:
result.append([item])
last_item = item
for row in result:
print [str(elem) for elem in row]
if __name__ == '__main__':
main()
Result:
['A([B(1), B(2), B(3)])', 'A([B(1), B(2), B(3)])']
['A([B(2), B(3)])']

You can sort a_list using the attribute b_list as a key.
a_list.sort(key=lambda a:a.b_list)
or:
from operator import itemgetter
a_list.sort(key=itemgetter("b_list"))

Related

How can I modify my __repr__ to respresent correctly?

My __repr__ method works fine using objects created in it's class, but with objects that were created with the help of importing a library and using methods from it, it only represented the memory address...
from roster import student_roster #I only got the list if students from here
import itertools as it
class ClassroomOrganizer:
def __init__(self):
self.sorted_names = self._sort_alphabetically(student_roster)
def __repr__(self):
return f'{self.get_combinations(2)}'
def __iter__(self):
self.c = 0
return self
def __next__(self):
if self.c < len(self.sorted_names):
x = self.sorted_names[self.c]
self.c += 1
return x
else:
raise StopIteration
def _sort_alphabetically(self,students):
names = []
for student_info in students:
name = student_info['name']
names.append(name)
return sorted(`your text`names)
def get_students_with_subject(self, subject):
selected_students = []
for student in student_roster:
if student['favorite_subject'] == subject:
selected_students.append((student['name'], subject))
return selected_students
def get_combinations(self, r):
return it.combinations(self.sorted_names, r)
a = ClassroomOrganizer()
# for i in a:
# print(i)
print(repr(a))
I tried displaying objects that don't rely on anther library, and they dispayed properly.
The issue I was facing was linked to me not understanding the nature of the object. itertools.combinations is an iterable, and in order to represent the values stored I needed to either:
unpack it inside a variable like:
def get_combinations(self, r):
*res, = it.combinations(self.sorted_names, r)
return res
Iter through it inside a loop and leave the original code intact like
for i in a.get_combinations(2):
print(i)
I prefer the second solution

How to add multiple initialised strings into and append() method

So I'm working on a simple code that works like a bag, this code currently can read and add singular initialised strings into an array that has been also initialised under all classes. Here's the code:
class Items:
# This class initializes the items name
def __init__(self, name):
self.name = name
class Bag:
# This class adds the initialised items and reads anything in the bag
def __init__(self, items, max_items):
# max_items is the maximum capacity of the Bag
self.items = items
self.max_items = max_items
def add_items(self):
if len(self.items) < self.max_items:
self.items.append(i.name) # <- Here is the problem at '(i.name)'
def read_items(self):
for item in self.items:
print(item)
i = Items("Item1")
i2 = Items("Item2")
b = Bag([], 10) # I'm just going to put example 10 strings for the max_items
b.add_items()
b.read_items()
The output of this code is "Item1".
I wanted the 'append()' method in 'add_items()' function to also add 'i2' initialisation and other initialisation that will be added in the future with any name that the user will put in automatically.
I think what you need is a list of items passed to your add_items function:
def add_items(self, items: list):
if (len(self.items) + len(items)) < self.max_items:
for i in items:
self.items.append(i.name)
You then later call it with
b.add_items([i, i2])
If you want to add multiple items at the same time without iterations, you can use extend() rather than append(), but you have to redefine add_items() accordingly:
def add_items(self, *args): # args will collect any number of items you pass in a tuple
if len(self.items) + len(args) <= self.max_items:
self.items.extend([i.name for i in args])
i = Items("Item1")
i2 = Items("Item2")
b = Bag([], 10)
b.add_items(i, i2)
b.read_items()
# output:
Item1
Item2
This would be a Pythonic way to do the job without unexpected behaviors.

python - getting specific generator data from the generator

let's start with the code
class MyClass:
def __init__(self):
self.elemplusone = None
self.elemplustwo = None
self.data = self.generate_data()
def generate_data(self):
for elem in range(10):
yield elem+1, elem+2
I need to get the first and the second element of generator. Right now, I'm calling it outside the class after creating an object:
a_generator = MyClass()
c = next(a_generator.data)
elemplusone = c[0]
elemplustwo = c[1]
but I need them to be specified (as separate generators) in the class and I can't create two generator methods.
Thanks
I also don't quite understand what you mean exactly. But does this help you?
class MyClass:
def __init__(self):
self.data = self.generate_data()
self.update_elements()
def update_elements(self):
self.elemplusone, self.elemplustwo = [x for x in next(self.data)]
def generate_data(self):
for elem in range(10):
print("Yielded")
yield elem + 1, elem + 2
a_generator = MyClass()
a_generator.elemplusone is 1 and a_generator.elemplustwo is 2.
Now you could call a_generator.update_elements() to yield your elements again and continue in your generator. Please let me know if this helps you. Good luck!

Does a list_iterator garbage collect its consumed values?

Suppose I have li = iter([1,2,3,4]).
Will the garbage collector drop the references to inaccessible element when I do next(li).
And what about deque, will elements in di = iter(deque([1,2,3,4])) be collectable once consumed.
If not, does a native data structure in Python implement such behaviour.
https://github.com/python/cpython/blob/bb86bf4c4eaa30b1f5192dab9f389ce0bb61114d/Objects/iterobject.c
A reference to the list is held until you iterate to the end of the sequence. You can see this in the iternext function.
The deque is here and has no special iterator.
https://github.com/python/cpython/blob/master/Modules/_collectionsmodule.c
You can create your own class and define __iter__ and __next__ to do what you want. Something like this
class CList(list):
def __init__(self, lst):
self.lst = lst
def __iter__(self):
return self
def __next__(self):
if len(self.lst) == 0:
raise StopIteration
item = self.lst[0]
del self.lst[0]
return item
def __len__(self):
return len(self.lst)
l = CList([1,2,3,4])
for item in l:
print( len(l) )

How to decrypted [<__main__.Food object at 0x1097ba828>

I am a python newbie. I want display actual names,values and calories instead of [<__main__.Food object at 0x1097ba828>, <__main__.Food object at 0x1097ba860>, <__main__.Food object at 0x1097ba898>] I know this question is very simple,but it would be a great help if you could let me know the answer!
class Food(object):
def __init__(self,n,v,w):
self.name = n
self.value = v
self.calories = w
def getValue(self):
return self.value
def getCal(self):
return self.calories
def density(self):
return self.getValue()/self.getCal()
def __str__(self):
return '<__main__.Food: '+self.name +' '+ self.value+' ' + self.calories
def buildMenu(self):
menu = []
for i in range(len(values)):
menu.append(Food(self.name[i], self.value[i], self.calories[i]))
return menu
names=['burger','fries','coke']
values=[1,2,3]
calories=[100,200,300]
if __name__ == '__main__':
new = Food(names, values, calories)
print(new.buildMenu())
Thank you!
I made two code changes to get what I think you're looking for. The first is to convert values to strings in your str function. The second is to use that.
def __str__(self):
return '<__main__.Food: '+ str(self.name) +' '+ str(self.value)+' ' + str(self.calories)
and
print (str(new)) #instead of print(new.buildMenu())
Now the output is:
<main.Food: ['burger', 'fries', 'coke'] [1, 2, 3] [100, 200, 300]
This is how I would do it, noting that we've created two classes: a separate Food and Menu class. The Menu class has an add method that appends to its foodItems property, though I don't feel like that's really necessary since we can just do direct property assignment:
m.foodItems = < some list of Food objects >
I've removed the confusing buildMenu method from the Food class, and defined __str__ methods for both classes:
class Food(object):
def __init__(self,n,v,w):
self.name = n
self.value = v
self.calories = w
def getValue(self):
return self.value
def getCal(self):
return self.calories
def density(self):
return self.getValue()/self.getCal()
def __str__(self):
return '\t'.join([self.name, str(self.value), str(self.calories)])
class Menu(object):
def __init__(self):
self.foodItems = []
def add(self, foodItem):
self.foodItems.append(foodItem)
def __str__(self):
"""
prints the food items
"""
s = 'Item\tValue\tCalories\n'
s += '\n'.join(str(f) for f in self.foodItems)
return s
names=['burger','fries','coke']
values=[1,2,3]
calories=[100,200,300]
m = Menu()
items = list(Food(n,v,c) for n,v,c in zip(names,values,calories))
m.foodItems = items
print(m)
And outputs like:
The issue you have is that you're printing a list of Food instances, not a single instance at a time. The list type's __str__ operator calls repr on the items the list contains, not str, so your __str__ method does not get run.
A simple fix is to just rename your __str__ method to __repr__.
I'd note that it's a bit strange that you're building a Food instance with lists of values for name, value and calories, just so that you can call a method on it to make a list of Food instances with the individual values. A more Pythoic approach would be to pass the lists to a classmethod that returns the list of instances, without the intermediate instance needing to exist:
#classmethod
def buildMenu(cls, names, values, calories):
menu = []
for i in range(len(values)): # consider using zip instead of looping over indexes
menu.append(cls(names[i], values[i], calories[i]))
return menu
You'd call it on the class:
if __name__ == '__main__':
print(Food.buildMenu(names, values, calories))

Categories

Resources