Modules, Classes, and Objects & OOPS


Python is something called an “object- oriented programming language.” What this means is there’s a construct in Python called a class that lets you structure your software in a particular way. Using classes, you can add consistency to your programs so that they can be used in a cleaner way, or at least that’s the theory.

Classes and objects are the two main aspects of object oriented programming. A class creates a new type where objects are instances of the class. An analogy is that you can have variables of type i n t which translates to saying that variables that store integers are variables which are instances (objects) of the int class.

Objects can store data using ordinary variables that belong to the object. Variables that belong to an object or class are referred to as fields. Objects can also have functionality by using functions that belong to a class. Such functions are called methods of the class. This terminology is important because it helps us to differentiate between functions and variables which are independent and those which belong to a class or object. Collectively, the fields and methods can be referred to as the attributes of that class.

Fields are of two types - they can belong to each instance/object of the class or they can belong to the class itself. They are called instance variables and class variables respectively.

A class is created using the class keyword. The fields and methods of the class are listed in an indented block.

The self

Class methods have only one specific difference from ordinary functions - they must have an extra first name that has to be added to the beginning of the parameter list, but you do not give a value for this parameter when you call the method, Python will provide it. This particular variable refers to the object itself, and by convention, it is given the name self.

Classes

A class is merely a container for static data members or function declarations, called a class's attributes. Classes provide something which can be considered a blueprint for creating "real" objects, called class instances. Functions which are part of classes are called methods.

The simplest class possible is shown in the following example.


In [4]:
# Declare a Class
class Class_Name(object):
    pass

class Class_Name_slow():
    pass

class Class_Name_for_lazy:
    pass
class Class_Name(base_classes_if_any):
    """optional documentation string"""

    static_member_declarations = 1

    def method_declarations(self):
        """
        documentation
        """
        pass

In [6]:
# first.py

class First:
    pass

fr = First()
print (type(fr))
print (type(First))
print(type(int))


<class '__main__.First'>
<class 'type'>
<class 'type'>

In [7]:
# first.py

class First(object):
    pass

fr = First()
print (type(fr))
print (type(First))
print(type(int))


<class '__main__.First'>
<class 'type'>
<class 'type'>

In [8]:
# first.py
# Class with it's methods  

class Second:
    def set_name(self, name):
        self.fullname = name
        
    def get_name(self):
        return self.fullname

try:
    sec = Second()
    print(sec.get_name())
except Exception as e:
    print(e)


'Second' object has no attribute 'fullname'

In [9]:
# first.py

class Second:
    def set_name(self, name):
        print(id(self))
        self.fullname = name
        
    def get_name(self):
        return self.fullname

sec = Second()
print(id(sec))
sec.set_name("Manish Gupta")
print(sec.get_name())


140352966397344
140352966397344
Manish Gupta

NOTE: both Second and sec are same object as their id's are same


In [1]:
class Second:
    def __init__(self, name = ""):
        self.fullname = name
        
    def set_name(self, name):
        print(id(self))
        self.fullname = name
        
    def get_name(self):
        return self.fullname

sec = Second("Vishal Saxena")
print(sec.get_name())


Vishal Saxena

In [44]:
# first.py

class Second:
    def __init__(self, name, age=35):
        self.name(name)
        self.age = age
        
    def name(self, new_name):
        self.fullname = new_name
        
    def get_name(self):
        return self.fullname

sec = Second("Arya")
print(sec.get_name())
print(sec.age)


Arya
35

In [11]:
class Second:
    def __init__(self, name, age=55):
        self.name(name)
        self.age = age
        
    def name(self, name):
        self.name = name
        
    def get_name(self):
        return self.name

sec = Second("Rajneekanth")
print(sec.get_name())
print(sec.age)
print(dir(sec))
dir(sec.__dir__)


Rajneekanth
55
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'age', 'get_name', 'name']
Out[11]:
['__call__',
 '__class__',
 '__delattr__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__name__',
 '__ne__',
 '__new__',
 '__qualname__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__self__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__text_signature__']

In [18]:
# first.py

class Second:
    fullname = "Mayank Johri"
    age = 33
        
    def name(self, name):
        self.fullname = name
        
    def get_name(self):
        return self.fullname

sec = Second()
print(dir(sec))
print(sec.get_name())
print(id(sec.fullname))
sec2 = Second()
print(id(sec2.fullname))


['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'age', 'fullname', 'get_name', 'name']
Mayank Johri
140097808551344
140097808551344

In [20]:
# first.py

class Second:
    fullname = "Ram Setu"
    age = 33
        
    def name(self, name):
        self.fullname = name
        
    def get_name(self):
        return self.fullname

In [21]:
rs_1 = Second()
rs_2 = Second()

In [25]:
print(rs_1.fullname == rs_2.fullname)
print(id(rs_1.fullname) == id(rs_2.fullname))


True
True

In [26]:
rs_1.name("ram setu")

In [27]:
print(rs_1.fullname == rs_2.fullname)
print(id(rs_1.fullname) == id(rs_2.fullname))


False
False

In [40]:
print(rs_1.fullname, " - ", rs_2.fullname)


['Mayank', 'Johri', 'ram setu']  -  ['Mayank', 'Johri', 'ram setu']
  • The magic of mutables

In [34]:
class Bridge:
    fullname = ["Mayank", "Johri"]
    age = 33
        
    def name(self, name):
        self.fullname.append(name)
        
    def get_name(self):
        return self.fullname

In [35]:
rs_1 = Bridge()
rs_2 = Bridge()

In [36]:
print(rs_1.fullname == rs_2.fullname)
print(id(rs_1.fullname) == id(rs_2.fullname))


True
True

In [37]:
rs_1.name("ram setu")

In [38]:
print(rs_1.fullname == rs_2.fullname)
print(id(rs_1.fullname) == id(rs_2.fullname))


True
True

In [39]:
print(rs_1.fullname, " - ", rs_2.fullname)


['Mayank', 'Johri', 'ram setu']  -  ['Mayank', 'Johri', 'ram setu']

In [ ]:


In [37]:
# Example
class FooClass:
    """my very first class: FooClass"""
    __version = 0.11 # class (data) attribute
    ver = 0.1
    
    def __init__(self, nm='John Doe'):
        'constructor'
        self.name = nm # class instance (data) attribute
    
    def showName(self):
        'display instance attribute and class name'
        print ('Your name is: ', self.name)
        print( 'My name is: ', self.__class__ )# full class name

    def showVersion(self):
        'display class(static) attribute'
        print( self.__version )# references FooClass.version
    
    def showVer(self):
        'display class(static) attribute'
        print( self.ver )# references FooClass.version 
    
    def setVersion(self, ver):
        'display class(static) attribute'
        self.__version = ver
        print( self.__version )# references FooClass.version  

        
# Create Class Instances
foo = FooClass()
arya = FooClass("Arya")
arya.showName()
# Calling class methods
foo.showName()


Your name is:  Arya
My name is:  <class '__main__.FooClass'>
Your name is:  John Doe
My name is:  <class '__main__.FooClass'>

In [38]:
# print(foo.showName())
foo.showVer()
arya.showVer()


0.1
0.1

In [39]:
print(id(foo.ver))
print(id(arya.ver))


139688258419928
139688258419928

In [40]:
print(foo.ver)
foo.setVersion(10)  # __version
foo.ver = 2020202   # ver


0.1
10

In [41]:
foo.showVer()
arya.showVer()


2020202
0.1

In [42]:
foo.name


Out[42]:
'John Doe'

In [29]:
foo.name = "Anamika Johri"
foo.name


Out[29]:
'Anamika Johri'

In [30]:
foo.showVer()
print(foo.ver)
print("-"*20)
print(arya.showVer())

# print(FooClass.__version)


2020202
2020202
--------------------
0.1
None

In [43]:
try:
    print(foo.__version)
except Exception as e:
    print(e)


'FooClass' object has no attribute '__version'

In [8]:
# Example
class User:
    """my very first class: FooClass"""
    __version = 0.11 # class (data) attribute
    ver = 0.1
    
    def __init__(self, firstname='John', surname="Doe"):
        'constructor'
        self.name = firstname + " " + surname 
        print ('Created a class instance for: ', self.name)
    
    def showName(self):
        'display instance attribute and class name'
        print ('Your name is: ', self.name)
        print( 'My name is: ', self.__class__ )# full class name

    def showVersion(self):
        'display class(static) attribute'
        print( self.__version )# references FooClass.version
    
    def showVer(self):
        'display class(static) attribute'
        print( self.ver )# references FooClass.version 
    
    def setVersion(self, ver):
        'display class(static) attribute'
        self.__version = ver
        print( self.__version )# references FooClass.version  

# Create Class Instances
user = User()
arya = User("Arya")
gupta = User(surname="Gupta")
print(arya.showName())


Created a class instance for:  John Doe
Created a class instance for:  Arya Doe
Created a class instance for:  John Gupta
Your name is:  Arya Doe
My name is:  <class '__main__.User'>
None

In [49]:
# Example
class User:
    """my very first class: FooClass"""
    __version = 0.11 # class (data) attribute
    ver = 0.1
    
    def __init__(self, firstname, surname):
        'constructor'
        self.name = firstname + " " + surname 
        print ('Created a class instance for: ', self.name)
    
    # full class name
    def showName(self):
        'display instance attribute and class name'
        print ('Your name is: ', self.name)
        print( 'My name is: ', self.__class__ )

    def showVersion(self):
        'display class(static) attribute'
        print( self.__version )# references FooClass.version
    
    def showVer(self):
        'display class(static) attribute'
        print( self.ver )# references FooClass.version 
    
    def setVersion(self, ver):
        'display class(static) attribute'
        self.__version = ver
        print( self.__version )# references FooClass.version

In [51]:
# Create Class Instances
try:
    user = User()
    arya = User("Arya")
    gupta = User(surname="Gupta")
    gupta = User(surname="Gupta", firstname="Manish")
    arya.showName()
except Exception as e:
    print(e)


__init__() missing 2 required positional arguments: 'firstname' and 'surname'

So, we can't have any object creation with lesser than two parameters. Lets comment out the first three object creation code and try again


In [52]:
# Create Class Instances
try:
#     user = User()
#     arya = User("Arya")
#     gupta = User(surname="Gupta")
    gupta = User(surname="Gupta", firstname="Manish")
    arya.showName()
except Exception as e:
    print(e)


Created a class instance for:  Manish Gupta
Your name is:  Arya
My name is:  <class '__main__.FooClass'>

In [14]:
class PrivateVariables():
    __version = 1.0
    _vers = 11.0
    ver = 10.0
    
    def show_version(self):
        return(self.__version)
    
    def show_vers(self):
        print(self._vers)

In [15]:
pv = PrivateVariables()
print(pv.ver)
print(pv._vers)
# print(pv.__version)

pv.ver = 111
print(pv.ver)
pv._vers = 1000
print(pv._vers)   # Convension only 
print(pv.show_version())


10.0
11.0
111
1000
1.0

In [17]:
print(dir(pv))


['_PrivateVariables__version', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_vers', 'show_vers', 'show_version', 'ver']

In [21]:
print(pv.__dict__)


{'ver': 111, '_vers': 1000}

In [32]:
print(pv.__dict__.get('__version', "default value"))


default value

In [33]:
print(pv.__dict__.get('ver'))


111

In [35]:
pv.__dict__['ver'] = 1010
print(pv.__dict__.get('ver'))


1010

In [39]:
try:
    print(pv.__version)
except Exception as e:
    print(e)


'PrivateVariables' object has no attribute '__version'

static / class variables

Static variables are variables declared inside the class definition, and not inside a method are class or static variables.

But before you go all, Yahooooo... about understanding of static variables. Please note that the implementation of static variables in python are different from Java/C++, they are unique in many ways.

Lets understand them a little using the following code


In [18]:
class Static_Test(object):
    val = "Rajeev Chaturvedi"

In [19]:
s = Static_Test()

In [20]:
print(s.val,"\b,", id(s.val))
print(Static_Test.val, "\b,", id(Static_Test.val))


Rajeev Chaturvedi , 140463287387960
Rajeev Chaturvedi , 140463287387960

So far so good, val & id of val from both the instance and class seems to be same, thus they are pointing to same memory location which contains the value. Now lets try to update it in class


In [22]:
Static_Test.val = "राजीव चतुर्वेदी"

In [25]:
print(s.val,"\b,", id(s.val))
print(Static_Test.val, "\b,", id(Static_Test.val))
s_new = Static_Test()
print(s_new.val,"\b,", id(s.val))


राजीव चतुर्वेदी , 140463287335304
राजीव चतुर्वेदी , 140463287335304
राजीव चतुर्वेदी , 140463287335304

So, if we update values at class level, than they are getting reflected in all the instances as well. Now lets try to update its value in an instance and check its effect


In [27]:
s.val = "Sachin"
print(s.val,"\b,", id(s.val))
print(Static_Test.val, "\b,", id(Static_Test.val))
s_new = Static_Test()
print(s_new.val,"\b,", id(s.val))


Sachin , 140463287695320
राजीव चतुर्वेदी , 140463287335304
राजीव चतुर्वेदी , 140463287695320

Once, instance value has been changed then it remain changed and cannot be reverted by changing class variable value as shown in the below code


In [29]:
Static_Test.val = "Sachin Shah"
print(s.val,"\b,", id(s.val))
print(Static_Test.val, "\b,", id(Static_Test.val))
s_new = Static_Test()
print(s_new.val,"\b,", id(s.val))


Sachin , 140463287695320
Sachin Shah , 140463287378800
Sachin Shah , 140463287695320

Static and Class Methods

Python provides decorators @classmethod & @staticmethod

@staticmethod

A static method does not receive an implicit first argument (self or cls). To declare a static method decorator staticmethod is used as shown in the below example


In [2]:
class Circle(object):
    PI = 3.14
    @staticmethod
    def area_circle(radius):
        area = 0
        try:
            area = PI * radius * radius
        except Exception as e:
            print(e)
        return area

c = Circle()
print(c.area_circle(10))


name 'PI' is not defined
0

As shown in the above example, static methods do not have access to any class or instance attributes. We tried to access class attribute PI and received error message that variable not defined.

Static methods for all intent and purpose act as normal function, but are called from within an object or class.

Static methods similar to class methods are bound to a class instead of its object, thus do not require a class instance creation and thus are not dependent on the state of the object.

Still there are few noticible differences between a static method and a class method, few of them are as follows:

  • Static method are isolated from its class/object and have access only to the parameters passed to it.
  • Class method works with the class since its parameter is always the class itself.

When do you use static method

So, if they do not have access to the class, then why are they created. We will try to understand the logic of why they should be created.

Grouping utility function to a class

In [ ]:
Many times, we have to

In [ ]:

Having a single implementation

In [ ]:


In [ ]:


In [ ]:


In [ ]:


In [ ]:

attributes

In Python, attribute is everything, contained inside an object. In Python there is no real distinction between plain data and functions, being both objects.

The following example represents a book with a title and an author. It also provides a get_entry() method which returns a string representation of the book.


In [43]:
class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author

    def get_entry(self):
        return f"{self.title} by {self.author}"

Every instance of this class will contain three attributes, namely title, author, and get_entry, in addition to the standard attributes provided by the object ancestor.


In [44]:
b = Book(title="Akme", author="Mayank")

In [45]:
print(dir(b))


['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'author', 'get_entry', 'title']

In [46]:
print(b.title)
b.title = "Lets Go"
print(b.title)
print(b.get_entry())


Akme
Lets Go
Lets Go by Mayank

In [4]:
data = b.get_entry
print(data)
print(data())
print(type(b.__dict__))
print(b.__dict__)
#print(b.nonExistAttribute())


<bound method Book.get_entry of <__main__.Book object at 0x0000013DDBEB6DA0>>
Lets Go by Mayank
<class 'dict'>
{'title': 'Lets Go', 'author': 'Mayank'}

In [5]:
def testtest(func):
    print(func())

testtest(data)


Lets Go by Mayank

Instead of using the normal statements to access attributes, you can use the following functions −

getattr : to access the attribute of the object

The getattr(obj, name[, default]) : to access the attribute of object.

The hasattr(obj,name) : to check if an attribute exists or not.

The setattr(obj,name,value) : to set an attribute. If attribute does not exist, then it would be created.

The delattr(obj, name) : to delete an attribute.

Properties

Sometimes you want to have an attribute whose value comes from other attributes or, in general, which value shall be computed at the moment. The standard way to deal with this situation is to create a method, called getter, just like I did with get_entry().

In Python you can "mask" the method, aliasing it with a data attribute, which in this case is called property.


In [23]:
class Book(object):
    def __init__(self, title, author):
        self.title = title
        self.author = author

    def get_entry(self):
        return "{0} by {1}".format(self.title, self.author)

    entry = property(get_entry)

b = Book(title="Pawn of Prophecy", author="David Eddings")
print(b.entry)


Pawn of Prophecy by David Eddings

Properties allow to specify also a write method (a setter), that is automatically called when you try to change the value of the property itself.

NOTE:

Don't Worry to much about properties, we have entire chapter dedicated for it.


In [11]:
class User():
    def __init__(self, name):
        self.name = name
    
    def getname(self):
        return "User's full name is: {0}".format(self.name) 
    
    def setname(self, name):
        self.name = name
        
    fullname = property(getname, setname)
    
user = User("Roshan Musheer")
print(user.fullname)
user.fullname = "Shaeel Parez"
print(user.fullname)
# print(x)
# print(p.name)


Users full name is: Roshan Musheer
Users full name is: Shaeel Parez

In [ ]:


In [16]:
class TestSetter():
    def setter(self, name):
        self.name = name
    myname = property(fset=setter)
    
ts = TestSetter()

ts.myname = "Mayank"
print(ts.name)


Mayank

In [28]:
class A:
    def get_x(self, neg=False):
        return -5 if neg else 5
    x = property(get_x)
    
a = A()
print(a.x)


5

In [30]:
class Book(object):
    def __init__(self, title, author):
        self.__title = title
        self.__author = author

    def __get_entry(self):
#         print("_get_entry")
        return "{0} by {1}".format(self.__title, self.__author)

    def __set_entry(self, value):
        if " by " not in value:
            raise ValueError("Entries shall be formatted as '<title> by <author>'")
        self.__title, self.__author = value.split(" by ")
    
    entry = property(__get_entry, __set_entry)

    def __getattr__(self, attr):
        print("Sorry attribure do not exist")
        return None

In [31]:
b = Book(title="Step in C", author="Mayank Johri")
print(b.entry)
b.entry = "Lets learn C by Mayank Johri"
print("*"*20)
print(b.entry)
print("*"*20)
b.entry = "Explore Go by Mayank Johri"
print("*"*20)
print(b.entry)
b.nonExistAttribute


Step in C by Mayank Johri
********************
Lets learn C by Mayank Johri
********************
********************
Explore Go by Mayank Johri
Sorry attribure do not exist

__new__

__new__ is called for new Class type,

Overriding the new method

As per "https://www.python.org/download/releases/2.2/descrintro/#__new__"

Here are some rules for __new__:

  • __new__ is a static method. When defining it, you don't need to (but may!) use the phrase "__new__ = staticmethod(__new__)", because this is implied by its name (it is special-cased by the class constructor).
  • The first argument to __new__ must be a class; the remaining arguments are the arguments as seen by the constructor call.
  • A __new__ method that overrides a base class's __new__ method may call that base class's __new__ method. The first argument to the base class's __new__ method call should be the class argument to the overriding __new__ method, not the base class; if you were to pass in the base class, you would get an instance of the base class.
  • Unless you want to play games like those described in the next two bullets, a __new__ method must call its base class's __new__ method; that's the only way to create an instance of your object. The subclass __new__ can do two things to affect the resulting object: pass different arguments to the base class __new__, and modify the resulting object after it's been created (for example to initialize essential instance variables).
  • __new__ must return an object. There's nothing that requires that it return a new object that is an instance of its class argument, although that is the convention. If you return an existing object, the constructor call will still call its __init__ method. If you return an object of a different class, its __init__ method will be called. If you forget to return something, Python will unhelpfully return None, and your caller will probably be very confused.
  • For immutable classes, your __new__ may return a cached reference to an existing object with the same value; this is what the int, str and tuple types do for small values. This is one of the reasons why their __init__ does nothing: cached objects would be re-initialized over and over. (The other reason is that there's nothing left for __init__ to initialize: __new__ returns a fully initialized object.)
  • If you subclass a built-in immutable type and want to add some mutable state (maybe you add a default conversion to a string type), it's best to initialize the mutable state in the __init__ method and leave __new__ alone.
  • If you want to change the constructor's signature, you often have to override both __new__ and __init__ to accept the new signature. However, most built-in types ignore the arguments to the method they don't use; in particular, the immutable types (int, long, float, complex, str, unicode, and tuple) have a dummy __init__, while the mutable types (dict, list, file, and also super, classmethod, staticmethod, and property) have a dummy __new__. The built-in type 'object' has a dummy __new__ and a dummy __init__ (which the others inherit). The built-in type 'type' is special in many respects; see the section on metaclasses.
  • (This has nothing to do to __new__, but is handy to know anyway.) If you subclass a built-in type, extra space is automatically added to the instances to accomodate dict and weakrefs. (The dict is not initialized until you use it though, so you shouldn't worry about the space occupied by an empty dictionary for each instance you create.) If you don't need this extra space, you can add the phrase "`__slots__ = []" to your class. (See above for more aboutslots`.)
  • Factoid: __new__ is a static method, not a class method. I initially thought it would have to be a class method, and that's why I added the classmethod primitive. Unfortunately, with class methods, upcalls don't work right in this case, so I had to make it a static method with an explicit class as its first argument. Ironically, there are now no known uses for class methods in the Python distribution (other than in the test suite). I might even get rid of classmethod in a future release if no good use for it can be found!

What is the difference between __new__ and __init__

Use __new__ when you need to control the creation of a new instance. Use __init__ when you need to control initialization of a new instance.

__new__ is the first step of instance creation. It's called first, and is responsible for returning a new instance of your class. In contrast, __init__ doesn't return anything; it's only responsible for initializing the instance after it's been created.

In general, you shouldn't need to override __new__ unless you're subclassing an immutable type like str, int, unicode or tuple.

From: http://mail.python.org/pipermail/tutor/2008-April/061426.html


In [1]:
class MyTest:
    def __new__(self):
        print("in new")
        
    def __init__(self):
        print("in init")

mnt = MyTest()


in new

In [2]:
class MyNewTest:
    def __new__(self):
        print("in new")

    def __new__(self, name):
        print("in new", name)
        
    def __init__(self, name):
        print("in init", name)

mnt = MyNewTest("Hari Hari")


in new Hari Hari

Lets look at another example, we have removed the __new__ method from the above class and created an object.


In [53]:
class MyNewTest:        
    def __init__(self, name):
        print("in init", name)

mnt = MyNewTest("Hari Hari")


in init Hari Hari

Now lets check where its goog idea to use __init__ and where __new__.

One thumb rule is try to avoid using __new__ and let python handle it because almost all the things you wish to do in constructor can be done in __init__. Still if you wish to do so, below examples will show you how to do it currectly.

In the first example, we have __init__ function and are using it.


In [3]:
class MyNewTest:        
    def __init__(self, name):
        print("in init", name)
        self.name = name
        
    def print_name(self):
        print(self.name)


mnt = MyNewTest("Hari Hari")
mnt.print_name()


in init Hari Hari
Hari Hari

We saw, that everything was working without any issue. Now lets try to replace __init__ with __new__.


In [4]:
# -----------------#
# Very Bad Example #
# -----------------#
class MyNewTest:        
    def __new__(cls, name):
        print("in init", name)
        cls.name = name
        
    def print_name(self):
        print(self.name)

try:
    mnt = MyNewTest("Hari Hari")
    mnt.print_name()
except Exception as e:
    print(e)


in init Hari Hari
'NoneType' object has no attribute 'print_name'

Now, since we have not returned any thing in __new__ thus mnt is null. We must have __new__ which returns the object itself. Now to over come this issue, we need to return an instance of our class. We can do that using instance = super(<class>, cls).__new__(cls) as shown in the below example


In [4]:
class MyNewTest(object):        
    def __new__(cls, name):
        print("in __new__:\n\t{0}".format(name))
        instance = super(MyNewTest, cls).__new__(cls)
        instance.name = name
        return instance
    
    def print_name(self):
        print("print_name:\n\t{0}".format(self.name))
        
mnt = MyNewTest("!!! Hari Om Hari Om !!!")
mnt.print_name()
ram_ram = MyNewTest("!!! Ram Ram !!!")
ram_ram.print_name()
mnt.print_name()


in __new__:
	!!! Hari Om Hari Om !!!
print_name:
	!!! Hari Om Hari Om !!!
in __new__:
	!!! Ram Ram !!!
print_name:
	!!! Ram Ram !!!
print_name:
	!!! Hari Om Hari Om !!!

or, we can create the class using the following code, instance = object.__new__(cls). As object is parent, we are directly calling it instead of using super.


In [5]:
class MyNewTest(object):        
    def __new__(cls, name):
        print("in __new__", name)
        instance =  object.__new__(cls)
        instance.name = name
        print("exiting __new__", name)
        return instance
    
    # __init__ is redundent in this example. 
    def __init__(self, name):    
        print("in __init__", name)
    
    def print_name(self):
        print(self.name)
        
mnt = MyNewTest("Hari Hari")
mnt.print_name()
ram_ram = MyNewTest("Ram Ram")
print(ram_ram)


in __new__ Hari Hari
exiting __new__ Hari Hari
in __init__ Hari Hari
Hari Hari
in __new__ Ram Ram
exiting __new__ Ram Ram
in __init__ Ram Ram
<__main__.MyNewTest object at 0x7fa10077be48>

both super(MyNewTest, cls).__new__(cls) and object.__new__(cls) produce the desired instance as shown in the above examples.

If we were to return anything other than instance of object, then __init__ function will never be called as shown in the below example.


In [3]:
class Distance(float):        
    def __new__(cls, dist):
        print("in __new__", dist)
        return dist*0.0254
    
    # __init__ is redundent in this example, 
    # as it will never be called. 
    def __init__(self, dist):    
        print("in __init__", dist)
    
    def print_dist(self):
        print(self.__name__)
        
try:
    mnt = Distance(22)
    print(mnt, type(mnt))
    mnt.print_dist()
except Exception as e:
    print(e)


in __new__ 22
0.5588 <class 'float'>
'float' object has no attribute 'print_dist'

In [12]:
class Distance(float):        
    def __new__(cls, dist):
        print("in __new__", dist)
        instance =  super(Distance, cls).__new__(cls)
        print(type(instance))
        instance.val = dist*0.0254
        return instance
    
    def __init__(self, dist):    
        print("in __init__", dist)
    
    def print_dist(self):
        print(self.val)
        

if __name__ == "__main__":
    try:
        mnt = Distance(22)
        print(mnt, type(mnt))
        mnt.print_dist()
    except Exception as e:
        print(e)


in __new__ 22
<class '__main__.Distance'>
in __init__ 22
0.0 <class '__main__.Distance'>
0.5588

Where can we use __new__

Creating singleton class

In singleton pattern, we create one instance of the class and all subsequent objects of that class points to the first instance.

Lets try to create a singleton class using __new__ constructor.


In [36]:
class Godlike(object):
    
    def __new__(cls, name):
        it = cls.__dict__.get("__it__")
        if it is not None:
            return it
        cls.__it__ = it = object.__new__(cls)
        it.init(name)
        return it
    
    def init(self, name):
        self.name = name
    
    def print_name(self):
        print(self.name)
        
        
ohm = Godlike("Ohm")
ram = Godlike("ram")
hari = Godlike("hari")

print(ohm is ram)
print(ohm is hari)
ohm.print_name()
ram.print_name()


True
True
Ohm
Ohm

Note, in the above example all three objects are pointing to same object ohm meaning all three objects are same.

Now, we might have situations where we need to raise exception, if creation of more than one instance is attempted. We can achieve it by raising an exception as shown in below example.


In [38]:
class SingletonError(Exception):
    pass

class HeadMaster(object):
    
    def __new__(cls, name):
        it = cls.__dict__.get("__it__")
        if it is not None:
            raise SingletonError(f"Count not create new instance for value {name}")
            
        cls.__it__ = it = object.__new__(cls)
        it.__init__(name)
        return it
    
    def __init__(self, name):        
        self.name = name
    
    def print_name(self):
        print(self.name)
        

try:
    print("Creating Anshu Mam as Primary School headmistress.")
    anshu_mam = HeadMaster("Anshu Shrivastava")
    print("Creating Rahim Sir as Primary School headmaster.")
    rahim_sir = HeadMaster("Rahim Khan")
except Exception as e:
    print(e)


Creating Anshu Mam as Primary School headmistress.
Creating Rahim Sir as Primary School headmaster.
Count not create new instance for value Rahim Khan

Regulating number of object creation

we are going to tweak previous example and convert it to have a finite number of objects created for the class


In [1]:
class HeadMaster(object):
    _instances = []  # Keep track of instance reference
    limit = 2

    def __new__(cls, *args, **kwargs):
        if len(cls._instances) >= cls.limit:
            raise RuntimeError("Creation Limit %s reached" % cls.limit)
        instance = object.__init__(cls)
        cls._instances.append(instance)
        return instance

    def __del__(self):
        self._instance.remove(self)

try:
    li1 = HeadMaster()
    li2 = HeadMaster()
    li3 = HeadMaster()
    li4 = HeadMaster()
except Exception as e:
    print(e)


Creation Limit 2 reached

Customize instance object

We can customize instance object using __new__

Customize Returned Object

As shown above, we can also return custom objects instead of instance of requested class as shown in one of the previous example.