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!