The world’s leading publication for data science, AI, and ML professionals.

All about Pythonic Class: The Birth and Style

Python Data Model

  • Part 2[a]
Class=”wp-block-image size-large”>Image: source
Image: source

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.

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 new Student 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 the self parameter. We can use any parameter name other than self.
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, but type(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-

  1. 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>
>>>
  1. 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:

  1. https://docs.python.org/3/reference/datamodel.html
  2. Fluent Python by Luciano Ramalho. This is the book every Pythonista should read to sharpen their python skills.

Related Articles