Python

Key features

  • Interpreted language. Does not need to be compiled before it is run.

  • Dynamically typed. No need to state the types of variables when you declare them. You can do x=111 and then x="I'm a string".

  • Object orientated programming. It allows the definition of classes along with composition and inheritance. Python does not have access specifiers (like C++’s public, private): “we are all adults here”.

  • Functions are first-class objects. They can be assigned to variables, returned from other functions and passed into functions. Classes are also first class objects.

  • Writing Python code is quick but running it is often slower than compiled languages. Fortunately,Python allows the inclusion of C based extensions so bottlenecks can be optimized away and often are. The numpypackage is a good example of this, it’s really quite quick because a lot of the number crunching it does isn’t actually done by Python

  • Use in many spheres – web applications, automation, scientific modeling, big data applications and many more.

Built-In Types

  • Immutable built-in datatypes:

    • Numbers

    • Strings

    • Tuples

  • Mutable built-in datatypes:

    • List

    • Dictionaries

    • Sets

*args and **kwargs

*args when we aren’t sure how many arguments are going to be passed to a function, or if we want to pass a stored list or tuple of arguments to a function.

It gives us the ability to pass N (variable) number of arguments.

This type of argument syntax doesn’t allow passing a named argument to the function.

Example of using the *args:

# Python code to demonstrate 
# *args for dynamic arguments 
def fn(*argList):  
    for argx in argList:  
        print (argx) 
    
fn('I', 'am', 'Learning', 'Python')

Output:

I
am
Learning
Python

**kwargsis used when we don’t know how many keyword arguments will be passed to a function, or it can be used to pass the values of a dictionary as keyword arguments.

It lets us pass N (variable) number of arguments which can be named or keyworded.

Example of using the **kwargs:

# Python code to demonstrate 
# **kwargs for dynamic + named arguments 
def fn(**kwargs):  
    for emp, age in kwargs.items(): 
        print ("%s's age is %s." %(emp, age)) 
    
fn(John=25, Kalley=22, Tom=32)

Output:

John's age is 25.
Kalley's age is 22.
Tom's age is 32.

The identifiers args and kwargs are a convention, you could also use *bob and **billy but that would not be wise.

def f(*args,**kwargs): print(args, kwargs)

l = [1,2,3]
t = (4,5,6)
d = {'a':7,'b':8,'c':9}

f()
f(1,2,3)                    # (1, 2, 3) {}
f(1,2,3,"groovy")           # (1, 2, 3, 'groovy') {}
f(a=1,b=2,c=3)              # () {'a': 1, 'c': 3, 'b': 2}
f(a=1,b=2,c=3,zzz="hi")     # () {'a': 1, 'c': 3, 'b': 2, 'zzz': 'hi'}
f(1,2,3,a=1,b=2,c=3)        # (1, 2, 3) {'a': 1, 'c': 3, 'b': 2}

f(*l,**d)                   # (1, 2, 3) {'a': 7, 'c': 9, 'b': 8}
f(*t,**d)                   # (4, 5, 6) {'a': 7, 'c': 9, 'b': 8}
f(1,2,*t)                   # (1, 2, 4, 5, 6) {}
f(q="winning",**d)          # () {'a': 7, 'q': 'winning', 'c': 9, 'b': 8}
f(1,2,*t,q="winning",**d)   # (1, 2, 4, 5, 6) {'a': 7, 'q': 'winning', 'c': 9, 'b': 8}

def f2(arg1,arg2,*args,**kwargs): print(arg1,arg2, args, kwargs)

f2(1,2,3)                       # 1 2 (3,) {}
f2(1,2,3,"groovy")              # 1 2 (3, 'groovy') {}
f2(arg1=1,arg2=2,c=3)           # 1 2 () {'c': 3}
f2(arg1=1,arg2=2,c=3,zzz="hi")  # 1 2 () {'c': 3, 'zzz': 'hi'}
f2(1,2,3,a=1,b=2,c=3)           # 1 2 (3,) {'a': 1, 'c': 3, 'b': 2}

f2(*l,**d)                   # 1 2 (3,) {'a': 7, 'c': 9, 'b': 8}
f2(*t,**d)                   # 4 5 (6,) {'a': 7, 'c': 9, 'b': 8}
f2(1,2,*t)                   # 1 2 (4, 5, 6) {}
f2(1,1,q="winning",**d)      # 1 1 () {'a': 7, 'q': 'winning', 'c': 9, 'b': 8}
f2(1,2,*t,q="winning",**d)   # 1 2 (4, 5, 6) {'a': 7, 'q': 'winning', 'c': 9, 'b': 8}

When to use them?

  • Function decorators.

  • Monkey patching (modifying some code at runtime). Example:

import someclass

def get_info(self, *args):
    return "Test data"

someclass.get_info = get_info

lists versus tuples

Lists are mutable i.e they can be edited.

Tuples are immutable (tuples are lists which can’t be edited).

Lists are slower than tuples.

Tuples are faster than lists.

Syntax: list_1 = [10, ‘Chelsea’, 20]

Syntax: tup_1 = (10, ‘Chelsea’ , 20)

How is Multithreading achieved in Python?

Python doesn't allow multi-threading in the truest sense of the word.

  1. Python has a multi-threading package but if you want to multi-thread to speed your code up, then it’s usually not a good idea to use it.

  2. Python has a construct called the Global Interpreter Lock (GIL). The GIL makes sure that only one of your ‘threads’ can execute at any one time. A thread acquires the GIL, does a little work, then passes the GIL onto the next thread.

  3. This happens very quickly so to the human eye it may seem like your threads are executing in parallel, but they are really just taking turns using the same CPU core.

  4. All this GIL passing adds overhead to execution. This means that if you want to make your code run faster then using the threading package often isn’t a good idea.

range() vs xrange()

For the most part, xrange and range are the exact same in terms of functionality. They both provide a way to generate a list of integers for you to use, however. The only difference is that range returns a Python list object and x range returns an xrange object.

This means that xrange doesn’t actually generate a static list at run-time like range does. It creates the values as you need them with a special technique called yielding. This technique is used with a type of object known as generators. That means that if you have a really gigantic range you’d like to generate a list for, say one billion, xrange is the function to use.

Pickling / Unpickling

Pickling: Pickle module accepts any Python object and converts it into a string representation and dumps it into a file by using dump function.

Unpickling: the process of retrieving original Python objects from the stored string representation.

How are arguments passed by value or by reference?

Everything in Python is an object and all variables hold references to the objects. The references values are according to the functions; as a result you cannot change the value of the references. However, you can change the objects if it is mutable.

Call By Value

In call-by-value, the argument, whether an expression or a value, gets bound to the respective variable in the function.

Python will treat that variable as local in the function-level scope. Any changes made to that variable will remain local and will not reflect outside the function.

Call By Reference

We use both “call-by-reference” and “pass-by-reference” interchangeably. When we pass an argument by reference, then it is available as an implicit reference to the function, rather than a simple copy. In such a case, any modification to the argument will also be visible to the caller.

Advantage: time and space efficiency because it leaves the need for creating local copies.

Disadvantage: a variable can get changed accidentally during a function call.

Namespaces

A collection of names. It maps names to corresponding objects. When different namespaces contain objects with the same names, this avoids any name collisions. Internally, a namespace is implemented as a Python dictionary.

On starting the interpreter, it creates a namespace for as long as we don’t exit.

We have:

  • local namespaces

  • global namespaces

  • built-in namespaces

Inheritance

OOP mechanism which allows an object to access its parent class features. It carries forward the base class functionality to the child.

We do it intentionally to abstract away the similar code in different classes.

The common code rests with the base class, and the child class objects can access it via inheritance. Example:

class PC: # Base class
    processor = "Xeon" # Common attribute
    def set_processor(self, new_processor):
        processor = new_processor

class Desktop(PC): # Derived class
    os = "Mac OS High Sierra" # Personalized attribute
    ram = "32 GB"

class Laptop(PC): # Derived class
    os = "Windows 10 Pro 64" # Personalized attribute
    ram = "16 GB"

desk = Desktop()
print(desk.processor, desk.os, desk.ram)

lap = Laptop()
print(lap.processor, lap.os, lap.ram)

Output:

Xeon Mac OS High Sierra 32 GB
Xeon Windows 10 Pro 64 16 GB

Multiple inheritance and MRO

MRO (Method Resolution Order): When we search for an attribute in a class that is involved in python multiple inheritance, an order is followed.

First, it is searched in the current class. If not found, the search moves to parent classes. This is left-to-right, depth-first.

Example:

class A:
    id = 1

class B:
    id = 2

class C:
    id = 3

class M(A, C, B):
    pass
M.id
# output: 1

class M(C, B, A):
   pass

M.id
# Output: 3

The class named first in the inheritance passes its value to the child class for the common attribute. This is the same with Python methods/functions of the class.

super()

Calls a method from the parent class.

Example:

 class Vehicle:
     def start(self):
         print("Starting engine")
     def stop(self):
         print("Stopping engine")
class TwoWheeler(Vehicle):
     def say(self):
         super().start()
         print("I have two wheels")
         super().stop()
Pulsar=TwoWheeler()
Pulsar.say()

Composition

A type of inheritance in Python. It intends to inherit from the base class but a little differently, i.e., by using an instance variable of the base class acting as a member of the derived class.

See the below diagram.

To demonstrate composition, we need to instantiate other objects in the class and then make use of those instances.

class PC: # Base class
    processor = "Xeon" # Common attribute
    def __init__(self, processor, ram):
        self.processor = processor
        self.ram = ram

    def set_processor(self, new_processor):
        processor = new_processor

    def get_PC(self):
        return "%s cpu & %s ram" % (self.processor, self.ram)

class Tablet():
    make = "Intel"
    def __init__(self, processor, ram, make):
        self.PC = PC(processor, ram) # Composition
        self.make = make

    def get_Tablet(self):
        return "Tablet with %s CPU & %s ram by %s" % (self.PC.processor, self.PC.ram, self.make)

if __name__ == "__main__":
    tab = Tablet("i7", "16 GB", "Intel")
    print(tab.get_Tablet())

Output:

Tablet with i7 CPU & 16 GB ram by Intel

Determine the type of instance and inheritance

type()

Tells us the type of object we’re working with.

type(3)
<classint>
type(False)
<classbool>
type(lambda :print("Hi"))
<class ‘function’>
type(type)
<classtype>

isinstance()

Takes in two arguments- a value and a type. If the value is of the kind of the specified type, it returns True.

isinstance(3, int)
True
isinstance((1), tuple)
False
isinstance((1,), tuple)
True

issubclass()

Takes two classes as arguments. If the first one inherits from the second, it returns True.

class A: pass
class B(A): pass

issubclass(B,A)
True

issubclass(A,B)
False

Errors And Exceptions

Errors: coding issues in a program which may cause it to exit abnormally.

Exceptions happen due to the occurrence of an external event which interrupts the normal flow of the program.

Exception handling with Try / Except / Finally

We enclose the unsafe code indented under the try block.

We can keep our fall-back code inside the except block.

Any instructions intended for execution last should come under the finally block.

try:
    print("Executing code in the try block")
    print(exception)
except:
    print("Entering in the except block")
finally:
    print("Reached to the final block")

Output:

Executing code in the try block
Entering in the except block
Reached to the final block

Raising Exceptions For A Predefined Condition

We can raise an exception based on some condition.

Example: we want the user to enter only odd numbers, else will raise an exception.

# Example - Raise an exception
while True:
    try:
        value = int(input("Enter an odd number- "))
        if value%2 == 0:
            raise ValueError("Exited due to invalid input!!!")
        else:
            print("Value entered is : %s" % value)
    except ValueError as ex:
        print(ex)
        break

Output:

Enter an odd number- 2
Exited due to invalid input!!!
Enter an odd number- 1
Value entered is : 1
Enter an odd number-

Module vs package

Each Python program file is a module, which imports other modules like objects and attributes.

The folder of Python program is a package of modules. A package can have modules or subfolders.

Closures

Function objects returned by another function. We use them to eliminate code redundancy.

Example: simple closure for multiplying numbers.

def multiply_number(num):
    def product(number):
        'product() here is a closure'
        return num * number
    return product

num_2 = multiply_number(2)
print(num_2(11))
print(num_2(24))

num_6 = multiply_number(6)
print(num_6(1))

The output is:

22
48
6

Monkey patching. Is it ever a good idea?

Changing the behavior of a function or object after it has already been defined.

Example:

import datetime
datetime.datetime.now = lambda: datetime.datetime(2012, 12, 12)

Most of the time it's a pretty terrible idea - it is usually best if things act in a well-defined way. One reason to monkey patch would be in testing. The mock package is very useful to this end

What Are Class Or Static Variables In Python Programming?

All the objects share common class or static variables.

But the instance or non-static variables are altogether different for different objects.

The programming languages like C++ and Java need to use the static keyword to make a variable as the class variable. However, Python has a unique way to declare a static variable.

All names initialized with a value in the class declaration becomes the class variables. And those which get assigned values in the class methods becomes the instance variables.

# Example 
class Test: 
    aclass = 'programming' # Class variable 
    def __init__(self, ainst): 
        self.ainst = ainst # Instance variable 

@classmethod, @staticmethod, @property

These are decorators: a special kind of function that either takes a function and returns a function, or takes a class and returns a class. The @ symbol is just syntactic sugar.

@my_decorator
def my_func(stuff):
    do_things

Is equivalent to:

def my_func(stuff):
    do_things

my_func = my_decorator(my_func)

This is how they behave:

class MyClass(object):
    def __init__(self):
        self._some_property = "properties are nice"
        self._some_other_property = "VERY nice"
    def normal_method(*args,**kwargs):
        print("calling normal_method({0},{1})".format(args,kwargs))
    @classmethod
    def class_method(*args,**kwargs):
        print("calling class_method({0},{1})".format(args,kwargs))
    @staticmethod
    def static_method(*args,**kwargs):
        print("calling static_method({0},{1})".format(args,kwargs))
    @property
    def some_property(self,*args,**kwargs):
        print("calling some_property getter({0},{1},{2})".format(self,args,kwargs))
        return self._some_property
    @some_property.setter
    def some_property(self,*args,**kwargs):
        print("calling some_property setter({0},{1},{2})".format(self,args,kwargs))
        self._some_property = args[0]
    @property
    def some_other_property(self,*args,**kwargs):
        print("calling some_other_property getter({0},{1},{2})".format(self,args,kwargs))
        return self._some_other_property

o = MyClass()
# undecorated methods work like normal, they get the current instance (self) as the first argument

o.normal_method 
# <bound method MyClass.normal_method of <__main__.MyClass instance at 0x7fdd2537ea28>>

o.normal_method() 
# normal_method((<__main__.MyClass instance at 0x7fdd2537ea28>,),{})

o.normal_method(1,2,x=3,y=4) 
# normal_method((<__main__.MyClass instance at 0x7fdd2537ea28>, 1, 2),{'y': 4, 'x': 3})

# class methods always get the class as the first argument

o.class_method
# <bound method classobj.class_method of <class __main__.MyClass at 0x7fdd2536a390>>

o.class_method()
# class_method((<class __main__.MyClass at 0x7fdd2536a390>,),{})

o.class_method(1,2,x=3,y=4)
# class_method((<class __main__.MyClass at 0x7fdd2536a390>, 1, 2),{'y': 4, 'x': 3})

# static methods have no arguments except the ones you pass in when you call them

o.static_method
# <function static_method at 0x7fdd25375848>

o.static_method()
# static_method((),{})

o.static_method(1,2,x=3,y=4)
# static_method((1, 2),{'y': 4, 'x': 3})

# properties are a way of implementing getters and setters. It's an error to explicitly call them
# "read only" attributes can be specified by creating a getter without a setter (as in some_other_property)

o.some_property
# calling some_property getter(<__main__.MyClass instance at 0x7fb2b70877e8>,(),{})
# 'properties are nice'

o.some_property()
# calling some_property getter(<__main__.MyClass instance at 0x7fb2b70877e8>,(),{})
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# TypeError: 'str' object is not callable

o.some_other_property
# calling some_other_property getter(<__main__.MyClass instance at 0x7fb2b70877e8>,(),{})
# 'VERY nice'

# o.some_other_property()
# calling some_other_property getter(<__main__.MyClass instance at 0x7fb2b70877e8>,(),{})
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# TypeError: 'str' object is not callable

o.some_property = "groovy"
# calling some_property setter(<__main__.MyClass object at 0x7fb2b7077890>,('groovy',),{})

o.some_property
# calling some_property getter(<__main__.MyClass object at 0x7fb2b7077890>,(),{})
# 'groovy'

o.some_other_property = "very groovy"
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# AttributeError: can't set attribute

o.some_other_property
# calling some_other_property getter(<__main__.MyClass object at 0x7fb2b7077890>,(),{})
# 'VERY nice'

Optional Statements Inside A Try-Except Block In Python.

  • else:

    • Runs a piece of code when the try block doesn’t create an exception.

  • finally:

    • Executes some steps which run, regardless of whether an exception happens or not.

Iterators

Array-like objects which allow moving on the next element. We use them in traversing a loop, for example, in a “for” loop.

Generators

A kind of function which lets us specify a function that acts like an iterator and hence can get used in a “for” loop.

In a generator function, the yield keyword substitutes the return statement.

# Simple Python function
def fn():
    return "Simple Python function."

# Python Generator function
def generate():
    yield "Python Generator function."

print(next(generate()))

The output is:

Python Generator function.

Decorators

Give us the ability to add new behavior to the given objects dynamically. In the example below, we’ve written a simple example to display a message pre and post the execution of a function.

def decorator_sample(func):
    def decorator_hook(*args, **kwargs):
        print("Before the function call")
        result = func(*args, **kwargs)
        print("After the function call")
        return result
    return decorator_hook

@decorator_sample
def product(x, y):
    "Function to multiply two numbers."
    return x * y

print(product(3, 3))

The output is:

Before the function call
After the function call
9

Generators

A function which returns an iterable object. We can iterate on the generator object using the yield keyword. But we can only do that once because their values don’t persist in memory, they get the values on the fly.

Generators give us the ability to hold the execution of a function or a step as long as we want to keep it.

Examples where it is beneficial to use generators.

  • Replace loops with generators for efficiently calculating results involving large data sets.

  • When we don’t want all the results and wish to hold back for some time.

  • Instead of using a callback function, we can replace it with a generator. We can write a loop inside the function doing the same thing as the callback and turns it into a generator.

Yield Keyword

"yield" can turn any function into a generator. It works like a standard return keyword. But it’ll always return a generator object.

A method can have multiple calls to the yield keyword.

See the example below.

def testgen(index):
  weekdays = ['sun','mon','tue','wed','thu','fri','sat']
  yield weekdays[index]
  yield weekdays[index+1]

day = testgen(0)
print next(day), next(day)

#output: sun mon

Functional programming: map, reduce, filter

a. filter()

Filter in some values based on conditional logic

list(filter(lambda x:x>5,range(8)))

[6, 7]

b. map()

Applies a function to every element in an iterable.

list(map(lambda x:x**2,range(8)))

[0, 1, 4, 9, 16, 25, 36, 49]

c. reduce()

Repeatedly reduces a sequence pair-wise until we reach a single value.

from functools import reduce
reduce(lambda x, y:x-y, [1, 2, 3, 4, 5]
# Output: -13

Python2 vs Python 3

Last updated