Referencing where a child object goes using deepcopy in Python - python

A question about deepcopy in Python:
I have a class A which contains a list of objects from class B, and a class C which contains a list of objects from class A.
class A(object):
def __init__(S):
self.S = set(S)
class B(object):
def __init__(buddies):
self.buddies = set(buddies)
class C(object):
def __init__(L):
self.L = set(L)
def SplitA(a,b):
left = deepcopy(a)
left.S -= new_b.buddies
right = A(new_b.buddies)
L |= left
L |= right
So I want C to have a Split function which, given an A object a and a B object b in a.S, will make two new A objects: one containing the 'buddies' of b (which are in a.S), one containing of the rest of a.S.
The problem is, I don't know how to figure out what the designated b becomes in the deepcopy. In other words,
how do I find new_b in the above code?
(Note: In my actual code it is important to do it in this order, i.e. adding new_a and then splitting a into left and right would not work.)

The answer is that the designated b does not become anything other than b in deep copy as you are not deep copying b at all. So just replace new_b with b in your example.

This code should do what you were looking for.
from copy import deepcopy
class A(object):
def __init__(self, S):
self.S = set(S)
class B(object):
def __init__(self, buddies):
self.buddies = set(buddies)
class C(object):
def __init__(self, L):
self.L = set(L)
def SplitA(self, a, b):
left = set()
left -= b.buddies # Since the objects in the sets are unchanged
# you can do a set difference that will leave you
# with only the members of a that are not in b
left = deepcopy(left) # Now get a deep copy of left
right = deepcopy(b.S) # and a deep copy of b.S
self.L |= left # and combine them
self.L |= right

Related

Custom object does not properly work as dictionary key even after overwriting __hash__() and __eq__()

NOTE: I am aware of this exact same question here and here. However, I have tried the solutions proposed by the answers there and they do not work for me (see sample code below).
A B object has a list of A. A is composed by a tuple of only two integers and an integer.
I am trying to use B objects as keys in a dictionary. However, even after implementing my own __eq__() and __hash__() methods, the length of my dictionary increases even after adding the same object to it.
See code below:
class A:
def __init__(self, my_tuple, my_integer):
self.my_tuple = my_tuple
self.my_integer = my_integer
def __eq__(self, other):
return self.my_tuple == other.my_tuple and self.my_integer == other.my_integer
class B:
def __init__(self):
self.list_of_A = []
def add(self, my_tuple, my_integer):
new_A = A(my_tuple, my_integer)
self.list_of_A.append(new_A)
def __hash__(self):
return hash(repr(self))
def __eq__(self, other):
for i in range(len(self.list_of_A)):
if self.list_of_A[i] != other.list_of_A[i]:
return False
return True
b_1 = B()
b_1.add((1,2), 3)
b_2 = B()
b_2.add((1,2), 3)
my_dict = {}
my_dict[b_1] = 'value'
print(len(my_dict))
my_dict[b_2] = 'value_2'
print(len(my_dict))
The output I am getting is
12
And the expected output is
11
Because I am adding the same object (i.e.:same properties values).
The hashes aren't equal because the repr()s aren't equal. Consider the following example I just did on my python console using your code:
>>> x = B()
>>> y = B()
>>> repr(x)
'<__main__.B object at 0x7f7b3a20c358>'
>>> repr(y)
'<__main__.B object at 0x7f7b3aa197b8>'
Obviously, x and y will have different hashes.
All you need to do, then, is overwrite __repr__() so that it outputs a deterministic value based on the contents of the object, rather than its memory address, and you should be good to go. In your case, that may look something like this:
class A:
...
def __repr__(self):
return f"A(my_tuple:{self.my_tuple}, my_integer:{self.my_integer})"
class B:
...
def __repr__(self):
return f"B(list_of_a:{self.list_of_a})"

Counting the occurence of instances of a certain class in list of lists

I have list of lists in which I want to count the number of B() and C() instances and am looking for a suitable method to do this. Using collections.Counter() and the .count() function have resulted in strange results, and I suspect I do not fully understand how list of lists work in python, or how lists of class instances work in python.
This is the list of lists:
lst = [[B() for w in range(x)] for h in range(y)]
with
class A():
def __init__(self, name):
self.name = name
class B(A):
def __init__(self, name = "B"):
A.__init__(self, name)
def update(self):
if random.random() < 0.05:
return C()
else: return self
class C(A):
def __init__(self, name = "C"):
A.__init__(self, name)
And, I use the below code to randomly change B() instances in lst into C() instances:
for row in range(y):
for column in range(x):
lst[row][column] = lst[row][column].update()
How do I count the number of B() and C() instances in the list?
You can use isinstance()
You can check what class an element is with isinstance().
Here is an example:
>>> a = C()
>>> isinstance(a, C)
True
So if you have your list, you can do:
occurrences_of_B = sum(isinstance(i, B) for r in list for i in r)
occurrences_of_C = sum(isinstance(i, C) for r in list for i in r)
you can get the occurrences of the B() and C() classes.
Essentially, we are using a generator comprehension to apply the isinstance() function to every element in the list. We then use sum on the generator as True evaluates to 1 and False to 0, so we will get the total count.
As a side note, although I said it is not good practice to name a list 'array', it is actually worse to name it exactly 'list' as this prevents you from being able to use the list() function! Better would probably be lst or l. :)

Python class and __iter__

What are the benefits of using the __iter__ function in a Python class?
In the code below I am just setting up two simple classes. The first class takes in a list as an argument, and I am able to loop over this list without using the __iter__ function. The second bit of code uses the __iter__ function to loop over a list.
What is the benefit of using __iter__ when there are already ways of looping over stuff in a class?
EG 1: no __iter__
class test_class:
def __init__(self, list):
self.container_list = list
def print (self):
a = self.container_list
return a
test_list = test_class([1,2,3,4,5,6,7])
x = test_class.print(test_list)
for i in x:
print (i)
EG 2: yes __iter__
class list_using_iter:
def __init__(self):
self.list = [1,2,3,4]
self.index = -1
def __iter__(self):
return self
def __next__(self):
self.index += 1
if self.index == len(self.list):
raise StopIteration
return self.list [self.index]
r = list_using_iter()
itr = iter(r)
print(next(itr))
print(next(itr))
print(next(itr))
print(next(itr))
print(next(itr)) # Raises the exception!
Your first example is not iterable, but contains an attribute that is. Your second example is iterable, but you iterate simply by "following" another iterable. Here's an example of a iterable that does more work itself:
import itertools
class Fibs:
def __init__(self, a, b):
self.a = a
self.b = b
def __iter__(self):
a = self.a
b = self.b
while True:
yield a
a, b = b, a + b
real_fibs = Fibs(0,1)
for i in itertools.islice(real_fibs, 10):
print(i)
Fibs.__iter__ isn't simply regurgitating values obtained from some other value's __iter__ method; it is computing and yielding new values on demand.
Actually, the preceding is an example of a class that knows how to create its own iterator, rather than having each object be iterable. Here's a version that defines next itself.
class Fibs:
def __init__(self, a, b):
self.a = a
self.b = b
def __iter__(self):
return self
def __next__(self):
rv = self.a
self.a, self.b = self.b, self.a + self.b
return rv
In both cases, the looping works because of __iter__. In your first example, your print function returns a loop.
The implementation of the for keyword will call __iter__ (or the corresponding slot within the C implementation since the code involved is in the C interpreter) in order to loop over the list.
In your second example you could have written
for elt in r:
print(elt)
which would have internally called __iter__ to implement the for loop.
In general you tend to use for rather than iter and next directly. The cases where you use iter and next directly are when you're producing a callback function that will produce an iterator or when you're defining one iterator in terms of another.
In terms of when should you write your own __iter__ or return some object that does its own iteration, that all depends on what functionality you want. For example, your first class is more powerful because two people can be iterating the list at the same time. In your second class, because you store the index in the class itself, only one person can successfully use the iterator at a time.
However, if you had complex enough behavior, the second approach where you define your own __iter__ might make sense.

Adding up instance variables of a class object

I want to sum up an instance variable of a class object in a list of that class object.
Class A(object):
def __init__(self):
self.a = 20
B = []
for i in range(10):
B.append(A())
# Can this be made more pythonic?
sum = 0
for i in B:
sum += i.a
I was thinking along the lines of using a map function or something? Don't know if that's pushing it too much though. Just curious.
class A(object):
def __init__(self):
self.a = 20
B = []
for i in range(10):
B.append(A())
s = sum(i.a for i in B)
print s
works.
You can use reduce
reduce(lambda acc, c: acc + c, [i.a for i in B])
or sum() with comprehension
sum([i.a for i in B])

concatenate numpy arrays that are class instance attributes in python

I am attempting to use a class that strings together several instances of another class as a numpy array of objects. I want to be able to concatenate attributes of the instances that are contained in the numpy array. I figured out a sloppy way to do it with a bunch of for loops, but I think there must be a more elegant, pythonic way of doing this. The following code does what I want, but I want to know if there is a cleaner way to do it:
import numpy as np
class MyClass(object):
def __init__(self):
self.a = 37.
self.arr = np.arange(5)
class MyClasses(object):
def __init__(self):
self.N = 5
# number of MyClass instances to become attributes of this
# class
def make_subclas_arrays(self):
self.my_class_inst = np.empty(shape=self.N, dtype="object")
for i in range(self.N):
self.my_class_inst[i] = MyClass()
def concatenate_attributes(self):
self.a = np.zeros(self.N)
self.arr = np.zeros(self.N * self.my_class_inst[0].arr.size)
for i in range(self.N):
self.a[i] = self.my_class_inst[i].a
slice_start = i * self.my_class_inst[i].arr.size
slice_end = (i + 1.) * self.my_class_inst[i].arr.size
self.arr[slice_start:slice_end] = (
self.my_class_inst[i].arr )
my_inst = MyClasses()
my_inst.make_subclas_arrays()
my_inst.concatenate_attributes()
Edit: Based on the response from HYRY, here is what the methods look like now:
def make_subclass_arrays(self):
self.my_class_inst = np.array([MyClass() for i in range(self.N)])
def concatenate_attributes(self):
self.a = np.hstack([i.a for i in self.my_class_inst])
self.arr = np.hstack([i.arr for i in self.my_class_inst])
you can use numpy.hstack() to concatenate arrays:
def concatenate_attributes(self):
self.a = np.hstack([o.a for o in self.my_class_inst])
self.arr = np.hstack([o.arr for o in self.my_class_inst])
See Also
vstack : Stack arrays in sequence vertically (row wise).
dstack : Stack arrays in sequence depth wise (along third axis).
concatenate : Join a sequence of arrays together.
For the latter function I would recommend this:
init = []
ContainerClass.arr = np.array([init + Array(myclass.arr) for myclass in self.my_class_inst])
typecast numpy array to normal array, catenate and typecast it back. Assuming now that you have simple 1D arrays. I don't remember by heart if numpy array has catenation function. You can use that instead of '+' sign and save the trouble of typecasting.
For the first you have the simplest form I can think of, although I usually use normal arrays instead of numpy ones for objects.
If you want to be really clever you can create an __add__ function for both of the classes. Then you can use '+' sign to add classes. a + b calls a.__add__(b). Now you would have to create functions that have following properties
MyClass + MyClass returns new MyClasses instance with a and b inside
MyClasses + MyClass adds MyClass to MyClasses in a way you want
Now if a,b,c,d are myClass instances, a+b+c+d should return MyClasses -class which contains MyClass instances a,b,c and d and their combined arrays. This would be the pythonic way, although its a bit too complicated in my taste.
edit:
Ok, sorry my bad. I did not have python when I wrote the code. This is the correct version:
init = []
my_inst.arr = np.array([init + list(myclass.arr.flat) for myclass in my_inst.my_class_inst]).flatten()
This is what I meant with the __add__ (and the pythonic way... regadless of its complicatedness):
import numpy as np
class MyClass(object):
def __init__(self):
self.a = 37.
self.arr = np.arange(5)
def __add__(self, classToAdd):
a = MyClasses() + self + classToAdd
return a
class MyClasses(object):
def __init__(self):
self.N = 0
self.my_class_inst = np.array([])
self.a = np.array([])
self.arr = np.array([])
def __add__(self, singleClass):
self.my_class_inst = np.hstack([self.my_class_inst, singleClass])
self.a = np.hstack([self.a, singleClass.a])
self.arr = np.hstack([self.arr, singleClass.arr])
self.N = self.my_class_inst.shape[0]
return self
#add_test = MyClass() + MyClass()
add_test = np.sum([MyClass() for i in range(5)])
print add_test.a, add_test.arr, add_test.N
print add_test.__class__, add_test.my_class_inst[0].__class__

Categories