Python # Range and For for index in range(6): print(index) # Range function is used generate a sequence of integers index = range(10, -1, -1) # start, stop and step, stops at 0 not including -1 # set class provides a mapping of unique immutable elements # One use of set is to remove duplicate elements dup_list = ('c', 'd', 'c', 'e') beta = set(dup_list) uniq_list = list(beta) # dict class is an associative array of keys and values. keys must be unique immutable objects dict_syn = {'k1': 'v1', 'k2': 'v2'} dict_syn = dict(k1='v1', k2='v2') dict_syn['k3'] = 'v3' # adding new key value del(dict_syn['k3']) # delete key value print(dict_syn.keys()) # prints all keys print(dict_syn.values()) # prints all values # User Input name = input('Name :') # Functions * A function is a piece of code, capable of performing a similar task repeatedly. * It is defined using **def** keyword in python. def <function_name>(<parameter1>, <parameter2>, ...): 'Function documentation' function_body return <value> * Parameters, return expression and documentation string are optional. def square(n): "Returns Square of a given number" return n**2 print(square.__doc__) //prints the function documentation string * 4 types of arguments * Required Arguments: non-keyword arguments def showname(name, age) showname("Jack", 40) // name="Jack", age=40 showname(40, "Jack") // name=40, age="Jack" * Keyword Arguments: identified by paramater names def showname(name, age) showname(age=40, name="Jack") * Default Arguments: Assumes a default argument, if an arg is not passsed. def showname(name, age=50) showname("Jack") // name="Jack", age=50 showname(age=40,"Jack") // name="Jack", age=40 showname(name="Jack", age=40) // name="Jack", age=40 showname(name="Jack", 40) // Python does not allow passing non-keyword after keyword arg. This will fail. * Variable Length Arguments: Function preocessed with more arguments than specified while defining the function def showname(name, *vartuple, **vardict) # *vartuple = Variable non keyword argument which will be a tuple. Denoted by * # **vardict = Variable keyword argument which will be a dictionary. Denoted by ** showname("Jack") // name="Jack" showname("Jack", 35, 'M', 'Kansas') // name="Jack", *vartuple=(35, 'M', 'Kansas') showname("Jack", 35, city='Kansas', sex='M') // name="Jack", *vartuple=(35), **vardict={city='Kansas', sex='M'} # An Iterator is an object, which allows a programmer to traverse through all the elements of a collection, regardless of its specific implementation. x = [6, 3, 1] s = iter(x) print(next(s)) # -> 6 # List Comprehensions -> Alternative to for loops. * More concise, readable, efficient and mimic functional programming style. * Used to: Apply a method to all or specific elements of a list, and Filter elements of a list satisfying specific criteria. x = [6, 3, 1] y = [ i**2 for i in x ] # List Comprehension expression print(y) # -> [36, 9, 1] * Filter positive numbers (using for and if) vec = [-4, -2, 0, 2, 4] pos_elm = [x for x in vec if x >= 0] # Can be read as for every elem x in vec, filter x if x is greater than or equal to 0 print(pos_elm) # -> [0, 2, 4] * Applying a method to a list def add10(x): return x + 10 n = [34, 56, 75, 3] mod_n = [ add10(num) for num in n] print(mod_n) # A Generator is a function that produces a sequence of results instead of a single value def arithmatic_series(a, r): while a < 50: yield a # yield is used in place of return which suspends processing a += r s = arithmatic_series(3, 10) # Execution of further 'arithmetic series' can be resumed only by calling nextfunction again on generator 's' print(s) //Generator #output=3 print(next(s)) //Generator starts execution # output=13 print(next(s)) //resumed # output=23 # A Generator expresions are generator versions of list comprehensions. They return a generator instead of a list. x = [6, 3, 1] g = (i**2 for i in x) # generator expression print(next(g)) # -> 36 # Dictionary Comprehensions -> takes the form {key: value for (key, value) in iterable} myDict = {x: x**2 for x in [1,2,3,4,5]} print (myDict) # Output {1: 1, 2: 4, 3: 9, 4: 16, 5: 25} # Calculate the frequency of each identified unique word in the list words = ['Hello', 'Hi', 'Hello'] freq = { w:words.count(w) for w in words } print(freq) # Output {'Hello': 2, 'Hi': 1} Create the dictionary frequent_words, which filter words having frequency greater than one words = ['Hello', 'Hi', 'Hello'] freq = { w:words.count(w) for w in words if words.count(w) > 1 } print(freq) # Output {'Hello': 2} # Defining Classes * Syntax class <ClassName>(<parent1>, ... ): class_body # Creating Objects * An object is created by calling the class name followed by a pair of parenthesis. class Person: pass p1 = Person() # Creating the object 'p1' print(p1) # -> '<__main__.Person object at 0x0A...>' # tells you what class it belongs to and hints on memory address it is referenced to. # initializer method -> __init__ * defined inside the class and called by default, during an object creation. * It also takes self as the first argument, which refers to the current object. class Person: def __init__(self, fname, lname): self.fname = fname self.lname = lname p1 = Person('George', 'Smith') print(p1.fname, '-', p1.lname) # -> 'George - Smith' # Documenting a Class * Each class or a method definition can have an optional first line, known as docstring. class Person: 'Represents a person.' # Inheritance * Inheritance describes is a kind of relationship between two or more classes, abstracting common details into super class and storing specific ones in the subclass. * To create a child class, specify the parent class name inside the pair of parenthesis, followed by it's name. class Child(Parent): pass * Every child class inherits all the behaviours exhibited by their parent class. * In Python, every class uses inheritance and is inherited from **object** by default. class MySubClass(object): # object is known as parent or super class. pass # Inheritance in Action class Person: def __init__(self, fname, lname): self.fname = fname self.lname = lname class Employee(Person): all_employees = [] def __init__(self, fname, lname, empid): Person.__init__(self, fname, lname) # Employee class utilizes __init __ method of the parent class Person to create its object. self.empid = empid Employee.all_employees.append(self) e1 = Employee('Jack', 'simmons', 456342) print(e1.fname, '-', e1.empid) # Output -> Jack - 456342 # Polymorphism * Polymorphism allows a subclass to override or change a specific behavior, exhibited by the parent class class Employee(Person): all_employees = EmployeesList () def __init__(self, fname, lname, empid): Person.__init__(self, fname, lname) self.empid = empid Employee.all_employees.append(self) def getSalary(self): return 'You get Monthly salary.' def getBonus(self): return 'You are eligible for Bonus.' * Definition of ContractEmployee class derived from Employee. It overrides functionality of getSalary and getBonus methods found in it's parent class Employee. class ContractEmployee(Employee): def getSalary(self): return 'You will not get Salary from Organization.' def getBonus(self): return 'You are not eligible for Bonus.' e1 = Employee('Jack', 'simmons', 456342) e2 = ContractEmployee('John', 'williams', 123656) print(e1.getBonus()) # Output - You are eligible for Bonus. print(e2.getBonus()) # Output - You are not eligible for Bonus. # Abstraction * Abstraction means working with something you know how to use without knowing how it works internally. * It is hiding the defaults and sharing only necessary information. # Encapsulation * Encapsulation allows binding data and associated methods together in a unit i.e class. * Bringing related data and methods inside a class to avoid misuse outside. * These principles together allows a programmer to define an interface for applications, i.e. to define all tasks the program is capable to execute and their respective input and output data. * A good example is a television set. We don’t need to know the inner workings of a TV, in order to use it. All we need to know is how to use the remote control (i.e the interface for the user to interact with the TV). # Abstracting Data * Direct access to data can be restricted by making required attributes or methods private, **just by prefixing it's name with one or two underscores.** * An attribute or a method starting with: + **no underscores** is a **public** one. + **a single underscore** is **private**, however, still accessible from outside. + **double underscores** is **strongly private** and not accessible from outside. # Abstraction and Encapsulation Example * **empid** attribute of Employee class is made private and is accessible outside the class only using the method **getEmpid**. class Employee(Person): all_employees = EmployeesList() def __init__(self, fname, lname, empid): Person.__init__(self, fname, lname) self.__empid = empid Employee.all_employees.append(self) def getEmpid(self): return self.__empid e1 = Employee('Jack', 'simmons', 456342) print(e1.fname, e1.lname) # Output -> Jack simmons print(e1.getEmpid()) # Output -> 456342 print(e1.__empid) # Output -> AttributeError: Employee instance has no attribute '__empid' # Exceptions * Python allows a programmer to handle such exceptions using **try ... except** clauses, thus avoiding the program to crash. * Some of the python expressions, though written correctly in syntax, result in error during execution. **Such scenarios have to be handled.** * In Python, every error message has two parts. The first part tells what type of exception it is and second part explains the details of error. # Handling Exception * A try block is followed by one or more except clauses. * The code to be handled is written inside try clause and the code to be executed when an exception occurs is written inside except clause. try: a = pow(2, 4) print("Value of 'a' :", a) b = pow(2, 'hello') # results in exception print("Value of 'b' :", b) except TypeError as e: print('oops!!!') print('Out of try ... except.') Output -> Value of 'a' : 16 --> oops!!! --> Out of try ... except. # Raising Exceptions * **raise** keyword is used when a programmer wants a specific exception to occur. try: a = 2; b = 'hello' if not (isinstance(a, int) and isinstance(b, int)): raise TypeError('Two inputs must be integers.') c = a**b except TypeError as e: print(e) # User Defined Exception Functions * Python also allows a programmer to create custom exceptions, derived from base Exception class. class CustomError(Exception): def __init__(self, value): self.value = value def __str__(self): return str(self.value) try: a = 2; b = 'hello' if not (isinstance(a, int) and isinstance(b, int)): raise CustomError('Two inputs must be integers.') # CustomError is raised in above example, instead of TypeError. c = a**b except CustomError as e: print(e) # Using 'finally' clause * **finally** clause is an optional one that can be used with try ... except clauses. * All the statements under finally clause are executed irrespective of exception occurrence. def divide(a,b): try: result = a / b return result except ZeroDivisionError: print("Dividing by Zero.") finally: print("In finally clause.") # Statements inside finally clause are ALWAYS executed before the return back # Using 'else' clause * **else** clause is also an optional clause with try ... except clauses. * Statements under else clause are executed **only when no exception occurs in try clause**. try: a = 14 / 7 except ZeroDivisionError: print('oops!!!') else: print('First ELSE') try: a = 14 / 0 except ZeroDivisionError: print('oops!!!') else: print('Second ELSE') Output: First ELSE --> oops!!! # Module * Any file containing logically organized Python code can be used as a module. * A module generally contains **any of the defined functions, classes and variables**. A module can also include executable code. * Any Python source file can be used as a module by using an import statement in some other Python source file. # Packages * A package is a collection of modules present in a folder. * The name of the package is the name of the folder itself. * A package generally contains an empty file named **__init__.py** in the same folder, which is required to treat the folder as a package. # Import Modules import math # Recommended method of importing a module import math as m from math import pi, tan from math import pi as pie, tan as tangent # Working with Files * Data from an opened file can be read using any of the methods: **read, readline and readlines**. * Data can be written to a file using either **write** or **writelines** method. * A file **must be opened**, before it is used for reading or writing. fp = open('temp.txt', 'r') # opening ( operations 'r' & 'w') content = fp.read() # reading fp.close() # closing # read() -> Reads the entire contents of a file as bytes. # readline() -> Reads a single line at a time. # readlines() -> Reads a all the line & each line is stored as an element of a list. # write() -> Writes a single string to output file. # writelines() -> Writes multiple lines to output file & each string is stored as an element of a list. * Reading contents of file and storing as a dictionary fp = open('emp_data.txt', 'r') emps = fp.readlines() # Preprocessing data emps = [ emp.strip('\n') for emp in emps ] emps = [ emp.split(';') for emp in emps ] header = emps.pop # remove header record separately emps = [ dict(zip(header, emp) for emp in emps ] # header record is used to combine with data to form a dictionary print(emps[:2]) # prints first 2 records * Filtering data based on criteria fil_emps = [emp['Emp_name'] for emp in emps if emp['Emp_work_location']=='HYD'] * Filtering data based on pattern import re pattern = re.compile(r'oracle', re.IGNORECASE) # Regular Expression oracle_emps = [emp['Emp_name'] for emp in emps if pattern.search(emp['Emp_skillset'])] * Filter and Sort data in ascending order fil_emps = [emp for emp in emps if emp['Emp_designation']=='ASE'] fil_emps = sorted(fil_emps, key=lambda k: k['Emp_name']) print(emp['Emp_name'] for emp in fil_emps ) * Sorting all employees based on custom sorting criteria order = {'ASE': 1, 'ITA': 2, 'AST': 3} sorted_emp = sorted(emp, key=lambda k: order[k['designation']]) * Filter data and write into files fil_emps = [emp for emp in emps if emp['Emp_Designation'] == 'ITA'] ofp = open(outputtext.txt, 'w') keys = fil_emps[0].keys() # Remove header from key name for key in keys: ofp.write(key+"\t") ofp.write("\n") for emp in fil_emps: for key in keys: ofp.write(emp[key]+"\t") ofp.write("\n") ofp.close() # Regular Expressions * Regex are useful to construct patterns that helps in filtering the text possessing the pattern. * **re module** is used to deal with regex. * **search** method takes pattern and text to scan and returns a Match object. Return None if not found. * Match object holds info on the nature of the match like **original input string, Regular expression used, location within the original string** match = re.search(pattern, text) start_index = match.start() # start location of match end_index = match.end() regex = match.re.pattern() print('Found "{}" pattern in "{}" from {} to {}'.format(st, text, start_index, end_index)) # Compiling Expressions * In Python, its more efficient t compile the patterns that are frequently used. * **compile** function of re module converts an expression string into a **RegexObject**. patterns = ['this', 'that'] regexes = [re.compile(p) for p in patterns] for regex in regexes: if regex.search(text): # pattern is not required print('Match found') * search method only returns the first matching occurrence. # Finding Multiple Matches * findall method returns all the substrings of the pattern without overlapping pattern= 'ab' for match in re.findall(pattern, text): print('match found - {}'.format(match)) # Grouping Matches * Adding groups to a pattern enables us to isolate parts of the matching text, expanding those capabilities to create a parser. * Groups are defined by enclosing patterns within parenthesis text= 'This is some text -- with punctuations.' for pattern in [r'^(\w+)', # word at the start of the string r'(\w+)\S*$', # word at the end of the string with punctuation r'(\bt\w+)\W+(\w+)', # word staring with 't' and the next word r'(\w+t)\b']: # word ending with t regex = re.compile(pattern) match = regex.search(text) print(match.groups()) # Output -> ('This',) ('punctuations',) ('text','with') ('text',) # Naming Grouped Matches * Accessing the groups with defined names text= 'This is some text -- with punctuations.' for pattern in [r'^(?P<first_word>\w+)', # word at the start of the string r'(?P<last_word>\w+)\S*$', # word at the end of the string with punctuation r'(?P<t_word>\bt\w+)\W+(?P<other_word>\w+)', # word staring with 't' and the next word r'(?P<ends_with_t>\w+t)\b']: # word ending with t regex = re.compile(pattern) match = regex.search(text) print("Groups: ",match.groups()) # Output -> ('This',) ('punctuations',) ('text','with') ('text',) print("Group Dictionary: ",match.groupdict()) # Output -> {'first_word':'This'} {'last_word': 'punctuations'} {'t_word':'text', 'other_word':'with'} {'ends_with_t':'text'} # Data Handling # Handling XML files * **lxml** 3rd party module is a highly feature rich with ElementTree API and supports querying wthe xml content using XPATH. * In the ElementTree API, an element acts like a list. The items of the list are the elements children. * XML search is faster in lxml. <?xml> <employee> <skill name="Python"/> </employee> from lxml import etree tree = etree.parse('sample.xml') root = tree.getroot() # gets doc root <?xml> skills = tree.findall('//skill') # gets all skill tags for skill in skills: print("Skills: ", skill.attrib['name']) # Adding new skill in the xml skill = etree.SubElement(root, 'skill', attrib={'name':'PHP'}) # Handling HTML files * **lxml** 3rd party module is used for parsing HTML files as well. import urllib.request from lxml import etree def readURL(url): urlfile = urllib.request.urlopen(url) if urlfile.getcode() == 200: contents = urlfile.read() return contents if __name__ == '__main__': url = 'http://xkcd.com' html = readURL(url) # Data Serialization * Process of converting **data types/objects** into **Transmittable/Storable** format is called Data Serialization. * In python, **pickle and json** modules are used for Data Serialization. * Serialized data can then be written to file/Socket/Pipe. From these it can be de-serialized and stored into a new Object. json.dump(data, file, indent=2) # serialized data is written to file with indentation using dump method data_new = json.load(file) # de-serialized data is written to new object using load method # Database Connectivity * **Python Database API (DB-API)** is a standard interface to interact with various databases. * Different DB API’s are used for accessing different databases. Hence a programmer has to install DB API corresponding to the database one is working with. * Working with a database includes the following steps: + Importing the corresponding DB-API module. + Acquiring a connection with the database. + Executing SQL statements and stored procedures. + Closing the connection import sqlite3 # establishing a database connection con = sqlite3.connect('D:\\TEST.db') # preparing a cursor object cursor = con.cursor() # preparing sql statements sql1 = 'DROP TABLE IF EXISTS EMPLOYEE' # closing the database connection con.close() # Inserting Data * Single rows are inserted using **execute** and multiple rows using **executeMany** method of created cursor object. # preparing sql statement rec = (456789, 'Frodo', 45, 'M', 100000.00) sql = ''' INSERT INTO EMPLOYEE VALUES ( ?, ?, ?, ?, ?) ''' # executing sql statement using try ... except blocks try: cursor.execute(sql, rec) con.commit() except Exception as e: print("Error Message :", str(e)) con.rollback() # Fetching Data * **fetchone**: It retrieves one record at a time in the form of a tuple. * **fetchall**: It retrieves all fetched records at a point in the form of tuple of tuples. # fetching the records records = cursor.fetchall() # Displaying the records for record in records: print(record) # Object Relational Mappers * An object-relational mapper (ORM) is a library that automates the transfer of data stored in relational database tables into objects that are adopted in application code. * ORMs offer a high-level abstraction upon a relational database, which permits a developer to write Python code rather than SQL to create, read, update and delete data and schemas in their database. * Such an ability to write Python code instead of SQL speeds up web application development. # Higher Order Functions * A **Higher Order function** is a function, which is capable of doing any one of the following things: + It can be functioned as a **data** and be assigned to a variable. + It can accept any other **function as an argument**. + It can return a **function as its result**. *The ability to build Higher order functions, **allows a programmer to create Closures, which in turn are used to create Decorators**. # Function as a Data def greet(): return 'Hello Everyone!' print(greet()) wish = greet # 'greet' function assigned to variable 'wish' print(type(wish)) # Output -> <type 'function'> print(wish()) # Output -> Hello Everyone! # Function as an Argument def add(x, y): return x + y def sub(x, y): return x - y def prod(x, y): return x * y def do(func, x, y): return func(x, y) print(do(add, 12, 4)) # 'add' as arg # Output -> 16 print(do(sub, 12, 4)) # 'sub' as arg # Output -> 8 print(do(prod, 12, 4)) # 'prod' as arg # Output -> 48 # Returning a Function def outer(): def inner(): s = 'Hello world!' return s return inner() print(outer()) # Output -> Hello world! * You can observe from the output that the **return value of 'outer' function is the return value of 'inner' function** i.e 'Hello world!'. def outer(): def inner(): s = 'Hello world!' return s return inner # Removed '()' to return 'inner' function itself print(outer()) #returns 'inner' function # Output -> <function inner at 0xxxxxx> func = outer() print(type(func)) # Output -> <type 'function'> print(func()) # calling 'inner' function # Output -> Hello world! * Parenthesis after the **inner** function are removed so that the **outer** function returns **inner function**. # Closures * A Closure is a **function returned by a higher order function**, whose return value depends on the data associated with the higher order function. def multiple_of(x): def multiple(y): return x*y return multiple c1 = multiple_of(5) # 'c1' is a closure c2 = multiple_of(6) # 'c2' is a closure print(c1(4)) # Output -> 5 * 4 = 20 print(c2(4)) # Output -> 6 * 4 = 24 * The first closure function, c1 binds the value 5 to argument x and when called with an argument 4, it executes the body of multiple function and returns the product of 5 and 4. * Similarly c2 binds the value 6 to argument x and when called with argument 4 returns 24. # Decorators * Decorators are evolved from the concept of closures. * A decorator function is a higher order function that takes a function as an argument and returns the inner function. * A decorator is capable of adding extra functionality to an existing function, without altering it. * The decorator function is prefixed with **@ symbol** and written above the function definition. + Shows the creation of closure function wish using the higher order function outer. def outer(func): def inner(): print("Accessing :", func.__name__) return func() return inner def greet(): print('Hello!') wish = outer(greet) # Output -> Accessing : greet wish() # Output -> Hello! - wish is the closure function obtained by calling an outer function with the argument greet. When wish function is called, inner function gets executed. + The second one shows the creation of decorator function outer, which is used to decorate function greet. def outer(func): def inner(): print("Accessing :", func.__name__) return func() return inner def greet(): return 'Hello!' greet = outer(greet) # decorating 'greet' # Output -> No Output as return is used instead of print greet() # calling new 'greet' # Output -> Accessing : greet - The function returned by outer is assigned to greet i.e the function name passed as argument to outer. This makes outer a decorator to greet. + Third one displays decorating the greet function with decorator function, outer, using @ symbol. def outer(func): def inner(): print("Accessing :", func.__name__) return func() return inner @outer # This is same as **greet = outer(greet)** def greet(): return 'Hello!' greet() # Output -> Accessing : greet # Descriptors * Python descriptors allow a programmer to create managed attributes. * In other object-oriented languages, you will find **getter and setter** methods to manage attributes. * However, Python allows a programmer to manage the attributes simply with the attribute name, without losing their protection. * This is achieved by defining a **descriptor class**, that implements any of **__get__, __set__, __delete__** methods. class EmpNameDescriptor: def __get__(self, obj, owner): return self.__empname def __set__(self, obj, value): if not isinstance(value, str): raise TypeError("'empname' must be a string.") self.__empname = value * The descriptor, EmpNameDescriptor is defined to manage empname attribute. It checks if the value of empname attribute is a string or not. class EmpIdDescriptor: def __get__(self, obj, owner): return self.__empid def __set__(self, obj, value): if hasattr(obj, 'empid'): raise ValueError("'empid' is read only attribute") if not isinstance(value, int): raise TypeError("'empid' must be an integer.") self.__empid = value * The descriptor, EmpIdDescriptor is defined to manage empid attribute. class Employee: empid = EmpIdDescriptor() empname = EmpNameDescriptor() def __init__(self, emp_id, emp_name): self.empid = emp_id self.empname = emp_name * Employee class is defined such that, it creates empid and empname attributes from descriptors EmpIdDescriptor and EmpNameDescriptor. e1 = Employee(123456, 'John') print(e1.empid, '-', e1.empname) # Output -> '123456 - John' e1.empid = 76347322 # Output -> ValueError: 'empid' is read only attribute # Properties * Descriptors can also be created using property() type. + Syntax: property(fget=None, fset=None, fdel=None, doc=None) - where, fget : attribute get method fset : attribute set method fdel – attribute delete method doc – docstring class Employee: def __init__(self, emp_id, emp_name): self.empid = emp_id self.empname = emp_name def getEmpID(self): return self.__empid def setEmpID(self, value): if not isinstance(value, int): raise TypeError("'empid' must be an integer.") self.__empid = value empid = property(getEmpID, setEmpID) # Property Decorators * Descriptors can also be created with property decorators. * While using property decorators, an attribute's get method will be same as its name and will be decorated with property. * In a case of defining any set or delete methods, they will be decorated with respective setter and deleter methods. class Employee: def __init__(self, emp_id, emp_name): self.empid = emp_id self.empname = emp_name @property def empid(self): return self.__empid @empid.setter def empid(self, value): if not isinstance(value, int): raise TypeError("'empid' must be an integer.") self.__empid = value e1 = Employee(123456, 'John') print(e1.empid, '-', e1.empname) # Output -> '123456 - John' # Introduction to Class and Static Methods Based on the **scope**, functions/methods are of two types. They are: * Class methods * Static methods # Class Methods * A method defined inside a class is bound to its object, by default. * However, if the method is bound to a Class, then it is known as **classmethod**. class Circle(object): no_of_circles = 0 def __init__(self, radius): self.__radius = radius Circle.no_of_circles += 1 def getCirclesCount(self): return Circle.no_of_circles c1 = Circle(3.5) c2 = Circle(5.2) c3 = Circle(4.8) print(c1.getCirclesCount()) # -> 3 print(Circle.getCirclesCount(c3)) # -> 3 print(Circle.getCirclesCount()) # -> TypeError: getCirclesCount() missing 1 required positional argument: 'self' class Circle(object): no_of_circles = 0 def __init__(self, radius): self.__radius = radius Circle.no_of_circles += 1 @classmethod def getCirclesCount(self): return Circle.no_of_circles c1 = Circle(3.5) c2 = Circle(5.2) c3 = Circle(4.8) print(c1.getCirclesCount()) # -> 3 print(Circle.getCirclesCount()) # -> 3 # Static Method * A method defined inside a class and not bound to either a class or an object is known as **Static** Method. * Decorating a method using **@staticmethod** decorator makes it a static method. def square(x): return x**2 class Circle(object): def __init__(self, radius): self.__radius = radius def area(self): return 3.14*square(self.__radius) c1 = Circle(3.9) print(c1.area()) # -> 47.7594 print(square(10)) # -> 100 * square function is not packaged properly and does not appear as integral part of class Circle. class Circle(object): def __init__(self, radius): self.__radius = radius @staticmethod def square(x): return x**2 def area(self): return 3.14*self.square(self.__radius) c1 = Circle(3.9) print(c1.area()) # -> 47.7594 print(square(10)) # -> NameError: name 'square' is not defined * square method is no longer accessible from outside the class Circle. * However, it is possible to access the static method using Class or the Object as shown below. print(Circle.square(10)) # -> 100 print(c1.square(10)) # -> 100 # Abstract Base Classes * An **Abstract Base Class** or **ABC** mandates the derived classes to implement specific methods from the base class. * It is not possible to create an object from a defined ABC class. * Creating objects of derived classes is possible only when derived classes override existing functionality of all abstract methods defined in an ABC class. * In Python, an Abstract Base Class can be created using module abc. from abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def area(self): pass @abstractmethod def perimeter(self): pass * Abstract base class Shape is defined with two abstract methods area and perimeter. class Circle(Shape): def __init__(self, radius): self.__radius = radius @staticmethod def square(x): return x**2 def area(self): return 3.14*self.square(self.__radius) def perimeter(self): return 2*3.14*self.__radius c1 = Circle(3.9) print(c1.area()) # -> 47.7594 # Context Manager * A Context Manager allows a programmer to perform required activities, automatically, while entering or exiting a Context. * For example, opening a file, doing few file operations, and closing the file is manged using Context Manager as shown below. with open('sample.txt', 'w') as fp: content = fp.read() * The keyword **with** is used in Python to enable a context manager. It automatically takes care of closing the file. import sqlite3 class DbConnect(object): def __init__(self, dbname): self.dbname = dbname def __enter__(self): self.dbConnection = sqlite3.connect(self.dbname) return self.dbConnection def __exit__(self, exc_type, exc_val, exc_tb): self.dbConnection.close() with DbConnect('TEST.db') as db: cursor = db.cursor() ''' Few db operations ... ''' * Example from contextlib import contextmanager @contextmanager def context(): print('Entering Context') yield print("Exiting Context") with context(): print('In Context') # Output -> Entering Context -> In Context -> Exiting Context # Coroutines * A Coroutine is **generator** which is capable of constantly receiving input data, process input data and may or may not return any output. * Coroutines are majorly used to build better **Data Processing Pipelines**. * Similar to a generator, execution of a coroutine stops when it reaches **yield** statement. * A Coroutine uses **send** method to send any input value, which is captured by yield expression. def TokenIssuer(): tokenId = 0 while True: name = yield tokenId += 1 print('Token number of', name, ':', tokenId) t = TokenIssuer() next(t) t.send('George') # -> Token number of George: 1 t.send('Rosy') # -> Token number of Rosy: 2 * **TokenIssuer** is a coroutine function, which uses yield to accept name as input. * Execution of coroutine function begins only when next is called on coroutine t. * This results in the execution of all the statements till a yield statement is encountered. * Further execution of function resumes when an input is passed using send, and processes all statements till next yield statement. def TokenIssuer(tokenId=0): try: while True: name = yield tokenId += 1 print('Token number of', name, ':', tokenId) except GeneratorExit: print('Last issued Token is :', tokenId) t = TokenIssuer(100) next(t) t.send('George') # Token number of George: 101 t.send('Rosy') # Token number of Rosy: 102 t.send('Smith') # Token number of Smith: 103 t.close() # Last issued Token is: 103 * The coroutine function TokenIssuer takes an argument, which is used to set a starting number for tokens. * When coroutine t is closed, statements under GeneratorExit block are executed. * Many programmers may forget that passing input to coroutine is possible only after the first next function call, which results in error. * Such a scenario can be avoided using a decorator. def coroutine_decorator(func): def wrapper(*args, **kwdargs): c = func(*args, **kwdargs) next(c) return c return wrapper @coroutine_decorator def TokenIssuer(tokenId=0): try: while True: name = yield tokenId += 1 print('Token number of', name, ':', tokenId) except GeneratorExit: print('Last issued Token is :', tokenId) t = TokenIssuer(100) t.send('George') t.send('Rosy') t.send('Smith') t.close() * coroutine_decorator takes care of calling next on the created coroutine t. def nameFeeder(): while True: fname = yield print('First Name:', fname) lname = yield print('Last Name:', lname) n = nameFeeder() next(n) n.send('George') n.send('Williams') n.send('John') First Name: George Last Name: Williams First Name: John