Skip to content

Python3

Posted on:September 23, 2022 at 03:22 PM

Difference between list.sort() and sorted()

The fragments of program code that produce or calculate new data values are called expressions. The simplest kind of expression is a literal.

Formatting strings:

For e.g.

Return value of functions

All functions in Python return a value, regardless of whether the function actually contains a return statement. - Functions without a return always hand back a special object, denoted None.

Formal parameters & actual parameters

The parameters appearing in the function definition are called formal parameters, and the expressions appearing in a function call are known as actual parameters.

Python passes parameters by value. If the value being passed is a mutable object (e.g. list), then changes made to the object may be visible to the caller.

Importing of Python modules

Importing a Python modules executes them. When a module is imported, Python creates a special attribute, name, inside that module and assigns it a string representing the module’s name.

However, when Python code is being run directly (not imported), Python sets the value of name to be main.

Exception handling in Python

Python names (references) and values

Every object has an identity, a type and a value. Once an object is created, the type and identity can’t be changed. Whether or not the object’s value can change after creation determines if the object is mutable or immutable.

Assignment statements bind a name (identifier) to an object. Assignment creates a new object, with few exceptions. For e.g.

>>> x = 100

Aliasing: Two variables referring to the same object.

>>> x = 100
>>> y = x # aliasing

How the objects are garbage collected

Inbuilt functions

  1. id() returns the identity (memory address) of the object. No two objects have the same identity.
  2. is and is not operators: these identity operators evaluates whether or not the objects have the same identity, i.e. if they are the same object.

Difference between += and + operator on mutable objects(e.g. list)

For mutable objects ’+=’ changes the object in place, whereas ’+’ gives a new object and assign to the name. ’+=’ calls iadd() whereas ’+’ calls add().

for e.g.

>>> lst = [1,2,3]
>>> lst += [4,5,6] # here id of lst doesn't change
>>> lst = lst + [7,8,9] # here id of lst changes.

Exceptions to the rule “Assignment creates a new object”

Though each assignment creates a new object. There are few exceptions due to optimizations done in CPython.

  1. For integers between -5 and 256, there is an internal array maintained and no new object is created. For e.g.
>>> a = 256
>>> b = 256
>>> a is b
True
  1. For small strings, if the same value string is assigned to another name, the old string object is used. For e.g
>>> a = "python"
>>> b = "python"
>>> a is b
True   # a and b refer to the same object!
  1. Empty immutable objects
# No new objects are created for empty tuples (immutable objects).
For e.g
>>> a = ()
>>> b = ()
>>> a is b
True  # a and b both refer to the same object in memory

Names and values(Ned batchelder)

  1. Names refer to values/objects.

  2. Many names can refer to one value/object. For e.g.
    x = 23
    y = x

  3. Names are reassigned independently of other names When we said “y = x”, that doesn’t mean that they will always be the same forever. Reassigning x leaves y alone. For eg.
    x = 23
    y = x
    x = 12 # this will not change the value refered to by the name ‘y’

Assignment:

  1. Assignment never copies data:
>>> x = 4 # bind 'x' with  object whose value is 4.
>>> y = x # bind y to the value pointed to by x
>>> x = 8 # rebind x to object whose value is 8.
>>> print(y) # will print 4, as 'y' is still bound to 4.

IMPORTANT: There is no way in python where a name can refer to another name. A name can only refer to values.

  1. Changes in a value are visible through all of its names.

Rebinding the name vs. mutating the value

x = 1
x = x + 1 # rebind the name to a new value

nums = [1, 2, 3]
nums.append(4) # mutating the value

Myth : Python assigns mutable and immutable values differently. All assignment works the same: it makes a name refer to a value. But with an immutable value, no matter how many names are referring to the same value, the value can’t be changed in-place, so you can never get into a surprising Presto-Chango situation.

  1. References can be more than just names

Anything that can appear on the left-hand side of an assignment statement is a reference, and everywhere I say “name” you can substitute “reference”.

my_obj.attr = 23
my_dict[key] = 24
my_list[index] = 25
my_obj.attr[key][index].attr = “etc, etc”

Note that “i = x” assigns to the name i, but “i[0] = x” doesn’t, it assigns to the first element of i’s value.It’s important to keep straight what exactly is being assigned to. Just because a name appears somewhere on the left-hand side of the assignment statement doesn’t mean the name is being rebound.

  1. Lots of things are assignment
- X = ...
- for X in ...
- [... for X in ...]
- (... for X in ...)
- {... for X in ...}
- class X(...):
- def X(...):
- def fn(X): ... ; fn(12)
- with ... as X:
- except ... as X:
- import X
- from ... import X
- import ... as X
- from ... import ... as X
  1. Python passes function arguments by assigning to them.

Fact: Python passes function arguments by assigning to them.

>>> l = [1,2,3]
>>> id(l)
140581310766344
>>> x = l[0]
>>> id(x)
10910400

Concurrency vs Parallelism vs Multiprocessing

Coroutine

A function that uses yield as a signal to the scheduler, indicating that the coroutine will be waiting until an event (such as IO) is completed.

Python nonlocal keyword :

Below code will print ‘Hello’ 2 times, due to use of nonlocal keyword.

def function_outside():
    msg = 'Hi'
    def function_inside():
        nonlocal msg   <-- Now msg refers to the enclosing msg label
        msg = 'Hello'
        print(msg)
    function_inside()
    print(msg)

function_outside()

Inner Functions

Inner functions are defined functions inside other functions.

Python closure

Closure is a function object that remembers values in enclosing scopes even if they are not present in memory. Basically, the method of binding data to a function without actually passing them as parameters is called closure. When the interpreter detects the dependency of inner nested function on the outer function, it stores or makes sure that the variables in which inner function depends on are available even if the outer function goes away.

In conclusion here are the three criteria’s for a closure:

  1. There must be a nested function (a function inside another function).
  2. This nested function has to refer to a variable defined inside the enclosing function.
  3. The enclosing function must return the nested function.

The code below implements a closure.

def func1():  #Outer function
  msg = 'I belong to func1'
  def func2(): #Nested function
      print (msg)
  return func2

>>> obj = func1()  #binding the function to an object
>>> obj() # contains inner function reference
I belong to func1 # the value of msg is retained even after the outer function goes out of scope.

Functions as First-Class Objects

In Python, functions are first-class objects. This means that functions can be passed around and used as arguments, just like any other object in Python.

Iterable and Iterators

Iterable : An iterable is an object that has an iter() method which returns an iterator, or which defines a getitem() method that can take sequential indexes starting from zero (and raises an IndexError when the indexes are no longer valid or StopIteration when the end of iterable has reached).

Iterator : An iterator is an object with a next (Python 2) or next() (Python 3) method. next() method signals when it is done by raising StopIteration exception.Iterator also implements iter() method which returns self object.

Generators function

Generators are functions that can be paused and resumed on the fly, returning an object that can be iterated over. Unlike lists, they are lazy and thus produce items one at a time and only when asked. So they are much more memory efficient when dealing with large datasets.

>>> def countdown(num):
...     print('Starting')
...     while num > 0:
...         yield num
...         num -= 1
...
>>> val = countdown(5) # the function does not execute as 'Starting' is not printed. Instead a generator is returned.

>>> val
<generator object countdown at 0x10213aee8>

When calling next() the first time, execution begins at the start of the function body and continues until the next yield statement where the value to the right of the statement is returned, subsequent calls to next() continue from the yield statement, and loop around and continue until another yield is called. If yield is not called (which in our case means we don’t go into the if function because num <= 0) a StopIteration exception is raised:

>>> next(val)
Starting
5
>>> next(val)
4
>>> next(val)
3
>>> next(val)
2
>>> next(val)
1
>>> next(val)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Generator Expressions

They are just like list comprehensions, except the parenthesis ( ) instead of [ ]. They return a generator object rather than a list. Generator expressions can run slower than list comprehensions (unless you run out of memory, of course), but they use less space, as can be seen from the code below.

>>> import sys
>>> g = (i * 2 for i in range(10000) if i % 3 == 0 or i % 5 == 0)
>>> print(sys.getsizeof(g))
72
>>> l = [i * 2 for i in range(10000) if i % 3 == 0 or i % 5 == 0]
>>> print(sys.getsizeof(l))
38216

Generator Expressions vs List comprehension

  1. Does not construct a list.
  2. Only useful purpose is iteration
  3. Once consumed, can’t be reused

The parens on a generator expression can dropped if used as a single function argument, for e.g:
sum(x\*x for x in s) <------ Generator expression with parens droppped.

Decorators : Decorator is a function that takes another function and extends the behavior of the latter function without explicitly modifying it.

say_whee = my_decorator(say_whee) is equivalent to below expression.

@my_decorator
def say_whee():
    print("Whee!")

# Below is a decorator which takes arguments and also return values It also gives correct name of wrapped functional
import functools

def decorator(func):
    # This will give correct name to func.__name__
    @functools.wraps(func)
    def wrapper_decorator(*args, **kwargs):
        # Do something before
        value = func(*args, **kwargs)
        # Do something after
        return value
    return wrapper_decorator

Variable scope

We call the part of a program where a variable is accessible its scope, and the duration for which the variable exists its lifetime.

A variable which is defined in the main body of a file is called a global variable. It will be visible throughout the file, and also inside any file which imports that file. Global variables can have unintended consequences because of their wide-ranging effects – that is why we should almost never use them. Only objects which are intended to be used globally, like functions and classes, should be put in the global namespace.

The inside of a class body is also a new local variable scope. Variables which are defined in the class body (but outside any class method) are called class attributes.

Attributes set on instances are called instance attributes. Class attributes are shared between all instances of a class, but each instance has its own separate instance attributes.

In some languages defining a variable can be done in a separate step before the first value assignment. It is thus possible in those languages for a variable to be defined but not have a value – which could lead to errors or unexpected behaviour if we try to use the value before it has been assigned. In Python a variable is defined and assigned a value in a single step, so we will almost never encounter situations like this.

By default, the assignment statement creates variables in the local scope. So the assignment inside the function does not modify the global variable, it creates a new local variable called, and assigns the value 3 to that variable.

global/nonlocal keyword

LEGB rule

*args and **kwargs

range function

List/Set/Dict Comprehension

List Comprehension: [ x for x in range(20) if x % 2 == 0]

Dictionary Comprehension: {k:v for (k,v) in dict1.items() if v>2} Excercise : Make a list of all possible 2 letter combinations, where, letters = ‘abcdef…xyz’.
Solution: [a + b for a in letters for b in letters]

Data Classes

A data class is a class typically containing mainly data, although there aren’t really any restrictions. It is created using the new @dataclass decorator.

A data class comes with basic functionality already implemented. For instance, you can instantiate, print, and compare data class instances straight out of the box.

If value of dataclass instance is same, then they are equal.

By default, data classes implement a .repr() method to provide a nice string representation and an .eq() method that can do basic object comparisons.

Metaclasses

fstrings in python

named tuple

Python descriptors

Python descriptors gives us a powerful technique to write reusable code that can be used between classes.

@classmethod, @staticmethod, and @property.

The @classmethod and @staticmethod decorators are used to define methods inside a class namespace that are not connected to a particular instance of that class.

The @property decorator is used to customize getters and setters for class attributes.

Use case for ‘@property’

Suppose in a class there are two properties, radius and circumference. circumference is calculated using radius. But if we modify radius then circumference will not change automagically. We define a function circumference() with decorator property (this now becomes a getter) and then access it like a normal attribute, ie obj.circumference (not obj.circumference()).

Background

Attributes(method and otherwise) of an objects are stored in dict on the object. (obj.__dict__).

If you access an attribute of an object (obj.foo). It gets you one of the following three.

What is a Data descriptor :

Descriptor protocol :

map/filter functions

They can be easily replicated by list comprehensions.
syntax: map(function_object, iterable1, iterable2,…) e.g.

def myfunc(a, b):
  return a + b

x = map(myfunc, ('apple', 'banana', 'cherry'), ('orange', 'lemon', 'pineapple'))

The filter() function returns an “iterator” were the items are filtered through a function to test if the item is accepted or not. filter syntax : filter(function, iterable) e.g.

ages = [5, 12, 17, 18, 24, 32]

def myFunc(x):
  if x < 18:
    return False
  else:
    return True

adults = filter(myFunc, ages)

for x in adults:
  print(x)

lambda functions :

lambda arguments: expression

Lambda functions can have any number of arguments but only one expression. The expression is evaluated and returned. Lambda functions can be used wherever function objects are required.

zip and enumerate builtins:

enumerate() gives index along with value of a sequence, for eg :

>>> a = ["a", "b", "c"]
>>> for i, v in enumerate(a):
        print i, v
0 a
1 b
2 c

zip() takes two or more sequences of same length and returns a sequence of tuple containing two or more elements. for e.g.

>>> a = [1, 2, 3]
>>> b = [3, 4, 5]
>>> c = [6, 7, 8]
>>> for i, j, k in zip(a, b, c):
        print i, j, k
1 3 6
2 4 7
3 5 8

Python OOP:

Python MRO(Method resolution order)

Head : First element of the list.
Tail : all elements except the head in the list.
Bad head : Present in the tail of other sequence.
Good head : Not in the tail of any other list/sequence.

How to compute the merge

Take the head of the first list, if this head is not in the tail of any of the other lists, then add it to the linearization of C and remove it from the lists in the merge, otherwise look at the head of the next list and take it, if it is a good head. Then repeat the operation until all the class are removed or it is impossible to find good heads. In this case, it is impossible to construct the merge, Python 2.3 will refuse to create the class C and will raise an exception.

Linearization of class C :

It is sum of C plus the merge of linearization of the parents and the list of parents, i.e.

Given
Class C(B1, B2…Bn)
Then, Linearization of class C = L[C(B1, B2…Bn)]
L[C(B1, B2…Bn)] = C + merge(L[B1], L[B1] … L[Bn], B1, B2…Bn)

Consider an example

# Object (Base class for all classes)

Class D(Object):
	pass

Class E(Object):
	pass

Class F(Object):
	pass

Class C(D, F):
	pass

Class B(D, E):
	pass

Class A(B, C):
	pass

In the above scenario:

L[O] = O
L[D] = [D, O]
L[E] = [E, O]
L[F] = [F, O]
L[B] = B + merge(L[D], L[E], [D, E])
L[B] = B + merge([D, O], [E, O], [D, E])
L[B] = [B, D] + merge([O], [E, O], [E])
L[B] = [B, D, E] + merge([O], [O])
L[B] = [B, D, E, O]

Static method vs Class method in Python :

class method :

Static method :

Instance variables: Contains data unique to each instance. They are visible in namespace of class instance.

Class variables: Contains data which is same in each instance. Can be accessed from instance as well as class. When we access class variable from instance, python first look for variablew in the instance, then look for it in the class. They are not visible in namespace of class instance, but in namespace of the class.

class Employee:

    raise_amt = 1.04

    def __init__(self, first, last, pay):
        self.first = first
        self.last = last
        self.email = first + '.' + last + '@email.com'
        self.pay = pay

    def fullname(self):
        return '{} {}'.format(self.first, self.last)

    def aply_raise(self):
        self.pay = int(Self.pay*self.raise_amt)

   @classmethod
    def set_raise_amt(cls, amount):
        cls.raise_amt = amount

    @classmethod           <-- Alternative constructor
    def from_string(cls, emp_str):
        first, last, pay = emp_str.split('-')
        return cls(first, last, pay)

    @staticmethod              <- Observe that neither class or instance is passed to them
    def is_workday(day):
        if day.weekday() == 5 or day.weekday() == 6:
            return False
        return True

These two are same :

emp1 = Employee('saurabh', 'prakash', 190000)
Employee.fullname(emp1)
emp1.fullname()

To print namespace of emp1

print(emp1.__dict__)

Classmethods and static methods

Classmethods as alternative constructors

Inheritance

Note : Below code will create a variable ‘raise_amt’ and will not affect the class variable.

emp1.raise_amt = 1.05
class Developer(Employee):      <---- Developer is derived class and Employee is base class
    raise_amt = 1.10

    def __init__(self, first, last, pay, prog_lang):
        super().__init__(first, last, pay)
        self.prog_lang = prog_lang

python builtin functions

Class decorators

Metaclasses