Note4
Representation
String conversion
To represent data effectively, an object value should behave like the kind of data it is meant to represent ,including producing a string representation of itself. String representations of data values are especially important in aninteractive language that automatically displays the string representation of the values of expressions in an interative session.
Python stipulates that all objects should produce two different string representations: one that is human-interpretable text and one that is a Python-interpretable expression.
The constructor function for strings , str, returns a human-readable string. Where the repr function returns a Python expression that evaluates to an equal object .
repr(object) => string : return the cannoical string representation of the object . For most object types , eval(repr(object)) == object
- The result of calling repr on the value of an expression is what python prints in an interactive session.
>>> 12e12
12000000000000.0
>>> print(repr(12e12))
12000000000000.0
- The str constructor often coinsides with repr , but provides a more interpretable text presentation in some cases
>>> from datetime import date
>>> tues = date(2011, 9, 12)
>>> repr(tues)
'datetime.date(2011, 9, 12)'
>>> str(tues)
'2011-09-12'
- The repr function always invokes a method called _repr_ on its argument
- The str constructor is implemented in a similar manner: it invokes a method call _str_ on its argument.
class Bear1():
""" a bear"""
def __repr__(self):
return '__repr__ Bear1()'
oski1 = Bear1()
print(oski1)
print(str(oski1))
print(repr(oski1))
print(oski1.__repr__()) # <bound method Bear1.__repr__ of __repr__ Bear1()>
print(oski1.__str__()) # <method-wrapper '__str__' of Bear1 object at 0x0000019C1C5E1488>
# the ans is:
# __repr__ Bear1()
# __repr__ Bear1()
# __repr__ Bear1()
# __repr__ Bear1()
# __repr__ Bear1()
print('===================='*2)
class Bear2():
""" a bear"""
def __repr__(self):
return '__repr__ Bear2()'
def __str__(self):
return '__str__ a Bear2'
oski2 = Bear2()
print(oski2) # print __str__
print(str(oski2)) # print __str__
print(repr(oski2))
print(oski2.__repr__()) # <bound method Bear2.__repr__ of __repr__ Bear2()>
print(oski2.__str__()) # <bound method Bear2.__str__ of __repr__ Bear2()>
print(oski2.__repr__)
print(oski2.__str__)
# the ans is
# __str__ a Bear2
# __str__ a Bear2
# __repr__ Bear2()
# __repr__ Bear2()
# __str__ a Bear2
print('===================='*2)
class Bear3():
""" a bear"""
def __init__(self):
# instance attribute
self.__repr__ = lambda : 'self.__repr__ oski3'
self.__str__ = lambda : 'self.__str__ this bear3'
def __repr__(self):
return '__repr__ Bear3()'
def __str__(self):
return '__str__ a Bear3'
oski3 = Bear3()
print(oski3) # always same as str(oksi3)
print(str(oski3)) #skip the instance attribute of the oski3 and go strait to the class
print(repr(oski3)) # ignore the instance of the oski3
# the following two are the conventional way
print(oski3.__repr__()) # <function Bear3.__init__.<locals>.<lambda> at 0x00000255A41F51F8>
print(oski3.__str__()) # <function Bear3.__init__.<locals>.<lambda> at 0x00000255A4245DC8>
# this ans is :
# __str__ a Bear3
# __str__ a Bear3
# __repr__ Bear3()
# self.__repr__ oski3
# self.__str__ this bear3
Special Methods
- _init_ method of a class is automatically invoked whenever an object is contructed
- The _str_ method is invoked automatically when printing , and the _repr_ is invoked in an interative session to display values
- True and False value
- 0 is false value and all other numbers are true values
- By default, objects of user-defined classes are considered to be true, but the special _bool_ method can be used to override this behavior.
- If an object defines the _bool_ method, then the python calls that method to determine its truth value.
- Suppose we want a bank account with 0 balance to be false ,we can set:
Account.__bool__ = lambda self: self.balance != 0
>>> bool(Account('Jack'))
False
>>> if not Account('Jack'):
print('Jack has nothing')
Jack has nothing
- Sequence Operations:
- the len function invoked the _len_ method of its argument to determine its lenth.
- The _getitem_ method is invoked by the element selection operator(often using square brackets) , but it can also be invoked directly.
>>> x = 'test'
>>> x.__len__()
4
>>> len(x)
4
>>> x[1]
'e'
>>> x.__getitem__(1)
'e'
- Callable objects
- In python , functions are first-class objects , so they can be passed around as data and have attributes like any other object
- Python also allows us to define objects that can be “called” like function by including a _call_ method.
# higher-order function
def make_adder(n):
"""
>>> add_three = make_adder(3)
>>> add_three(4)
7
"""
def adder(k):
return n + k
return adder
# define __call__
class Adder(object):
def __init__(self, n):
self.n = n
def __call__(self, k):
return self.n + k
add_three_obj = Adder(3)
add_three_obj(4)
# 7
Here the Adder class behaves like the make_adder higher-order function , and the add_three_obj object behaves like the add_three function. We have further blurred the line between data and functions.
Linked List
Linked List is very similar as Tree.
Using two different ways to contruct Linked List
- Linked list data abstraction
empty = 'empty'
def is_link(s):
""" s is a linked list if it is empty or a (first ,rest) pair"""
return s == empty or (len(s)==2 and is_link(s[1]))
def link(first,rest):
"""construct a linked list from its first element and the rest"""
assert is_link(rest), " rest must be a list"
return [first,rest]
def first(s):
""" return the first element of a linked list s"""
assert is_link(s),"first only applies to a linked lists"
assert s!= empty, "empty linked list has no first element"
return s[0]
def rest(s):
""" return the rest of the elements of a linked list s"""
assert is_link(s),"rest only applies to a linked lists"
assert s!= empty, "empty linked list has no rest"
return s[1]
-
Some useful function for Linked List
-
Using class
class Link:
empty = ()
def __init__(self, first, rest=empty):
assert rest is Link.empty or isinstance(rest,Link)
self.first = first
self.rest = rest
def __repr__(self):
if self.rest:
rest_repr = ', ' + repr(self.rest)
else:
rest_repr = ''
return 'Link(' + repr(self.first) + rest_repr +')'
def __str__(self):
string = '<'
while self.first is not Link.empty:
string += str(self.first) + ' '
self = self.rest
return string + str(self.first) + '>'
- Using recursion to defined some functions for class Link.
def range_link(start,end):
""" return a Link containing consecutive integers from start to end
>>> range_link(3,6)
Link(3, Link(4, Link(5)))
"""
if start >= end:
return Link.empty
else:
return Link(start , range_link(start+1,end))
def map_link(f,s):
""" return a Link that contains f(x) for each x in Link s
>>> map_link(square, range_link(3,6))
Link(9, Link(16, Link(25)))
"""
# if s.first == Link.empty:
if s is Link.empty:
return s
else :
return Link(f(s.first),map_link(f,s.rest))
def filter_link(f,s):
"""Return a Link that contains only the elements x of Link s for which f(x) is a true values
>>> filter_link(odd,range_link(3,6))
Link(3, Link(5))
"""
if s is Link.empty:
return s
filtered_rest = filter_link(f,s.rest)
if f(s.first):
return Link(s.first,filtered_rest)
else:
return filtered_rest
def extend_link(s1, s2):
if s1 is Link.empty:
return s2
else:
return Link(s1.first , extend_link(s1.rest, s2))
Hw05
store_digits(n)
def store_digits(n):
"""Stores the digits of a positive number n in a linked list.
>>> s = store_digits(1)
>>> s
Link(1)
>>> store_digits(2345)
Link(2, Link(3, Link(4, Link(5))))
>>> store_digits(876)
Link(8, Link(7, Link(6)))
>>> # a check for restricted functions
>>> import inspect, re
>>> cleaned = re.sub(r"#.*\\n", '', re.sub(r'"{3}[\s\S]*?"{3}', '', inspect.getsource(store_digits)))
>>> print("Do not use str or reversed!") if any([r in cleaned for r in ["str", "reversed"]]) else None
"""
"*** YOUR CODE HERE ***"
'''
len_n = 0
test = n
while test:
len_n += 1
test = test // 10
if not n:
return Link.empty
elif len_n ==1:
return Link(n)
else:
first = n // (10 ** (len_n-1))
other = n - first * (10 ** (len_n-1))
return Link(first , store_digits(other))
'''
def helper(n,prev):
if n<10:
return Link(n,prev)
else:
return helper(n//10,Link(n%10,prev))
return helper(n,Link.empty)
Efficiency
Growth Categories
Common orders of growth | time for input n+1 &【time for input n】 | time |
---|---|---|
exponentail growth : eg recursive fib | a ∗ b n + 1 = 【 a ∗ b n 】 ∗ b a * b^{n+1} = 【a * b^n】 *b a∗bn+1=【a∗bn】∗b | Θ ( b n ) \Theta(b^n) Θ(bn) |
quadratic growth : eg:overlap | a ∗ ( n + 1 ) 2 = 【 a ∗ n 2 】 + a ∗ ( 2 n + 1 ) a * (n+1)^2 = 【a * n^2】 + a*(2n+1) a∗(n+1)2=【a∗n2】+a∗(2n+1) | Θ ( n 2 ) \Theta(n^2) Θ(n2) |
linear growth: eg: slow exp | a ∗ ( n + 1 ) = 【 a ∗ n 】 + a a*(n+1) = 【a*n】+a a∗(n+1)=【a∗n】+a | Θ ( n ) \Theta(n) Θ(n) |
logarithmic growth : e.g.:exp_fast | a ∗ l n ( 2 ∗ n ) = 【 a ∗ l n n 】 + a l n 2 a*ln(2*n) = 【a*lnn】+ aln2 a∗ln(2∗n)=【a∗lnn】+aln2 | Θ ( l o g n ) \Theta(logn) Θ(logn) |
constant growth e.g.: abs | increasing n does not affect it | Θ ( 1 ) \Theta(1) Θ(1) |
def memo(f):
cache = {} # keys are arguments that map to return values
def memorized(n):
if n not in cache:
cache[n] = f(n)
return cache[n]
return memorized # same behavior as f ,if f is pure function
def count(f):
def counted(n):
counted.call_count +=1
return f(n)
counted.call_count = 0
return counted
def fib(n):
if n == 0 or n == 1:
return n
else:
return fib(n-2) + fib(n-1)
def overlap(a,b):
count = 0
for i in a:
for j on b:
if i == j:
count+=1
return count
def exp_fast(b,n):
if n ==0:
return 1
elif n % 2 == 0:
return square(exp_fast(b,n//2))
else:
return b*exp_fast(b,n-1)
def exp(b,n):
if n == 0:
return 1
else:
return b * exp(b,n-1)
def count_frame(f):
def counted(n):
counted.open_count += 1
if counted.open_count > counted.max_count:
counted.max_count = counted.open_count
result = f(n)
counted.open_count -=1
return result
counted.open_count = 0
counted.max_count = 0
return counted
Sets
Set literals follow the mathematical notion of elements enclosed in braces.
- Duplicate elements are removed upon construction
- Sets are unordered collections , and so the printed ordering may differ from the element ordering in the set literal.
- ‘+’ can not be used in Sets, instead using union and intersection.
- Sets are mutable , and can be changed one element at a time using add, remove, discard, and pop
- clear and update can provide multi-element mutations
>>> s = { 1, 2, 2, 4, 8, 5}
>>> s
{1, 2, 4, 5, 8}
>>> type(s)
<class 'set'>
>>> 3 in s
False
>>> 8 in s
True
>>> len(s)
5
>>> s.union({3,6,1})
{1, 2, 3, 4, 5, 6, 8}
>>> s.intersection({4, 5, 6, 7})
{4, 5}
>>> s
{1, 2, 4, 5, 8}
>>> s.add(6)
>>> s
{1, 2, 4, 5, 6, 8}
>>> s.remove(6)
>>> s
{1, 2, 4, 5, 8}
>>> s.discard(2)
>>> s
{1, 4, 5, 8}
# pop() for sets takes no argument
>>> s.pop()
1
>>> s
{4, 5, 8}
>>> s = {4, 5, 8}
>>> s.clear()
>>> s
set()
>>> s.update({1, 4, 3})
>>> s
{1, 3, 4}
Implementing sets
Sets as unordered sequences
One way to represent a set is a sequence where no element appears more than once.
- The implementation of set_contains requires Θ ( n ) \Theta(n) Θ(n) time on average to test membership of an element, where n is the size of the set s.
def empty(s):
return s is Link.empty
def set_contains(s, v):
"""return True if and only if set s contains v
>>> s = Link(3, Link(5, Link(2)))
>>> set_contains(s, 3)
True
>>> set_contains(s, 1)
False
"""
if empty(s):
return False
elif s.first = v:
return True
else:
return set.contains(s.rest, v)
- Using this linear-time function for membership , we can adjoin an element to a set , also in a linear time.
def adjoin_set(s, v):
""" return a set containing all elements of s and elements of v
>>> t = adjoin_set(s, 2)
>>> t
Link(2, Link(3, Link(5, Link(2))))
"""
if set_contains(s, v):
return s
else:
return Link(v, s)
- Intersecting two sets set1 and set2 also requires membership testing , but this time each element of set1 must be tested for membership in set2 , leading to a quadratic order of growth in the number of steps. Θ ( n 2 ) \Theta(n^2) Θ(n2) ,for two sets of size n.
def intersect_set(set1, set2):
"""return a set containing all elements common to set1 and set2
"""
return filter_link(lambda v:set_contains(set2 v), set1)
- Computing the union of two sets. The union_set function also requires a linear number of membership tests ,creating a process that also includes Θ ( n 2 ) \Theta(n^2) Θ(n2) steps
def union_set(set1, set2):
""" return aset containing all elements either in set1 or set2"""
set1_no_set2 = filter_link(lambda v:not set_contains(set2 v), set1)
return extend_link(set1_no_set2 ,set2)
Sets as ordered sequences
One way to speed up our set operations is to change the representation so that the set elments are listed in increasing order.
- One advantage of ordering shows up in set_contains : In checking for the presence of an object , we no longer have to scan the entire set . If we reach a set element that is larger that the item we are looking for , then we know that the item is not in the set.
- How many steps does it save?
- In ther worst case, the iterm we are looking for may be the largest one in the set, so the number of steps is the same as for the unordered representation. On the other hand, if we search for items of many different sizes we can expert that sometimes we will be able to stop searching at a point near the beginning of the list and other times we will still need to examine most of the list. On average we should expert to have to examine about half of the items in the set. Thus , the average numver of the steps required will be about n/2.
- How many steps does it save?
def set_contains(s, v):
if s.empty or s.first > v:
return False
elif s.first == v:
return True
else:
return set_contains(s.rest, v)
Sets as binary search trees
We can do better than the ordered-list representation by arranging the set elements in the form of a tree with exactly two branches.
The entry of the root of the tree holds one elements of the set. The entires within the left branch include all elements smaller than the one at the root. Entries in the right branch include all elements greater than the one at the root.
In all binary search trees, all elements in the left branch be smaller than the entry at the root , and that all elements in the right subtree be larger
- The advantage of the tree representation is this: Suppose we want to check whether a value v is contained in a set . We begin by comparing v with entry. If v is less than this ,we know that we need only search the left subtree , if v is greater , we need only search the right subtree. Now is the tree is “balanced”, each of these subtrees will be about half the size of the original. Thus , in one step we have reduced the problem of searching a tree of size n to searching a tree of size n/2. Since the size of tree is halved at each step, we should expect that the number of steps needed to search a tree grows as Θ ( l o g n ) \Theta(logn) Θ(logn). For large sets ,this will be a significant speedup over previous representations.
This set_contains function exploits the ordering structure of the tree-structured set:
def set_contains(s, v):
if s is None:
return False
elif s.entry == v:
return True
elif s.entry < v:
return set_contains(s.right, v)
elif s.entry > v:
return set_contains(s.left, v)