Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does python's Exception's repr keep track of passed object's to __init__?

Please see the below code snippet:

In [1]: class A(Exception):
   ...:     def __init__(self, b):
   ...:         self.message = b.message
   ...: 

In [2]: class B:
   ...:     message = "hello"
   ...: 

In [3]: A(B())
Out[3]: __main__.A(<__main__.B at 0x14af96790>)

In [4]: class A:
   ...:     def __init__(self, b):
   ...:         self.message = b.message
   ...: 

In [5]: A(B())
Out[5]: <__main__.A at 0x10445b0a0>

if A subclasses from Exception, its repr returns a reference to B() even though we only pass B()'s message attribute.

Why is this the intentional behavior in Exception.repr and how does it work in python psuedocode if possible given the cpython code isn't too readable ?

like image 686
marwan Avatar asked Aug 13 '21 14:08

marwan


People also ask

What is the purpose of __ init __ in Python?

The __init__ method is the Python equivalent of the C++ constructor in an object-oriented approach. The __init__ function is called every time an object is created from a class. The __init__ method lets the class initialize the object's attributes and serves no other purpose. It is only used within classes.

What is the purpose of defining the functions __ str __ and __ repr __ within a class how are the two functions different?

To put it simply: __str__ is used in to show a string representation of your object to be read easily by others. __repr__ is used to show a string representation of the object.

What is __ init __ In Python class?

"__init__" is a reseved method in python classes. It is called as a constructor in object oriented terminology. This method is called when an object is created from a class and it allows the class to initialize the attributes of the class.

What is __ init __? Explain with example *?

The Default __init__ Constructor in C++ and Java. Constructors are used to initializing the object's state. The task of constructors is to initialize(assign values) to the data members of the class when an object of the class is created.

What happens when you run a repr() function in Python?

When we run a repr () function, it internally invokes the __repr__ () method which is already present in the class as seen in the above code. When a class is passed to the repr () function, it returns a string containing the name of the passed class and the id of the passed class’s instance.

Why does the second PRINT statement throw an exception in Python?

The second print statement tries to access the fourth element of the list which is not there and this throws an exception. This exception is then caught by the except statement. A try statement can have more than one except clause, to specify handlers for different exceptions. Please note that at most one handler will be executed.

What is the difference between str() and repr() in Python?

Although repr () and str () both return a similar-looking string, the style of the content within the string is different. str () returns an easily readable unofficial string while repr () returns an official univocal string. We say official because the string produced by the repr () function can be later used as an argument for the eval function.

What is built-in exception in Python?

Built-in Exceptions¶. In Python, all exceptions must be instances of a class that derives from BaseException. In a try statement with an except clause that mentions a particular class, that clause also handles any exception classes derived from that class (but not exception classes from which it is derived).


2 Answers

When a Python object is created, it is the class's __new__ method that is called, and __init__ is then called on the new instance that the __new__ method returns (assuming it returned a new instance, which sometimes it doesn't).

Your overridden __init__ method doesn't keep a reference to b, but you didn't override __new__, so you inherit the __new__ method defined here (CPython source link):

static PyObject *
BaseException_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    // ...
    if (args) {
        self->args = args;
        Py_INCREF(args);
        return (PyObject *)self;
    }
    // ...
}

I omitted the parts that are not relevant. As you can see, the __new__ method of the BaseException class stores a reference to the tuple of arguments used when creating the exception, and this tuple is therefore available for the __repr__ method to print a reference to the objects used to instantiate the exception. So it's this tuple which retains a reference to the original argument b. This is consistent with the general expectation that repr should return Python code which would create an object in the same state, if it can.

Note that it is only args, not kwds, which has this behaviour; the __new__ method doesn't store a reference to kwds and __repr__ doesn't print it, so we should expect not to see the same behaviour if the constructor is called with a keyword argument instead of a positional argument. Indeed, that is what we observe:

>>> A(B())
A(<__main__.B object at 0x7fa8e7a23860>,)
>>> A(b=B())
A()

A bit strange since the two A objects are supposed to have the same state, but that's how the code is written, anyway.

like image 149
kaya3 Avatar answered Oct 13 '22 00:10

kaya3


Alright, I think I've found the trick. Here's the C source code, but I'll reimplement something similar in Python to demonstrate.

In addition to the usual __init__ (which you're overriding), Python also has a magic method called __new__. When you construct an A as A(B()), it's doing something roughly like

b = B()
a = A.__new__(A, b)
a.__init__(b)

Now, you've overridden A.__init__, so Exception.__init__ never gets called. But A.__new__ is simply Exception.__new__ (more precisely, it's BaseException.__new__, which is essentially the C source code I linked). And, based on the linked code, that's roughly

class BaseException:
  def __new__(cls, *args):
    obj = object.__new__(cls)
    obj.args = args
    return obj

So we explicitly store the arguments tuple in a field called args on the exception object. This is the actual arguments tuple passed to the constructor, even if we override __init__. So repr is just referencing self.args to get the original arguments back.

Note that I'm being a bit imprecise here. If you check BaseException.__new__ in the REPL, you'll see that it's still object.__new__. The C callbacks work differently and use some compiler magic we don't have access to, but the basic idea is the same.

like image 4
Silvio Mayolo Avatar answered Oct 13 '22 00:10

Silvio Mayolo