Monday, March 17, 2014

*Any Number of Arguments

*Any Number of Arguments


Something really cool that I came across today is putting a "*" in the argument in a function. It allows for you use any number of arguments for that function which is really cool and useful!

The the following example:

def add_all_values(*values):
    total = 0
    for x in values:
        total += x
    return total

add_all_values can take any number of  arguments:

>>> add_all_values()
0
>>> add_all_values(1)
1
>>> add_all_values(1, 2)
3
>>> add_all_values(1, 2, 3)
6
>>> add_all_values(1, 2, 3, 4)
10
>>> add_all_values(1, 2, 3, 4, 5)
15
>>> add_all_values(1, 2, 3, 4, 5, 6)
21
>>> add_all_values(1, 2, 3, 4, 5, 6, 7)
28
>>> add_all_values(1, 2, 3, 4, 5, 6, 7, 8)
36
>>> add_all_values(1, 2, 3, 4, 5, 6, 7, 8, 9)
45
>>> add_all_values(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

55

What is really fascinating is that this can be used to increase the versatility of higher order functions. Take the following examples:

def test_values(func, *values):
    a = []
    for x in values:
        a.append(func(x))
    return a

def square_value(value):
    return (value**2)

def cube_value(value):
    return (value**3)

def value_to_power_of_value(value):
    return (value**value)

So what's really cool is that now, we can use different number of arguments in the higher order function when we call the various lower order ones!

>>> test_values(square_value, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
>>> test_values(cube_value, 1, 2, 3, 4, 5)
[1, 8, 27, 64, 125]
>>> test_values(value_to_power_of_value, 1, 2, 3, 4)
[1, 4, 27, 256]

Higher Order Functions!

Higher Order Functions

I think higher order function (functions of functions) are very cool!

For example, you can make a function that test a function with various variables and returns all the answers in a list! Look at the following example:

def square_value(value:int) -> int:
    """
    >>> square_value(5)
    25
    """
    return (value**2)

def function_tester(func:object, values:list) -> list:
    """
    >>> function_tester(square_value, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
    [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
    """
    result = []
    for x in values:
        result.append(func(x))
    return result 

>>> function_tester(square_value, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

Very cool! I am very curious where this can be applied and how I can use this new trick in my code! I'm sure they can help me reduce the amount of code I need to write, especially when I need to write very similar functions, or when I need to write functions that have a combination of previously defined methods.

__eq__


__repr__


__init__

__init__

1. subjective reaction
2. steps you took to leviate events

Friday, February 21, 2014

Sorting and Efficiency

Sorting and Efficiency

There are various algorithms for sorting. Examples include bubble sort, insertion sort, quicksort, and mergesort. Some are more efficient and some are less. They also perform better or worse depending on the data array presented to them.

Bubble sort:

BEST: O(n)
NORMAL: O(n^2)
WORST: O(n^2)

def bubble_sort(L):
    length = len(L) - 1
    sorted = False

    while not sorted:
        sorted = True
        for i in range(length):
            if L[i] > L[i+1]:
                sorted = False
                L[i], L[i+1] = L[i+1], L[i]
    return L

Insertion sort:

BEST: O(n)
NORMAL: O(n^2)
WORST: O(n^2)

def insertion_sort(L):
    for i in range(1, len(L)):
        tmp = L[i]
        k = i
        while k > 0 and tmp < L[k - 1]:
            L[k] = L[k - 1]
            k -= 1
            L[k] = tmp
    return L

Quicksort:

 BEST: O(n log(n))
NORMAL: O(n log(n))
WORST: O(n^2)

def quick(L):
    if len(L) > 1:
        pivot = L[0] # there are much better ways of choosing the pivot!
        smaller_than_pivot = [x for x in L[1:] if x < pivot]
        larger_than_pivot = [x for x in L[1:] if x >= pivot]
        return quick(smaller_than_pivot) + [pivot] + quick(larger_than_pivot)
    else:
        return L

Mergesort:

BEST: O(n log(n))
NORMAL: O(n log(n))
WORST: O(n log(n))

def merge(L1:list, L2:list) -> 
    decreasing_from_largest = []
    while L1 and L2:
        if L1[-1] > L2[-1]:
            decreasing_from_largest.append(L1.pop())
        else:
            decreasing_from_largest.append(L2.pop())
    decreasing_from_largest.reverse() 
    return L1 + L2 + decreasing_from_largest

def merge_sort(L):

    if len(L) < 2 :
        return L
    else :
        left_sublist = L[:len(L)//2]
        right_sublist = L[len(L)//2:]
        return merge2(merge_sort(left_sublist), 
                      merge_sort(right_sublist))


I think it's very interesting to learn how to make sorting algorithms more and more efficient, depending on the array size. I interesting how the big O of an algorithm only depends on the highest order of the function.

Recursion

Iteration vs Recursion 


An iteration looks like this:

def interation(a):
    b = 1
    for i in range(a):
        b = b * (i+1)
    return b

Essentially the code iterates over the same set of instructions and returns the final result. Here is what it looks like:

>>> iteration(5)
i b * (i + 1) b
0 1 * (0 + 1) 1
1 1 * (1 + 1) 2
2 2 * (2 + 1) 6
3 6 * (3 + 1) 24
4 24 * (4 + 1) 120




A recursion looks like this:

def recursion(a):
    if a <= 1:      #base case
        return 1
    else:
        return a * recursion(a - 1)

This recursive function calls same function over and over again until the base case and then evaluates the entire expression. Here s what it looks like:

>>> recursion(5)
factorial(5)
5 * factorial(4)
4 * factorial(3)
3 * factorial (2)
2 * factorial (1)
1        #base case
(((((1)*2)*3)*4)*5) = 120


Recursions are very powerful and useful! Knowing how to run a recursive call is powerful for coding and opens up a whole new field of opportunities!

Thursday, February 20, 2014

Python: Default Values

Python's built in functions have built in values. For example, the print function has the following default values:

>>> help(print)
Help on built-in function print in module builtins:

print(...)
    print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)
...


The default values are pre-set. So the following code produces:

>>> print(1, 2, 3, 4, 5)
1 2 3 4 5


You can change various default values:

>>> print(1, 2, 3, 4, 5, sep='.')
1.2.3.4.5
>>> print(1, 2 , 3, 4, 5, sep='. . .', end='!')
1. . .2. . .3. . .4. . .5!


You can also make functions that have their own default values:

>>> def default_value(value1, value2 = 0, value3 = []):
>>>     print (value1, value2, value3, sep='. . .')
>>> default_value('abc')

abc. . .0. . .[]


You can change default values:

>>> def default_value(value1, value2 = 0, value3 = []):
>>>     print (value1, value2, value3, sep='. . .')
>>> default_value('abc', value2= 1234, value3 = ['a', 'b', 1])

abc. . .1234. . .['a', 'b', 1]


But be careful when working with mutable objects:

>>> def default_value(value1, value2=[]):
>>>     (value2.append(value1))
>>>     print(value2)
>>> default_value(1)
[1]
>>> default_value(1)
[1, 1]
>>> default_value(1)
[1, 1, 1]
>>> default_value(1)
[1, 1, 1, 1]



    

Tuesday, February 18, 2014

Classes

Every other class derives from the object class.

>>> help(object)
Help on class object in module builtins:

class object
  |     The most base type


Special methods in Python begin and end with 2 underscores and are called by particular syntax, e.g.:

x.__init__( . . . ) 
x.__eq__(y) <==> x==y
x.__str__() <==> str(x)
x.__floordiv__(y) <==> x//y

Various functions like print() call __str__

Monday, January 27, 2014

Python: Object Oriented Programming

What is object oriented programming?

Classes and Object

The two basic concepts in object oriented programming are the class and object. 

Class a class defines a new king of thing and how that thing can behave.
Object is a thing with particular set of properties tat behaves how its class tells it to.

# A simple class
class Empty(object):
    ''' A class that does nothing. '''
    pass

# Two object of class Empty
object_one = Empty()
object_two = Empty()

object_one and object_two are variables that point to objects. Both of these these objects point to the class Empty. Like objects, classes point to other classes in a hierarchical fashion and acquire their characteristics. For example, the class Empty points to the class object. Python adds this reference to Empty because we put object in parenthesis after Empty's name when we defined it.

We define the behavior of a class with methods. Methods are functions defined inside a class that can be called to get the object to do something.

# A class with a method
class OneMethod(object):
    ''' A class that squares and returns the_value it is fed. '''

    def square(self, the_value):
        return (the_value * the_value)

>>> object_three = OneMethod()
>>> object_three.square(10)
100

We call the OneMethod class the exact same way that we call "sting.upper()" or "list.pop()", and all of the other python builtin methods. The "self" argument in the square function refers to the object the method is being called for. For example, the "self" refers to object_three.

Objects and classes combine functions with data making them more customizable and easier to manage.

# An example of customizability
class Square(object):

     def square(self, value):
        return(value * value / self.devide)

>>> a = Square()
>>> a.devide = 2
>>> a.square(10)
50.0

>>> b = Square()
>>> b.devide = 4
>>> b.square(10)
25.0

As you can see, we can have different objects referring to the same method, without modifying it. This is very error-proof and allows us to easily manage multiple uses of the function throughout our code.

Constructor

def __init__(self, *)