Why are the statements in a class body executed before any instances of the class are created?

Objects bundle together logically related data (which describe the state of the object) and callable objects that operate on that data, namely methods. This is known as “encapsulation”.

For objects of the same type, the methods are identical, so individual instances are distinguished by the values of their data attributes. For example, for objects of type Triangle, you can define methods to compute perimeter and area using appropriate formulas, along with data attributes representing the lengths of the sides. Triangle instances are then distinguished by their side lengths.

If the methods are the same, it would be a waste of memory to create and store separate method objects for every instance. Instead, it is sufficient to create them once, and have all instances share these callable objects.

This kind of “sharing” is what class definitions provide. In the body of a class, we define all the functions and data attributes that all instances will use. However, in order for these functions to be available for use by instances, the code must be executed within the body of the class definition, thereby creating the function objects whose references (names) are added to the class namespace.

When a method is called on an instance, the interpreter looks up the method name in the instance’s class. If it finds it as a valid function attribute, it creates a callable object, passing both the instance object and the function object it just found. This callable object is the method object, which is then invoked by passing the instance as the first argument to the function, followed by any additional arguments provided at call time.

It is important to note that, unlike the functions defined in the class, a method object is only created temporarily for the duration of the call. (Of course, if it is assigned to a variable, it will exist as long as there is a reference to it.)

To better understand the mechanism, all of this is illustrated in the example below.

Here, a class named Rectangle1 is defined. An instance of it is specified by its two side lengths. In addition, you can call a method named scale, which returns a new rectangle instance whose sides are a given multiple of the original rectangle’s sides. At the end of the class definition, there is a print statement.

The test code is simple: first we create an instance, then we call the scale() method on it, and print the result. When you run the program, you will get the expected new rectangle instance. But before that, you can also see the output from the print statement at the end of the class definition. This indicates that the statements in the class definition are executed before instantiation.

In the following code snippet, we define a class named Rectangle2, which also produces rectangle instances. This time, however, we emulate instance method calls using a custom Method class. We achieve this by creating, inside the __init__ function of Rectangle2, an attribute named scale on the newly created instance (referenced by self). Then, an instance of the Method class is assigned to it. This Method instance is initialized with both the rectangle instance and the function object named scale from the Rectangle2 class. To make it visible when the Method instance is called, we include a print statement in its __call__ special function.

The test lines and their output are similar to the previous example, with the difference that now you can also see the message printed when the Method object is invoked. This shows that, in this case, the instance does not use the normal method call mechanism as in Rectangle1; instead, a custom Method object is called.

Closure functions, like object instances, can also encapsulate state by capturing variables, so they could, in principle, be used as “instances.” However, as mentioned at the end of the previous post discussing multiple closures accessing the same enclosed local variable, this approach is only practical when there are few instances. This is because creating a closure produces not only the data attributes but also a new callable object, which is undesirable from a memory usage perspective.

Further details about the relationship between class instances and methods can be found in the e-book Python Knowledge Building Step by Step from the Basics to the First Desktop Application, in the chapter “The Symbiosis of Function and Object – Methods” and in the section “Finding the Corresponding Attribute Value for an Attribute Name”.

Interested in the e-book Python Knowledge Building Step by Step: From the Basics to Your First Desktop Application?