<jupyter_start><jupyter_text>Class, object and Inheritance Class : Is a user defined blueprint from which objects are created.Classes are created by keyword class. Members : Are nothing but variables and functions defined within a class. Note : Function defined as a member of a class is generally called as method. Attributes : Are nothing but the members of the class. Object : Is an instance of a class from which class attributes can be accessed. Note : Self parameter should be passed as a default parameter to the function within the class.<jupyter_code>#Example to perform arithmatic operations using class.
class cal:
    def fn(self, a, b):
        print('Addition', a+b)
        print('subtraction', a-b)
        print('multiplication', a*b)
        print('division', a/b)
        print('floor_division', a//b)
        print('modulus', a%b)
        print('power', a**b)
ob = cal() #object
ob.fn(5, 3)<jupyter_output>Addition 8
subtraction 2
multiplication 15
division 1.6666666666666667
floor_division 1
modulus 2
power 125<jupyter_text>Inheritance : Is nothing but using the attributes and methods of one class in another. Sub class: Is a class which inherits all the attributes and methods of another class. Base class : Is a class from which all the attributes and methods are inherited.<jupyter_code>#example for inheritance.
class family:
    grand_parent_1 = 'a'
    grand_parent_2 = 'b'
    parent_1 = 'c'
    parent_2 = 'd'
class parent(family):
    parent_1_ch_1 = 'e'
    parent_1_ch_2 = 'f'
    parent_1_ch_1_c_1 = 'g'
    parent_1_ch_1_c_2 = 'h'
p = parent()
print('My grandparent_1 name is {} and my grandparent_2 name is {} they have two childs one of them is {} and the other one is {} then again parent_1 has two childs one of them is {} and the other one is {} and again the child of parent_1 has two childs one of them is {} and the other one is {}'.format(p.grand_parent_1, p.grand_parent_2, p.parent_1, p.parent_2, p.parent_1_ch_1, p.parent_1_ch_2, p.parent_1_ch_1_c_1, p.parent_1_ch_1_c_2))
class a:
    number = 0
    name = 'noname' #these are the attributes which are just like variables in python.
def fn():
    b = a() #this is an object created for class 'a'
    b.number = 18
    b.name = 'virat'
    print(b.name + " " + str(b.number))
if __name__=='__main__':
    fn()<jupyter_output>virat 18<jupyter_text>Note: While defining a function inside a class a paremeter called self is always passed.<jupyter_code>class a:
    def b(self, x, y):
        self.x = x
        self.y = y #these are the class methods
def main():
    c = a()
    c.b(5, 6)
    print(c.x+c.y)
if __name__=='__main__':
    main()<jupyter_output>11<jupyter_text>Inheritance: Inheritance is defined as a way in which a particular class inherits features from its base class.Base class is also knows as ‘Superclass’ and the class which inherits from the Superclass is knows as ‘Subclass’<jupyter_code>class pet:
    def __init__(self, name, age): #this is the constructor in python
        self.name = name
        self.age = age 
class dog(pet):
    def __init__(self, gender):
        self.gender = gender
d = dog('toomy')
d.name = 'tommy'
d.age = 7
d.gender = 'female'
print(d.name, d.age, d.gender)<jupyter_output>tommy 7 female<jupyter_text>Iterators: Iterators are objects that can be iterated upon.Python uses the __iter__() method to return an iterator object of the class.The iterator object then uses the __next__() method to get the next item.for loops stops when StopIteration Exception is raised.<jupyter_code>class reverse:
    def __init__(self, data): #is run when an object is created
        self.data = data
        self.index = len(data)
    def __iter__(self):
        return self
    def __next__(self):
        if(self.index == 0):
            raise StopIteration
        self.index -= 1
        return self.data[self.index]
def main():
    rev = reverse('dog')
    for char in rev:
        print(char, end = '')
if __name__=='__main__':
    main()
class a:
    def fn(self):
        print('Hello')
b = a()
b.fn()
class a:
    def __init__(self, name):
        self.name = name
    def fn(self):
        print('Hello Im', self.name)
b = a('Virat')
b.fn()
class csstudent:
    stream = 'EEE'
    def __init__(self, roll):
        self.roll = roll
a = csstudent(101)
b = csstudent(102)
print(a.roll)
print(b.roll)
print(csstudent.stream)
class eestudent:
    stream = 'EEE'
    def __init__(self, roll):
        self.roll = roll
    def setAddress(self, address):
        self.address = address
    def getAddress(self):
        return self.address
a = eestudent(101)
a.setAddress('Bangalore')
print(a.getAddress())<jupyter_output>Bangalore<jupyter_text>Note: Empty class in python can be created using pass statement.<jupyter_code>class a:
    pass
class A:
    __a = 0 #hidden variable inside a class
    def inc(self, b):
        self.__a += b
        print(self.__a)
x = A()
x.inc(2)
print(x.__a)<jupyter_output>2<jupyter_text>Note: In the above program, we tried to access hidden variable outside the class using object and it threw an exception.<jupyter_code>#Accessing the value of hidden variable using tricky syntax.
class A: 
  
    __a = 10
   
x = A()      
print(x._A__a)<jupyter_output>10<jupyter_text>Note: We can access the value of hidden attribute by a tricky syntax as shown in the above example. Printing Objects: Printing objects gives us information about objects we are working with.<jupyter_code>class Test:
    def __init__(self, a, b):
        self.a = a
        self.b = b
    def __repr__(self):
        return 'Test a:%s b%s'%(self.a, self.b)
    def __str__(self):
        return "From str method of Test: a is %s, b is %s"%(self.a, self.b)
t = Test(1234, 5678)
print(t) #this calls __str__
print([t]) #this calls __repr__
#example for inheritence
class person:
    def __init__(self, name):
        self.name = name
    def getName(self):
        return self.name
    def isEmp(self):
        return False
class Employee(person):
    def isEmp(self):
        return True
emp = person('AB')
print(emp.getName(), emp.isEmp())

emp = Employee('Virat')
print(emp.getName(), emp.isEmp())<jupyter_output>AB False
Virat True<jupyter_text>Note: Issubclass() is a function that directly tells us if a class is subclass of another class.<jupyter_code>#example demonstrating above example
class base(object):
    pass
class derived(base):
    pass
print(issubclass(derived, base))
print(issubclass(base, derived))
d = derived()
b = base()
print(isinstance(d, base))
print(isinstance(b, derived))
#example demonstarting the multiple inheritance
class Base1(object):
    def __init__(self):
        self.str1 = 'Virat'
        print('Base1')
class Base2(object):
    def __init__(self):
        self.str2 = 'AB'
        print('Base2')
class Derived(Base1, Base2):
    def __init__(self):
        Base1.__init__(self)
        Base2.__init__(self)
        print('Derived')
    def dis(self):
        print(self.str1, self.str2)
d = Derived()
d.dis()<jupyter_output>Base1
Base2
Derived
Virat AB<jupyter_text>Polymorphism in Python Polymorphism means having many forms.  In programming, polymorphism means same function name (but different signatures) being used for different types.<jupyter_code>#example for inbuilt polymorphic functions.
print(len('Cricket'))
print(len([1, 2, 3])) #note: here len is used to find the length of both string as well as list.
#example for user-defined polymorphism.
def add(x, y, z = 0):
    return x+y+z
print(add(2, 3))
print(add(2, 3, 4))
#polymorphism with class methods.
class ind():
    def cap(self):
        print('Delhi is the capital of India')
    def lan(self):
        print('Hindi is the official language')
    def type(self):
        print('India is a developing country')
class usa():
    def cap(self):
        print('Washington D.C. is the capital of India')
    def lan(self):
        print('English is the primary language')
    def type(self):
        print('usa is a developed country')
in_ob = ind()
us_ob = usa()
for i in (in_ob, us_ob):
    i.cap()
    i.lan()
    i.type()<jupyter_output>Delhi is the capital of India
Hindi is the official language
India is a developing country
Washington D.C. is the capital of India
English is the primary language
usa is a developed country<jupyter_text>Note: In Python, Polymorphism lets us define methods in the child class that have the same name as the methods in the parent class. In inheritance, the child class inherits the methods from the parent class. However, it is possible to modify a method in a child class that it has inherited from the parent class. This is particularly useful in cases where the method inherited from the parent class doesn’t quite fit the child class. In such cases, we re-implement the method in the child class. This process of re-implementing a method in the child class is known as Method Overriding.<jupyter_code>#example polymorphism with inheritance.
class bird():
    def intro(self):
        print('There are many type of birds')
    def flight(self):
        print('Many birds can fly')
class sparrow(bird):
    def flight(self):
        print('Sparrow can fly')
class ostrich(bird):
    def flight(self):
        print('Ostrich can not fly')
ob_bi = bird()
ob_sp = sparrow()
ob_os = ostrich()

ob_bi.intro()
ob_bi.flight()

ob_sp.intro()
ob_sp.flight()

ob_os.intro()
ob_os.flight()
# example implementing polymorphism with function.
class ind():
    def cap(self):
        print('Delhi is the capital of India')
    def lan(self):
        print('Hindi is the official language')
    def type(self):
        print('India is a developing country')
class usa():
    def cap(self):
        print('Washington D.C. is the capital of India')
    def lan(self):
        print('English is the primary language')
    def type(self):
        print('usa is a developed country')
def fn(obj):
    obj.cap()
    obj.lan()
    obj.type()
ob_in = ind()
ob_us = usa()
fn(ob_in)
fn(ob_us)
from datetime import date
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    @classmethod
    def frombirthyear(cls, name, year):
        return cls(name, date.today().year-year)
    @staticmethod
    def isAdult(age):
        return age>18
person1 = Person('Virat', 30)
person2 = Person.frombirthyear('Virat', 1990)

print(person1.age)
print(person2.age)

print(Person.isAdult(30))
#example for changing the class member in python.
class CSSstudent:
    stream = 'cse'
    def __init__(self, name, roll):
        self.name = name
        self.roll = roll
a = CSSstudent('virat', 31)
b = CSSstudent('AB', 34)
print('Initially')
print(a.stream)
print(b.stream)
a.stream = 'ece' #you can not change class variable using object.
print('After changing')
print(a.stream)
print(b.stream)<jupyter_output>Initially
cse
cse
After changing
ece
cse<jupyter_text>Note: We can only change class variables using class name.<jupyter_code>#example demonstrating above note.
class CSSstudent:
    stream = 'cse'
    def __init__(self, name, roll):
        self.name = name
        self.roll = roll
a = CSSstudent('virat', 31)
print(a.stream)
CSSstudent.stream = 'ece'
b = CSSstudent('AB', 34)
print(b.stream)<jupyter_output>cse
ece<jupyter_text>Constructors in python Constructors are generally used for instantiating an object.The task of constructors is to initialize(assign values) to the data members of the class when an object of class is created.In Python the __init__() method is called the constructor and is always called when an object is created. There are two types of constructors in python.1. __Default__ Constructor.2. __Parameterized__ Constructor. The default constructor is simple constructor which doesn’t accept any arguments.It’s definition has only one argument which is a reference to the instance being constructed.Constructor with parameters is known as parameterized constructor.The parameterized constructor take its first argument as a reference to the instance being constructed known as self and the rest of the arguments are provided by the programmer.<jupyter_code>class A: 
    name = ""  
    def __init__(self): #default constructor
        self.name = "AB" 
    def print_name(self): 
        print(self.name)  
obj = A() 
obj.print_name() 
class Subtraction:
    first = 0
    second = 0
    answer = 0
    def __init__(self, f, s): #parameterized constructor
        self.first = f
        self.second = s
    def display(self):
        print('First = '+ str(self.first))
        print('Second = '+ str(self.second))
        print('Answer ='+str(self.answer))
    def calculate(self):
        self.answer = self.first - self.second
ob = Subtraction(2000, 1000)
ob.calculate()
ob.display()<jupyter_output>First = 2000
Second = 1000
Answer =1000<jupyter_text>Destructors in python Destrucors are called when an object gets destroyed.  In Python, destructors are not needed as much needed in C++ because Python has a garbage collector that handles memory management automatically.The _____del_____() method is known as destructor in python. It is called when all references to the object have been deleted i.e when an object is garbage collected.<jupyter_code>#example demonstrating usage of destructor in python.
class A:
    def __init__(self):
        print('Created')
    def __del__(self):
        print('Destructor is called')
a = A()
del a<jupyter_output>Created
Destructor is called<jupyter_text>Note : The destructor was called after the program ended or when all the references to object are deleted i.e when the reference count becomes zero, not when object went out of scope.<jupyter_code>class A:
    def __init__(self):
        print('Created')
    def __del__(self):
        print('Destructor called')
def make_ob():
    print('Making object...')
    ob = A()
    print('func end....')
    return ob
print('calling make_ob func...')
ob = make_ob()
print('Program end')<jupyter_output>calling make_ob func...
Making object...
Created
func end....
Program end<jupyter_text>str() v/s repr() in python str() and repr() are both used to string represention of an object.<jupyter_code>#example of str()
a = 'Hello World'
print(str(a))
print(str(230/13))
#example of repr()
b = 'Hello World'
print(repr(b))
print(repr(230/13))
import datetime
a = datetime.datetime.now()
print(str(a))
print(repr(a))
class complex:
    def __init__(self, real, imag):
        self.real = real
        self.imag = imag
    def __repr__(self):
        return 'Rational(%s, %s)'%(self.real, self.imag)
    def __str__(self):
        return '%s + i%s'%(self.real, self.imag)
t = complex(10, 20)
print(str(t))
print(repr(t))
#examples using type function.
a = 3
print(type(a))
l = [1, 2, 3]
print(type(l))
b = 'hello'
print(type(b))<jupyter_output><class 'int'>
<class 'list'>
<class 'str'><jupyter_text>Note: Every type in Python is defined by Class. So in above example, unlike C or Java where int, char, float are primary data types, in Python they are object of int class or str class. So we can make a new type by creating a class of that type. For example we can create a new type Student by creating Student class.<jupyter_code>#example.
class student:
    pass
st_ob = student()
print(type(st_ob))<jupyter_output><class '__main__.student'><jupyter_text>Class and Instance Attribute in Python. Class Attributes belongs to class instance, they will be shared by all the instances. Such attributes are defined in the class body.<jupyter_code>class sampleclass:
    count = 0
    def increase(self):
        sampleclass.count += 1
s1 = sampleclass()
s1.increase()
print(s1.count)
s2 = sampleclass()
s2.increase()
print(s2.count)
print(sampleclass.count)<jupyter_output>1
2
2<jupyter_text>Instance Attributes: Unlike class attributes instance attributes are not shared by all the objects. Every object has it's own copy of instance attribute.To list the attributes of an object we have two functions:1.vars() : This function displays the object attributes in dictionary format.2.dir() : Whereas this function displays more attributes than vars function. This displays class attributes as well.<jupyter_code>class emp:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    def show(self):
        print(self.name)
        print(self.age)
e1 = emp('XYZ', 20)
print(vars(e1))
print(dir(e1))<jupyter_output>{'name': 'XYZ', 'age': 20}
['__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', 'name', 'show']<jupyter_text>Reflection in Python. Reflection refers to the ability for code to be able to examine attributes about objects that might be passed as parameters to a function. For example, if we write type(obj) then Python will return an object which represents the type of obj.Using reflection, we can write one recursive reverse function that will work for strings, lists, and any other sequence that supports slicing and concatenation. If an obj is a reference to a string, then Python will return the str type object. Further, if we write str() we get a string which is the empty string. In other words, writing str() is the same thing as writing “”. Likewise, writing list() is the same thing as writing [].<jupyter_code>def reverse(seq): 
    SeqType = type(seq) 
    emptySeq = SeqType() 
      
    if seq == emptySeq: 
        return emptySeq 
      
    restrev = reverse(seq[1:]) 
    first = seq[0:1] 
       
    result = restrev + first 
      
    return result 
  
print(reverse([1, 2, 3, 4])) 
print(reverse("HELLO"))<jupyter_output>[4, 3, 2, 1]
OLLEH<jupyter_text>Reflection-enabling functions: Include type(), isinstance(), getattr(), dir() and callable().Callable(): Any function that can be called. For an object, determines whether the object can be called. A class can be made callable by defining __class__() method. This method return True if the object appears to be callable, else it returns False.<jupyter_code>#example for callable().
x = 5
def fn():
    print('callable')
y = fn
if(callable(x)):
    print('True')
else:
    print('False')
if(callable(y)):
    print('True')
else:
    print('False')
#example for callable when used in OOPS.
class a:
    def __call__(self):
        print('Something')
print(callable(a))<jupyter_output>True<jupyter_text>Dir: This method try's to return the valid attributes of an object.If the object has __dir__() method, the method will be called and must return the list of attributes. If the object doesn’t have __dir()__ method, this method tries to find information from the __dict__ attribute (if defined), and from type object. In this case, the list returned from dir() may not be complete.<jupyter_code>#example.
a = [1, 2, 3, 4]
print(dir(a))

b = ['a', 'b']
print(dir(a))<jupyter_output>['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str[...]<jupyter_text>Getattr(): Will return the value of named attribute of an object. If not found, it returns the default value provided to the function. The getattr method takes three parameters object, name and default(optional).<jupyter_code>#example.
class employee:
    salary = 25000
    company = 'XYZ'
e = employee()
print(getattr(e, 'salary'))
print(e.salary)<jupyter_output>25000
25000<jupyter_text>Barrier Objects in Python. Some common function calls related to threading.Barrier class are:barrier.broken: Returns True if the barrier is in broken state.barrier.parties: Number of threads required to pass a barrier.barrier.abort(): Put the barrier into broken state. This causes any future calls to wait() to fail with the BrokenBarrierError.Abort function calls on barrier are often required to skip the conditions of deadlocking during program execution.barrier.reset(): Return the barrier to default, empty state.barrier.wait(): Pass the barrier. When all the threads party to the barrier have called this function, they are all released simultaneously. If a timeout is provided, it is used in preference to any that was supplied to the class constructor.The return value is an integer in the range 0 to parties – 1, different for each thread. If the call times out, the barrier is put into the broken state. This method may raise a BrokenBarrierError exception if the barrier is broken or reset while a thread is waiting.barrier.n_wait: The number of threads currently waiting in the barrier.<jupyter_code>import threading
barrier = threading.Barrier(3)
class thread(threading.Thread):
    def __init__(self, thread_ID):
        threading.Thread.__init__(self)
        self.thread_ID = thread_ID
    def run(self):
        print(str(self.thread_ID) + '\n')
        barrier.wait()
t1 = thread(100)
t2 = thread(101)
t1.start()
t2.start()
barrier.wait()
print('Exit\n')<jupyter_output>100

101

Exit<jupyter_text>Timer Objects in python. Timer objects are used to represent actions that needs to be scheduled to run after a certain instant of time. These objects get scheduled to run on a separate thread that carries out the action. However, the interval that a timer is initialized with might not be the actual instant when the action was actually performed by the interpreter because it is the responsibility of the thread scheduler to actually schedule the thread corresponding to the timer object.Timer is a sub class of Thread class defined in python. It is started by calling the start() function corresponding to the timer explicitly.<jupyter_code>#example for creating a timer object.
import threading
def fn():
    print('Hello there')
    
timer = threading.Timer(10.0, fn)
timer.start()
print('Exit')<jupyter_output>Exit
Hello there<jupyter_text>cancelling a timer: Stop the timer, and cancel the execution of the timer’s action. This will only work if the timer is still in its waiting stage.<jupyter_code>import threading
def fn():
    print('Hello There')
timer = threading.Timer(1.0, fn)
timer.start()
print('Cancelling Timer')
timer.cancel()
print('Exit')<jupyter_output>Cancelling Timer
Exit<jupyter_text>Garbage Collection in python. Python's memory allocation and deallocation is automatic. Python uses two stratergies to allocate and deallocate the memory.1.Reference counting.2.Garbage Collection. Prior to Python version 2.0, the Python interpreter only used reference counting for memory management. Reference counting works by counting the number of times an object is referenced by other objects in the system. When references to an object are removed, the reference count for an object is decremented. When the reference count becomes zero, the object is deallocated.<jupyter_code>#example for reference counting.
b = 9 #here 9 is an object and 'b' is reference to an object.
b = 4 #reference count of object 'b' becomes zero as the value of object changes.<jupyter_output><empty_output><jupyter_text>A reference cycle is created when there is no way that the refence count of the object can reach.Note: A reference cycle is created when there is no way the reference count of the object can reach. Reference cycles involving lists, tuples, instances, classes, dictionaries, and functions are common. The easiest way to create a reference cycle is to create an object which refers to itself.<jupyter_code>#example demonstarting above note.
def fn():
    x = []
    x.append(x)
fn()
#Ways to make an object eligible for garbage collection
#1.
x = []
x.append(1)
x.append(2)
del x<jupyter_output><empty_output><jupyter_text>2.Automatic Garbage Collection of Cycles: Because reference cycles take computational work to discover, garbage collection must be a scheduled activity. Python schedules garbage collection based upon a threshold of object allocations and object deallocations. When the number of allocations minus the number of deallocations is greater than the threshold number, the garbage collector is run. One can inspect the threshold for new objects (objects in Python known as generation 0 objects) by importing the gc module and asking for garbage collection thresholds.<jupyter_code>import gc
print(gc.get_threshold())<jupyter_output>(700, 10, 10)<jupyter_text>Here, the default threshold on the above system is 700. This means when the number of allocations vs. the number of deallocations is greater than 700 the automatic garbage collector will run. Thus any portion of your code which frees up large blocks of memory is a good candidate for running manual garbage collection. 3.Manual Garbage Collection: Invoking the garbage collector manually during the execution of a program can be a good idea on how to handle memory being consumed by reference cycles.<jupyter_code>import gc
collected = gc.collect()
print(collected)<jupyter_output>204
{'fraction_non_alphanumeric': 0.09869797721460126, 'fraction_numerical': 0.011198945981554678, 'mean_word_length': 4.1696714743589745, 'pattern_counts': {'":': 0, '<': 97, '<?xml version=': 0, '>': 98, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 1, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '30', 'dir': 'jupyter-structured-clean-dedup', 'id': '596084', 'license': 'Apache-2.0', 'n_tokens_mistral': 8032, 'n_tokens_neox': 7721, 'n_words': 3062, 'path': 'Oops.ipynb', 'repo_name': 'Raghu010/Learn-python-3.x', 'size': '25806'}