-1

Let's say we call a constructor expression:

account2 = Account('John Paul', Decimal('-1234'))

The class definition has an initialization method of:

def __init__(self, name, balance):
    if balance < Decimal('0.00'):
        raise ValueError

    self.name = name
    self.balance = balance

In our constructor expression call the balance is negative and a ValueError is raised by the __init__ method.

If I then try to reference the variable account I get a NameError from my REPL. However, doesn't the class object have to be instantiated for that reference 'self' to be passed into __init__ for the initialization method to run in the first place?

I think that Python is reacting correctly, but I'm a tad confused how Python works in the background.

enter image description here

user3558409
  • 99
  • 1
  • 2
  • 7
  • 1
    please, provide error traceback and steps to reproduce an error – Azat Ibrakov Oct 18 '19 at 16:02
  • 4
    The right hand side is always completely evaluated before being assigned to your variable. As `Account('John Green', Decimal('-60.00'))` couldn't be evaluated, nothing got assigned to `account`. – Thierry Lathuille Oct 18 '19 at 16:04
  • "If I then try to reference the variable account..." Please show code that illustrates this verbal description. – Code-Apprentice Oct 18 '19 at 16:05
  • 2
    The *object* certainly existed, in order for `.__init__()` to be invoked on it. However, the exception was raised before that object could be stored anywhere; the assignment to `account2` never took place. – jasonharper Oct 18 '19 at 16:07
  • Welcome to StackOverflow. Please read and follow the posting guidelines in the help documentation, as suggested when you created this account. [Minimal, complete, verifiable example](https://stackoverflow.com/help/minimal-reproducible-example) applies here. We cannot effectively help you until you post your MCVE code and accurately specify the problem. We should be able to paste your posted code into a text file and reproduce the problem you specified. – Prune Oct 18 '19 at 16:07
  • 1
    @user3558409 the object is constructed before it is passed to `__init__`. The object is created by `__new__`, then the result of that is passed to `__init__`. – juanpa.arrivillaga Oct 18 '19 at 16:08
  • Thanks all. Question answered in full. – user3558409 Oct 18 '19 at 16:09

4 Answers4

4
  • An object is created implicitly with __new__.
  • That object is passed as self to __init__.
  • __init__ raises an exception.
  • The exception cause the expression Account(...) to, well, raise an exception, which causes the statement account = ... to not complete.

So yes, the object did exist briefly while the right hand side of the assignment statement was being evaluated, but it never got to the stage where that object would have been assigned to account.

ruohola
  • 21,987
  • 6
  • 62
  • 97
deceze
  • 510,633
  • 85
  • 743
  • 889
2

However, doesn't the class object have to be instantiated for that reference 'self' to be passed into __init__ for the initialization method to run in the first place?

Yes, self has a valid value inside __init__(). However, account is not assigned a value until after __init__() returns successfully. Since an error is thrown instead, account never gets assigned a value.

is the object still instantiated, but floating around in space without the attributes filled in. __init__ is still called with self after all.

Yes, the object is still in memory somewhere. But since there are no references to it, it is elligible for garbage collection.

Code-Apprentice
  • 81,660
  • 23
  • 145
  • 268
  • 2
    Just a note, in CPython, objects with a reference count of 0 are reclaimed *immediately*, but the language does not guarantee that, simply that they are eligible for garbage collection. – juanpa.arrivillaga Oct 18 '19 at 16:16
2

When you do Account('John Paul', Decimal('-1234')) then first Account.__new__ is called to create the object and if that object is an instance of Account then Account.__init__ is invoked to further customize that object. Both actions are performed as a result of Account(...) and are used to compute the r.h.s. of the assignment:

account2 = Account('John Paul', Decimal('-1234'))

If an error is raised during that computation no value exists to be assigned and so the assignment never happens. That's why you receive the NameError.

You can use the following example class to verify that both __new__ and __init__ are called:

class Account:
    def __new__(cls, *args, **kwargs):
        print(f'__new__({cls}, {args}, {kwargs})')
        return super().__new__(cls)

    def __init__(self, *args, **kwargs):
        print(f'__init__({self}, {args}, {kwargs})')
        raise ValueError()


Account('John Paul', -1234)

which prints:

__new__(<class '__main__.Account'>, ('John Paul', -1234), {})
__init__(<__main__.Account object at 0x7fafcdfacd68>, ('John Paul', -1234), {})
a_guest
  • 34,165
  • 12
  • 64
  • 118
0

The other answers address why you get an error when trying to refer to account later - it's because the "assignment" part of the statement doesn't execute after the "construct" part of the statement runs into its own error.

I'll answer the other half of your question:

However, doesn't the class object have to be instantiated for that reference 'self' to be passed into __init__ for the initialization method to run in the first place?

If you've ever wondered why __init__() is called "init", instead of something else, this is why.

The process that happens when you invoke a class like that (Account(...)) actually has two phases. __init__() is the second phase - initialization, wherein an already-allocated object is customized and prepared. The first phase is a method called __new__().

__new__() is a static method, and it's responsible for creating the object to which self, in __init__(), refers.

Python documents __new__() in the data model docs.

See also this answer for more detail in the order of execution.

chepner
  • 497,756
  • 71
  • 530
  • 681
Green Cloak Guy
  • 23,793
  • 4
  • 33
  • 53