Alberto Patiño S
Alberto Patiño S

Reputation: 95

How to track the evolution of intermediate values in calls to object methods without many alterations of class definition in Python

I have this class in which a variable c is used as a intermediate step of the operate() method.

class DumbAdder():
    def __init__(self, a : float, b : float):
        super(DumbAdder, self).__init__()
        self.a = a
        self.b = b
        
    def operate(self):
        c = self.a
        for step in range(self.b):
            c = c + 1
        result = c
        print(result)

After creating the object x by calling DumbAdder with arguments a, b, and calling operate(), we obtain a result which is the sum of the two arguments.

x = DumbAdder(10,20)
x.operate()

In this case we get 30 printed on the screen as a result.

now, let's say we have a new instance y

y = DumbAdder(5,10)

Now, my question: Is there a way to access the values of c in y, when calling operate(), for each step of the for loop, that is, to display 6,7,8...13,14,15 without modifying the definition of operate()? or with minimal modifications

My goal is to be able to switch between a 'normal mode' and a 'debug mode' for my classes with iterations inside the methods. The debug mode would allow me to inspect the evolution of the intermediate values.

NOTE

While writing the question I came up with this solution. I post the question in case someone wants to share a more efficient or elegant way.

class DumbAdder():
    def __init__(self, a : float, b : float):
        super(DumbAdder, self).__init__()
        self.a = a
        self.b = b
        self.mode = 'normal'
        self.c_history = []
       
    def store_or_not(self, c):
        if self.mode == 'normal':
            pass
        elif self.mode == 'debug':
            self.c_history.append(c)
        
    def operate(self):
        c = self.a
        for x in range(self.b):
            c = c + 1
            self.store_or_not(c)
        result = c
        print(result)

Upvotes: 0

Views: 55

Answers (1)

chepner
chepner

Reputation: 531693

The typical way to handle this is to log the value, e.g. using the logging module.

def operate(self):
    logger = logging.getLogger("operate")
    c = self.a
    for x in range(self.b):
        c = c + 1
        logger.debug(c)
    result = c
    print(result)

What logging.debug does is not operate's concern; it's just a request to do something. What that something is defined the logger's configuration and the handler(s) attached to the logger. Your application can do that without modifying your code at all, since all loggers live in a single application-wide namespace. For example,

# Only log errors by default...
logging.basicConfig(level=logging.ERROR)
# ... but log debugging information for this particular logger
logging.getLogger("operate").setLevel(logging.DEBUG)

This uses the same logging configuration for every instance of DumbAdder, rather than allowing per-instance debugging. I suspect that's all you really need, but if not, you can have __init__ create an instance specific logger and configure it at that time.

def __init__(self, a, b, debug=False):
    super().__init__()
    self.a = a
    self.b = b
    self.logger = logging.getLogger(f'{id(self)}')  # Logger specific to this object
    if debug:
        self.logger.setLevel(logging.DEBUG)

def operate(self):
    c = self.a
    for x in range(self.b):
        c = c + 1
        self.logger.debug(c)
    result = c
    print(result)

Handlers can do much more than simply write to the screen. The default handler created by logging.basicConfig writes to standard error, but the logging.handler module provides for writing to files, syslog, sockets, SMTP servers for e-mail, HTTP services, etc, and you can always write your own handlers to do really whatever you want with the message that the debug method constructs from the value of c it receives.

Upvotes: 1

Related Questions