- Part 2[a]

In case you missed the first part of this series, you can read it here Objects, Types and Values; Python Data Model – Part 1.
We divided this "All about Pythonic Class" particular title into two parts.
- All about Pythonic Class: The Birth and Style
- All about Pythonic Class: The Life Cycle
The title of these chapters in the end might not unambiguously reflect the idea a reader just has depicted into her/his mind. We will try to cover the very basic concepts of how a Python class is formed and destructed with a simplified narrative. The creation and destruction of a Python class, more specifically a Python 3.x class, inevitably juxtaposes with a number of seemingly discrete topics.
More importantly, a reader needs at least moderate knowledge of the Python data model, special methods, metaprogramming, and how the interpreter works behind the scene. Also, there are some repetitions, circular discussion, and oversimplifications that are inevitable – e.g. we had to stick with objects to explain the behavior of a class.
1. How to write a basic class
Let’s write a basic class for Her Excellency Miss Foo, a famed post-rock guitar practitioner in our locality, who has requested us to write a program that will keep track of her students’ _names, money that they will pay her and assign an email_ address with her domain name. She is a developer herself but with a tight daily schedule and asked lazy programmers like us for help. She will, however, use this blueprint or class into her own script. Example of real-life code reuse, huh! Let’s help her.
>>> class Student(): # ...[1]
pass
>>> student_1 = Student() # ... [2]
>>> student_2 = Student() # ... [3]
>>> student_1, student_2 # ... [4]
(<__main__.Student object at 0x7fec1b6387b8>, <__main__.Student object at 0x7fec1b638780>)
>>> print(id(student_1), id(student_2)) # ... [5]
140652048517048 140652048516992
>>> type(student_1) # .......... [6]
<class '__main__.Student'>
We just have written a skeleton Student
class and passed the interpreter a pass
statement telling her to keep it intact until we assign anything to it. We also create two Student
class entity in the statements [2]
and [3]
. Two most important points to note here that despite being derived from the same class Student
, student_1
and student_2
have different identities and locations in the RAM– statements [5]
and [4]
.
On a Sunday morning, Andrew Thompson got admitted into her class. Miss Foo will charge him 5,000 bucks every month. Let’s add his name
>>> student_1.firstname = "Andrew"
>>> student_1.lastname = "Thompson"
>>> student_1.pays = 5000
>>> student_1.mail = student_1.firstname.lower()+'_'+student_1.lastname.lower()+"@msfooguitar.edu"
>>> student_1.mail
'[email protected]'
On a Friday morning, Marc Byrd, a friend of Andrew, enrolled in the class. As he is a beginner, Miss Foo has to pay more effort and attention to him. Marc will pay 6000 bucks a month. Let’s log his name into the registry book.
>>> student_2.firstname = "Marc"
>>> student_2.lastname = "Byrd"
>>> student_2.pays = 6000
>>> student_2.mail = student_2.firstname.lower()+'_'+student_2.lastname.lower()+"@msfooguitar.edu"
>>> student_2.mail
'[email protected]'
Notice the approach we have adopted in the aforementioned example serves our purpose but makes it complex to go further.
- We are assigning instances to each object repetitively. This will create further complexity when the class will produce more objects.
- We will need to define the class whenever we need them. The current example is not helping us.
We can make the situation a bit more flexible by avoiding repetition and ensuring code reusability by literally writing a blueprint of what properties the Student
class and its objects will get.
class Student():
def __init__(self, fname, lname, pays): # ... [1] an initialiser.
self.fname = fname
self.lname = lname
self.pays = pays
self.mail = f"{self.fname.lower()}@msfooguitar.edu"
student_1 = Student("Andrew", "Thompson", 5000)
student_2 = Student("Marc", "Byrd", 6000)
Now what we have here is,
- The class
Student
now acts like a form with three mandatory fields– first name, last name, and the amount of money to pay along with an additional email address– to be filled up by each new student or object when (s)he enrolls in a course. This way a coder can avoid repetitions. - The
__init__
magic method (will discuss within minutes) initializes the variables, fields, or properties each newStudent
object/instance will get. This method is called at the creation of each new object of a class. Remember that__init__
is not a constructor. In Python,__new__
is the constructor - Have you noticed the
self
parameter is passed in the initializer? When an object is created from a class, the interpreter will first pass the object itself to__init__
the method which is conventionally denoted by theself
parameter. We can use any parameter name other thanself
.
class Student():
def __init__(self, fname, lname, pays): # ... [1] an initialiser.
self.fname = fname
self.lname = lname
self.pays = pays
self.mail = f"{self.fname.lower()}@msfooguitar.edu"
def details(self):
print(f"name: {self.fname} {self.lname} pays: {self.pays} mail:
{self.mail}")
student_1 = Student("Andrew", "Thompson", 5000)# ... [2]
student_2 = Student("Marc", "Byrd", 6000)
print(student_1.details(), student_2.details())
# Output
'''
name: Andrew Thompson pays: 5000 mail: [email protected]
name: Marc Byrd pays: 6000 mail: [email protected]
'''
The statement [2]
can be rewritten as Class.objectMethod(object). In our case, it is equivalent to Student.details(student_1)
.
An interesting note that we can override an instance method like a variable on the fly. For example, we can change details()
the callable function into a new string.
student_1.details = "A new string."
print(student_1.__dict__)
# Output
'''
{'fname': 'Andrew',
'lname': 'Thompson',
'pays': 5000,
'mail': '[email protected]',
'details': 'A new string.'}
'''
Notice that details
has now become a string object which is not callable. The dict is a dictionary that contains what variables the object has.
Notice that details
has now become a string object which is not callable.
print(student_1.details())
# Output
'''
Traceback (most recent call last)
...
---> 19 print(student_1.details())
TypeError: 'str' object is not callable
'''
2. Class and Object: Two sides of a coin
Continuing from the first of this series, a discussion on Python Data Model, we will frequently meet the ‘cliche’ and repetitive notion "In Python, everything is an Object."
They are either a prototype or metaclass of type or objects of some other classes
This might be compared to the days when we had just started our *nix journey where we were told at least hundreds of times that everything from a textfile to the keyboard ‘is a file.’ In Python 3.x, a class is also an object. That is, we can treat a class as an object. We can manipulate and modify a class on the fly like the aforementioned examples:
>>> class ObjClass(): pass
...
>>> ObjClass.sentence = "A class is an Object" # ...[1]
>>> ObjClass.sentence
'A class is an Object'
>>> type(ObjClass) # ... [2]
<class 'type'>
>>> print(ObjClass)
<class '__main__.ObjClass'>
>>> ObjClass = "I'm a string now" # ... [3]
>>> type(ObjClass)
<class 'str'>
By treating the class itself as an object, we can change its type [3]
Why is this possible?
To be precise we can run a loop to see that our regular data types, even type
itself is derived from type
metaclass. We’ll have an in-depth chapter on metaclasses. For now, a metaclass takes properties from other classes to generate a new class with additional functionalities.
class MyClass():
pass
myObj = MyClass()
for x in str, int, float, type, True, bool, myObj, MyClass:
print(f'{x} : {type(x)}')
# Output
'''
<class 'str'> : <class 'type'>
<class 'int'> : <class 'type'>
<class 'float'> : <class 'type'>
<class 'type'> : <class 'type'>
True : <class 'bool'> # ...
<class 'bool'> : <class 'type'> # ... [2]
<__main__.MyClass object at 0x7f70d8380550> : <class '__main__.MyClass'>
<class '__main__.MyClass'> : <class 'type'>
'''
So, from the above examples, it is clear that classes and objects in Python are the same things under the hood. They are either a prototype or metaclass of type or objects of some other classes
3. Differences between Old-style classes and New-style classes
An old-style class used to be declared as class A()
while a new style was class A(object)
that "inherits from the object, or from another new-style class." According to the documentation:
"Up to Python 2.1, old-style classes were the only flavour available to the user. The concept of (old-style) class is unrelated to the concept of type: if x is an instance of an old-style class, then
x.__class__
designates the class of x, buttype(x)
is always<type 'instance'>
. … This reflects the fact that all old-style instances, independently of their class, are implemented with a single built-in type, called instance. … The major motivation for introducing new-style classes is to provide a unified object model with a full meta-model.
Let’s dive into an example-
- Old-style class:
#python 2.1
class Student():
_names_cache = {}
def __init__(self, fname):
self.fname = fname
def __new__(cls,fname):
return cls._names_cache
.setdefault(name,object.__new__(cls,fname))
student_1 = Student("Andrew")
student_2 = Student("Andrew")
print student_1 is student_2
print student_1
print student_2
>>> False
<__main__.Student instance at 0xb74acf8c>
<__main__.Student instance at 0xb74ac6cc>
>>>
- New-style class:
#python 2.7
class Student():
_names_cache = {}
def __init__(self, fname):
self.fname = fname
def __new__(cls,fname):
return cls._names_cache
.setdefault(fname,object.__new__(cls,fname))
student_1 = Student("Andrew")
student_2 = Student("Andrew")
print student_1 is student_2
print student_1
print student_2
>>> True
<__main__.Student instance at 0xb74acf8c>
<__main__.Student instance at 0xb74ac6cc>
>>>
Python 3 has only a new-style class. Even if you write an ‘old-style class’, it is implicitly derived from object
. New-style classes have some advanced features lacking in old-style classes, such as super
, the new C3 mro, some magical methods, etc. Keep in mind that we are using Python 3.x throughout our articles.
Guido has written The Inside Story on New-Style Classes, a really great article about new-style and old-style classes in Python.
In the next part, we will discuss Class Life Cycle
References:
- https://docs.python.org/3/reference/datamodel.html
- Fluent Python by Luciano Ramalho. This is the book every Pythonista should read to sharpen their python skills.