Skip to main content

some Python3

Getting Started ..

** numerically how will you find the square root of a number
(i was seeing this tutorial and saw this, it was pretty good).

so let me say i want y which is square root of x now what can we do, from above we say y*y = x, so we need to find y such that y when multiplied by itself gives us x.
so how to do it.
a simple algorithm:
- take a number n (you can choose n to be x/2)
now 
- if n*n == x (or n*n is near x (we need to decide what do mean by near)), n is the square root of x,
- else set n = (n + x/n)/2
and repeat the above two steps until the if condition is true.

1. Python is case sensitive - very important, and its an interpreted language not compiled. wikipediastack overflow

2. In Python everything is an object an you can get the type of the object by using 
type(<put the object here>).

-- Inputs --

3. Now if you want to take user input
abc = input('write something')
- it will propmpt you to write something it will be stored as a string in variable abc, if you want to input an integer do the below
abc = int(input('write something'))
this will convert the input to integer if it is convertible e.g. 123 its convertible to integer, abc not convertible this time it will give ValueError.
If you write below it will evaluate the thing which you enter
abc eval(input('write something'))
the above thing is dangerous as inputter :P can write anything and it will get evaluated.

** Remember in Python 3 we only have input not raw_input,
if you want what was the difference refer here stack overflow.

-- --

4. A cool algo to find the cube root of a number iteratively :
cube = 27
epsilon = 0.01
num_guess = 0
low = 0
high = cube
guess = (high + low)/2.0

while abs(guess ** 3 - cube) >= epsilon:
    if guess ** 3 < cube:
        low = guess
    else:
        high = guess
    guess = (high + low)/2.0
    num_guess += 1
    #print((guess ** 3) - cube)
print('number of guesses',num_guess)
print('number closest to cube root is',guess)

** For -ve numbers just make 
low = cube
high = 0

or more generally use the below for both +ve and -ve numbers
low = min(0, cube)
high = max(0, cube)

** Also cube root of a +ve number below 1 will be greater than itself generally so our above algorithm will fail. So we can handle this simply by writing this after we write the above two lines.
if cube < 1:
    low = cube
    high = 1

5. You can simply do list('abhishek') to get ['a', 'b', 'h', 'i', 's', 'h', 'e', 'k'] i.e. a list of characters of the string.

6. Some function examples - 
Allowed - 
def g(y):
    print(x)
    print(x + 2)
x = 2
g(x)
print(x)

Not Allowed - 
def g(y):
   x += 1
x = 2
g(x)
print(x)

7. tuple - it is also immutable like string, its defined in parenthesis
e.g. t = ('abhi', 2), if you want to define a tuple with a single element you need to define it like t = ('abhi',), if you just define it ('abhi') without comma at the end it will be treated like a string/float/integer depending on the data inside the parenthesis.
Also we can do something like this - 
def func1():
    return 'abhi'

def func2():
    return 'sing'

tpl = (func1, func2)

**easy way to swap variables
(x, y) = (y, x)

**also tuple allows you to return multiple values from a function

8. append vs extend - lists -
x = [1,2,3]
x.append([2,3])
print(x) - > [1, 2, 3, [2, 3]]

x = [1,2,3]
x.extend([2,3])
print(x) - > [1, 2, 3, 2, 3]

to remove elements:
x.remove(2) -> remove first occurrence of 2 from the list.
del(x[2]) -> removes element at index 1.
x.pop() -> removes last element from a list.

9. some basic list/string operation.
.split() to split a string to a list using the the value in the parenthesis as delimiter, in case empty uses space as delimiter.
e.g. 'abhi sing'.split() -> ['abhi', 'sing']
'<joiner>'.join(<some list of char or string>) -> concatenates elements inside the join with the joiner.
e.g. ' '.join(['abhi', 'sing']) -> abhi sing
sorted(<some list>) - gives sorted version of the list , does not mutate the list.
* but <list>.sort() - gives you sorted list i.e. mutates the list, now the list becomes the sorted list
<list>.reverse() - gives you reversed list i.e. mutates the list.
** if you want to make a copy of a list list1 , instead of doing
list2 = list1 (this will just make a variable list2 point to list1, its called aliasing) you can do list2 = list1[:] or list2 = list1.copy()
** avoid mutating the list as you iterate over it.
e.g. 
l1 = [1,2,3,4]
l2 = [1,2,5,6]

def remove_dupls(L1, L2):
    for e in L1:
        if e in L2:
            L1.remove(e)
remove_dupls(l1, l2)
print(l1)

The above code prints [2,3,4] not [2,4], why because python in the back keeps track of the indexes while iterating, so once the index becomes 1 and we remove the first element then index of 3 becomes 1 and the iterator skips the 2 as it is in 0th index.

10. A simple recursion code for multiplication
** the idea behind recursion is that solving a smaller version of same problem until we get to the very base version of the same problem.
def multiply(a, b):
    if b == 1:
        return a
    else:
        return a + multiply(a, b-1)
print(multiply(2, 3))

For factorial
def factorial(a):
    if a == 1:
        return 1
    else:
        return a * factorial(a-1)
print(factorial(5))

For palindrome
def isPal(s):
    if len(s)<=1:
        return True
    else:
        return s[0] == s[-1] and isPal(s[1:-1])
isPal('abhiihba')

For Tower of Hanoi


11. dictionary - keys should be distinct and immutable
grades = {'abhi', 'A+',  'jiya', 'C+', 'dibba', 'B+', 'abba', 'A-'}

add an entry - 
grades['jabba'] = 'B-'

test if key is in dictonary -
if 'abhi' in grades:
    <do something>

delete entry -
del(grades['jiya'])

to get all the keys -
grades.keys()

to get all the values -
grades.values()

one good example of using dictionary:
remember fibonacci series using recursion, as the number n increases as it will calculate all the preceding values again and again it will take so much time
e.g. the below code takes more than 82 seconds to get the value
t1 = time()
def fibo(n):
    if n==1 or n==2:
        return 1
    else:
        return fibo(n-1) + fibo(n-2)
print(fibo(40))
t2 = time()
print(t2-t1)

but if we use dictionary to save previous values it only takes fraction of the values
t1 = time()
def fibo1(n, d):
    if n in d:
        return d[n]
    else:
        ans = fibo1(n-1, d) + fibo1(n-2, d)
        fibo_dict[n] = ans
        return ans
 print(fibo1(40, fibo_dict))
  t2 = time()
  print(t2-t1)



12. Classes of tests
- Unit testing
validate each piece of program
testing each function seperately

-Regression testing
add tests for bugs as you find them
catch reintroduced errors that were previously fixed

-Integration testing
does oveall program work
tend to rush to do this

13. Testing types
-Black Box testing
doing testing based on the given specifications , e.g. looking at doc string (comments) - here we only use the comments not code

-Glass Box testing
go through all the possible paths in a code how you can exit the code - here we use the full code for testing

** why debugging word was used, as in earlier time the computers were kinda big and electrical and bugs used to sometimes get electrocuted and hence doing some sort circuit or some other issue, so they had to debug the computers for proper functioning. (may be this is why not sure)

14. Debugging
-most basic debugging is using print statements to get the output at critical points where you might thing the code needs to be checked.
-don't write the entire program and then test, keep testing as you go e.g. write a function test it then go on, also its good to do version controlling (e.g. github).

15. Exceptions
try and except statements
try:
    some code ..
except:
    print("some error message")

try and except statements with specific message for separate errors
try:
    some code ..
except ValueError:
    print("some error message for value error")
    except ZeroDivisionError:
    print("some error message for zero division error")
except:
    print("some error message for all other errors")

also we can have else and/or finally see below code, else will come after excepts and finally at last
try:
    print(1/2)
except ZeroDivisionError:
    print('divide by zero')
except TypeError:
    print('type error')
else: #it will get printed if there is not exception i.e. try is successful
    print('success')
finally: #it will always get executed
print('it will print no matter what')

raising our own exception
try:
    raise ValueError('some exception')
except ValueError:
    print('some valueerrorprint')

16. Assertion
def someFunc():
    a = 1
    b = 2
    assert not b == 0, 'denominator is zero' #it means we are asserting that b should not be equal to zero, so if b == 0 it will give AssertionError
    return a/b
print(someFunc())

the above function will return value only if statement next to assert is true else it will give assertion error saying 'denominator is zero'


17. OOP's
Everything in python is an object and so everything has a type.
Objects are data abstraction that captures -
-an internal representation
--through data attributes
-an interface for interacting with objects
--through making methods
--defines behavior but hide implementation

** some important things dir(), help(), ??

a class will have a data attributes and methods
- we generally define data attribute in the __init__ method

__init__(self, x, y): #self is used to refer to an instance of the class.
    self.x = x #and we need to assign values using self.x then only that x will belong to that instance.
    self.y = y

** here self is a general word we can use anything here, but in python community/convention we use self.

basic example
import math 
class Coordinates(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def distance(self, other):

        return (math.sqrt((self.x - other.x)**2+(self.y - other.y)**2))

c = Coordinates(3, 4)
zero = Coordinates(0, 0)

c.distance(zero) #is equivalent to Coordinates.distance(c, zero)

now 'c' here is an object of class Coordinates
so if you do print(c) it will give o/p 
<__main__.Coordinates object at 0x0000000008CCDF60>
not very informative , in such you can define a __str__ method,
so if you call the print(c) now it will give you the o/p defined in this __str__ method.

 class Coordinates(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def distance(self, other):
        some code ...

    def __str__(self):
        return "<"+str(self.x)+" , "+str(self.y)+">"
c = Coordinates(3, 4)
so now when we do print(c) it will return <3, 4>

if you want to check if an object is an instance of a class use below code
isinstance(c, Coordinates) it will return True

in class we can define multiple methods, the list of methods which we can define can be seen using dir() method , so for above code if we want to know what methods we can have for object 'c' then we can simply write dir(c).
As we can see we have a bunch of methods e.g. __add__, so it means we can have an add method in our class, and the way we can use this method is as follows
class Fraction(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __float__(self):
        return self.x/self.y
    
    def __str__(self):

        return "<"+str(self.x)+" , "+str(self.y)+">"
c = Fraction(3, 4)

d = Fraction(2, 5)
e = c + d
The above code will give below error
TypeError: unsupported operand type(s) for +: 'Fraction' and 'Fraction'

but if we have an __add__ method then we won't get any error.
class Fraction(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __float__(self):
        return self.x/self.y

    def __add__(self, other):
        top = self.x*other.y + self.y*other.x
        bottom = self.y*other.y
        return Fraction(top, bottom)
    
    def __str__(self):

        return "<"+str(self.x)+" , "+str(self.y)+">"
c = Fraction(3, 4)
d = Fraction(2, 5)
e = c + d
in the above code e = c + d will translate to e = Fraction.__add__(c, d) or e = c.__add__(d) and this will return an Fraction(23, 20) object and assign it to e.

18. More OOP's Classes and Inheritance
* the name of the class is the type
* also remember putting self is compulsory as we can only call methods inside the class using the objects of the class and whenever we call a method of a class using on object of the class the object is always passed as first argument by default.
* also if we define a method without self, we can call it only by using the classname.methodname(), e.g.
class Fraction(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def someMethod():
        print('something')

frac1 = Fraction(x, y)
frac1.someMethod()# this will give error as we are calling this method using the instance of the class so by default the object will get passed but we don't have a self in the method to receive it 
Fraction.someMethod()# this is fine this will print 'something'

- we can have getter and setter methods in the class these just simple methods, getters are generally used to return the value of an attribute of an object from the class, and setter are used to set the values. e.g. in the below code we are setting name of an object using the set_name method and using get_method to retrieve the name

class Animal(object):
    def __init__(self, age):
        self.age = age
        self.name = None

    def set_age(self, name):
        self.age = age

    def get_age(self):
        return self.age

    def set_name(self, name):
        self.name = name

    def get_name(self):
        return self.name

animal1 = Animal(10)
animal1.set_name('Anni')
print(animal1.get_name())

* also you should not access the attribute directly instead you should use getter methods.
e.g.
we can use animal1.age but instead we should use animal1.get_age() its a part of data protection/hiding, one basic example is if we the developer wants to call the attribute year instead of age animal1.age wont work but get_age() method will always work

** also python allows us to change the class attribute from outside, e.g. animal1.age = 'older' (someone might even put string even age is supposed to be numeric) - we should not do this, also python allows us to create attributes outside the class e.g. animal.size = 'big' (size was not defined inside the class)

-default parameters
e.g.
class Animal():
    def __init__(..):
        some code ..
    
    def set_name(self, new_name=""):#a default name is assigned if no parameter is passed
        self.name = new_name

Animal1 = Animal()
Animal1.set_name()
or we can do 
Animal1.set_name("fluffy")

19. Hierarchies
- Parent Class - its like superclass
- Child Class - subclass
-- it inherits all data and method/behavior from the parent class
-- add more info
-- add more behavior
-- override behavior

e.g. of a subclass

class Animal():
    def __init__(..):

        some code ..

class Cat(Animal):# Cat class inherits from Animal Class
                               # also if we don't have a __init__ in the subclass it inherits from the superclass 
    def speak(self):# this method is specific to Cat class
        print('meow')

* if we call a method from a child class object it will search in the child class if the method is there it will call that, else it will search in the parent class and use that method if it exists in the parent class.
Also we can call our superclass methods from our child class methods e.g.
class Cat(Animal):
    def __init__(self, age, name):
        Animal.__init__(self, age) # as we already have this code in superclass so why not use it, we can initialise this here also
        self.set_name(name)

   def speak(self):
       print('meow')

*Class variable, till now we saw instance variables meaning the variables we for a particular instance of a class, different objects can have different values of the same variable meaning the variable values belongs to that particular object.
class variables and their values are shared between all instances of a class
class Rabbit(Animal):
    tag = 1.
    def __init__(self, age, name):
        Animal.__init__(self, age) 
        self.id = Rabbit.tag # here we are giving unique id to each instance of Rabbit class
        Rabbit.tag+=1 # tag keeps increasing by 1 as we keep creating instance of Rabbit class

** just something, str(3).zfill(3) this will create a string '003' 

20. Programming efficiency 
we usually measure this by how well our program will scale, so we generally use order for this , e.g. if the code is O(n) or O(log(n)), or O(n^2) or something else may be exponential (c^n) or power(n^c).

* a good program to convert number to string, yes you can also do it simply by doing str(num)
def intToStr(i):
    digits = '0123456789'
    if i == 0:
        return '0'
    res = ''
    while i>0:
        res = digits[i%10] + res # it will give the reminder which will act as an index for digits
        i = i//10
    return res
print(intToStr(123432))

so whats the order of the above program, how many times can we divide the integer i by 10, its log(i), here i is the integer not length of i as for e.g. the smallest and largest 6 digit numbers are 100000 and 999999, now log(100000) = 5 and log(999999) = 5.9999.. so any 6 digit number can be divided by 10 , 5 times.

* a good algorithm for the power set concept, for example we have four numbers {1,2,3,4} we need to generate all possible combinations of these example, we need a null set {}, we need all single values {1},{2},{3},{4}, we need all the two pairs and so on.
so how to generate this.
-get an empty set {}
-add first element 1 to it {1}
-now add 2 to both the above elements {2}, {1,2}
-now add 3 to all the above  {3}, {1,3}, {2,3}, {1,2,3}
-now add 4 to all the above {4},{1,4},{2,4},{1,2,4},{3,4},{1,3,4},{2,3,4},{1,2,3,4}
-like this we get all the possible valuesBelow is the code for the same 
(this is an example of exponential (2^n) complexity)
def genSubset(L):    
    if len(L) == 0:
        return [[]]
    smaller = genSubset(L[:-1])
    extra = L[-1:]
    new = []
    for small in smaller:
        new.append(small + extra)
    return smaller + new

21. Searching and Sorting
Same old things ..
* one thing is when we search linearly then the search in O(n),
when the list is sorted it takes O(log(n)) - binary search, so when it is efficient to sort then search ?
sort + O(log(n)) < O(n)
=> sort < O(n) - O(log(n)) -> when sorting less than O(n) 
but this is not possible as sorting a list has to be atleast O(n),
so now what, the catch here is we have to sort the list only once but we search multiple times.
now if do k searches then sort + k*O(log(n)) < k*O(n)
now if k is large the sort term becomes irrelevant

* merge sort .. 






















Comments