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