1

I want to create a class in Python that implements a __add__ object method that allows summing two objects of the same class. Let's call this class Indicator and the two objects to sum ind1 and ind2. The Indicator object has only one property elements , which is a dictionary of integer values.

My implementation of __add__ combines the elements properties of two objects, eventually summing those values with the same key.

from __future__ import annotations
import copy
class Indicator:
    def __init__(self, elements={}):
        self.elements = elements

    def __add__(self, other: Indicator):
        new = copy.deepcopy(self)
        new.values =  {k: self.elements.get(k, 0) + other.elements.get(k, 0) for k in set(self.elements) | set(other.elements)}
        return new

ind1 = Indicator({1:1,2:2,3:3})
ind2 = Indicator({1:1,2:2})

new = ind1 + ind2
print('ind1: ',ind1.elements)
print('ind2: ',ind2.elements)
print(new.elements) # {1: 2, 2: 4, 3: 3}

I would like __add__ to return an object whose elements property gets updated as one or both objects in the summation get updated along the code flow.

For example,

ind1.elements[4] = 4
print(new.elements) # I would like this to be {1: 2, 2: 4, 3: 3, 4:4}

ind1.elements[1] = 3
print(new.elements) # I would like this to be {1: 4, 2: 4, 3: 3, 4:4}

How can I do it?

EDIT

First of all, let me thank you all the users who posted a comment/answer. Following the suggestions given in the comments and answers, I came up with the following solution. The idea is to add two lists as properties of Indicator: self.adds and self.linked.

  • The list self.adds collects the addends of the summation. It gets filled up when __add__ is called. So, in the example below, ind1.adds is [] and ind2.adds is [] since both objects don't arise from a sum. On the contrary, new.adds is [ind1,ind2]

  • The list self.linked collects all those object that needs to be updated whenever self gets updated. In the example below, ind1.linked is [new] and ind2.linked is [new].

I am not completely satisfied with this solution. For example, it fails to work if we sum up three objects and then modify one of them. I can try to fix the code, but I am wondering if I am doing something unconventional. Any thoughts? The code is the following

from __future__ import annotations
import copy


class Indicator:
    def __init__(self, elements=None):
        if elements is None:
            self._elements = {}
        else:
            self._elements = elements
        self.adds = []
        self.linked = []

    @property
    def elements(self):
        return self._elements

    @elements.setter
    def elements(self, value):
        self._elements = value
        for i in range(len(self.linked)):
            el = self.linked[i]
            el.update()

    def update(self):
        summation = self.adds[0]
        for obj in self.adds[1:]:
            summation = summation.__add__(obj)
        self._elements = summation.elements

    def __add__(self, other: Indicator):
        new = copy.deepcopy(self)
        self.linked.append(new)
        other.linked.append(new)
        new.adds = [self, other]
        new._elements = {k: self.elements.get(k, 0) + other.elements.get(k, 0) for k in
                         set(self.elements) | set(other.elements)}
        return new


ind1 = Indicator({1: 1, 2: 2, 3: 3})
ind2 = Indicator({1: 1, 2: 2})

new = ind1 + ind2
print('ind1: ', ind1.elements)
print('ind2: ', ind2.elements)
print(new.elements)  # {1: 2, 2: 4, 3: 3}
ind1.elements = {0: 0, 1: 3}
print('Updating ind1: ',new.elements == (ind1+ind2).elements)

ind2.elements = {0: 0, 7: 9}
print('Updating ind2: ',new.elements == (ind1+ind2).elements)
apt45
  • 412
  • 1
  • 6
  • 14
  • 2
    Not related to your problem, but don't use a dictionary literal as a default value. See https://stackoverflow.com/questions/1132941/least-astonishment-and-the-mutable-default-argument – Barmar Jan 18 '23 at 22:02
  • To implement what you want, all the instances that are used in the summation will need a reference to the instance that contains the sum. Then whenever you change the value of one of those instances, it calls a method of the sum instance to update its total. – Barmar Jan 18 '23 at 22:04
  • There's no built-in way to make this happen AFAIK. – Barmar Jan 18 '23 at 22:04
  • You have to create your own class derived from "dict" which sends notifications if their values are changed. – Michael Butscher Jan 18 '23 at 22:06
  • @MichaelButscher can you provide a simple implementation of what you are thinking? – apt45 Jan 18 '23 at 22:07
  • Does this returned object also need to be an `Indicator` or can it be some other class? – tdelaney Jan 18 '23 at 22:14
  • @tdelaney the returned object has to be an `Indicator` – apt45 Jan 18 '23 at 22:19
  • 1
    This is not a simple task, asking someone to write it for you seems inappropriate. I explained the general approach, now it's your job as the programmer to implement it. – Barmar Jan 18 '23 at 22:23
  • @Barmar I thought it was a simple task, my bad. I would never ask someone to write a code for me. I will think about a solution based on these comments. Thanks! – apt45 Jan 18 '23 at 22:25
  • @Barmar I updated the post with a partial solution. It works great with two objects but it fails if I sum up three objects. – apt45 Jan 19 '23 at 05:14

3 Answers3

2

A very rough idea how this can be done (with a lot of work left to you):

class dict_obs(dict):
    def __init__(self, observer, *args, **kwargs):
        self._observer = observer
        super().__init__(*args, **kwargs)

    def __setitem__(self, key, value):
        self._observer.notify(key)
        super().__setitem__(key, value)


class Indicator:
    def notify(self, key):
        print(f"{key} changed")



observer = Indicator()

d = dict_obs(observer, {1:1,2:2,3:3})

d[2] = 5

Prints

2 changed
Michael Butscher
  • 10,028
  • 4
  • 24
  • 25
  • Thank you! I'll try to get inspiration from this answer – apt45 Jan 19 '23 at 03:04
  • I haven't thought about implementing your answer but I came up with a partial solution (see edited post). It works great with two objects but it fails if I sum up three objects. – apt45 Jan 19 '23 at 05:15
1

I combined @MichaelButscher's suggestion and your update into one bit of code. I added objects' names and kept the print in notify for demonstration purpose; note that the diff argument in notify means that it only works for numerical values (but then, since you implement an __add__ method, that shouldn't be a problem!).

class Indicator(dict):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.derived=[]
        self.name = kwargs['name']

    def __setitem__(self, key, value):
        self.notify(key, value - self.get(key,0))
        super().__setitem__(key, value)
        
    def __add__(self, other: Indicator):
        new = Indicator({k:self.get(k,0)+other.get(k,0) for k in self.keys()|other.keys()},
                        name=self.name + '+' + other.name)
        self.derived.append(new)
        other.derived.append(new)
        return new
        
    def notify(self, key, diff):
        print(f'Indicator {self.name} : key {key} changed/added')
        for ind in self.derived:
            ind[key] = diff + ind.get(key,0)

    

Examples

a = Indicator({1:1,2:2}, name = 'a')
b=Indicator({0:3,1:7}, name = 'b')
x=a+b
print(x)
# {0: 3, 1: 8, 2: 2, 'name': 'a+b'}

a[1]=10
# Indicator a : key 1 changed/added
# Indicator a+b : key 1 changed/added
print(x)
# {0: 3, 1: 17, 2: 2, 'name': 'a+b'}

y = x + b
a[10]=77
# Indicator a : key 10 changed/added
# Indicator a+b : key 10 changed/added
# Indicator a+b+b : key 10 changed/added
print(x)
# {0: 3, 1: 17, 2: 2, 'name': 'a+b', 10: 77}
print(y)
# {0: 6, 1: 24, 2: 2, 'name': 'a+b+b', 10: 77}
Swifty
  • 2,630
  • 2
  • 3
  • 21
-1

Your issue is with this line:

new.values =  {k: self.elements.get(k, 0) + other.elements.get(k, 0) for k in set(self.elements) | set(other.elements)}

You are creating sets of k:v pairs when you really only want the keys. Here is a working solution:

class Indicator:
    def __init__(self, elements={}):
        self.elements = elements
    
    def __add__(self, other):
        keys = set(self.elements.keys()).union(set(other.elements.keys()))
        new = {}
        for k in keys:
            new[k] = self.elements.get(k,0) + other.elements.get(k,0)
        return Indicator(new)
Ted B
  • 104
  • 1
  • 1
  • 6