Python's Data Structures: Basics and Advanced
PyRoma 2015
About Me
Full Name: Andrea Iuliano
Age: 25
Master Degree: Computer Science
At: UniRoma3
Passions:
- Computer Vision
- Videogames
- Animals (reptiles lover :D)
Contacs:
- github.com/Pausa90
- andreaiuliano90@gmail.com
Computer Vision Engineer
"if I can see it, It can see it"
More data structures?
We already have a general purpose data structures which providing a containing functions:
- list
- tuple
- set
- frozenset
- dict
List
The most basic data structure offered by Python.
Each element is stored into a sequence space and it can be accessed by an index (eg. an integer number).
list1 = ['physics', 'chemistry', 1997, 2000];
list2 = [1, 2, 3, 4, 5 ];
list3 = ["a", "b", "c", "d"];
List
Also they can be used as a stack structure, offering in an easily way LIFO accessing.
>>> stack = [3, 4, 5]
>>> stack.append(6)
>>> stack.append(7)
>>> stack
[3, 4, 5, 6, 7]
>>> stack.pop()
7
>>> stack
[3, 4, 5, 6]
Tuple
An immutable collection of elements. You cannot update or change the value of tuple's elements or its size.
>>> tupl = (1, 2, 3, 4, 5, 6, 7 )
>>> tupl[0]
1
>>> tupl[0] = 10
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-3-947ebd095daf> in <module>()
----> 1 tupl[0] = 10
1
TypeError: 'tuple' object does not support item assignment
Set
It is an unordered collection with no duplicated elements. It supports mathematical operations like union, intersection and difference.
>>> set1 = set(range(0,5))
>>> set2 = set(range(3,8))
>>> set1
{0, 1, 2, 3, 4}
>>> set2
{3, 4, 5, 6, 7}
>>> set1.union(set2)
{0, 1, 2, 3, 4, 5, 6, 7}
>>> set1.intersection(set2)
{3, 4}
>>> set1.difference(set2)
{0, 1, 2}
Text
Frozenset
Same as set with an immutable feature
>>> frozen_set = frozenset(range(0,5))
>>> frozen_set
frozenset({0, 1, 2, 3, 4})
>>> frozen_set.
frozen_set.copy frozen_set.issubset
frozen_set.difference frozen_set.issuperset
frozen_set.intersection frozen_set.symmetric_difference
frozen_set.isdisjoint frozen_set.union
Dict
A dictionary is an unordered set of (key : value) pairs, with a unique key value.
>>> dict = {'Name': 'Valerio', 'Age': 25, 'Name': 'Andrea'};
>>> dict
{'Age': 25, 'Name': 'Andrea'}
>>> dict['Name'];
'Andrea'
>>> dict.keys()
['Age', 'Name']
>>> dict.values()
[25, 'Andrea']
Advanced Collections
" So why we need it? We have a useful primitive data structures, that we can combine as we like it! "
The correct answer is:
- code reuse
- no bugs "guaranteed"
- high-performance guaranteed
Collections
Introduced with Python2:
- Counter
- deque
- defaultdict
- namedtuple
- OrderedDict
Introduced with Python3:
- ChainMap
- UserDict
- UserList
- UserString
Counter
It is a dict subclass which can easily count hashable objects.
>>> char_counter = Counter("Hi, it works even with strings")
>>> char_counter
Counter({' ': 5, 'i': 4, 's': 3, 't': 3, 'e': 2, 'n': 2, 'r': 2, 'w': 2,
'g': 1, 'H': 1, 'k': 1, ',': 1, 'o': 1, 'v': 1, 'h': 1})
>>> string_counter = Counter(["Hi", ",", "it", "works", "even", "with",
"strings"])
>>> string_counter
Counter({'even': 1, ',': 1, 'it': 1, 'Hi': 1, 'works': 1, 'with': 1,
'strings': 1})
>>> string_counter.elements()
<itertools.chain at 0x7fe9b37ebb10>
>>> list(string_counter.elements())
['even', ',', 'it', 'Hi', 'works', 'with', 'strings']
Counter
>>> # Find the ten most common words in Hamlet
>>> import re
>>> words = re.findall(r'\w+', open('hamlet.txt').read().lower())
>>> Counter(words).most_common(10)
[('the', 1143), ('and', 966), ('to', 762), ('of', 669), ('i', 631),
('you', 554), ('a', 546), ('my', 514), ('hamlet', 471), ('in', 451)]
>>> # Some mathematicals examples
>>> c = Counter(a=3, b=1)
>>> d = Counter(a=1, b=2)
>>> c + d # add two counters together: c[x] + d[x]
Counter({'a': 4, 'b': 3})
>>> c - d # subtract (keeping only positive counts)
Counter({'a': 2})
>>> c & d # intersection: min(c[x], d[x])
Counter({'a': 1, 'b': 1})
>>> c | d # union: max(c[x], d[x])
Counter({'a': 3, 'b': 2})
Deque
A generalization of stacks and queues.
Advantages:
- thread-safe
- memory efficients appends and pops (both sides) with approximately O(1)
- Can create infinite size deque or set a maxlen value. If it is setted and if the deque is full, the newest element pop out the oldest in the other side
Deque
>>> deq = deque([], 3)
>>> deq.append(1)
>>> deq
deque([1], maxlen=3)
>>> deq.append(2)
>>> deq.append(3)
>>> deq
deque([1, 2, 3], maxlen=3)
>>> deq.append(4)
>>> deq
deque([2, 3, 4], maxlen=3)
>>> deq.appendleft(5)
>>> deq
deque([5, 2, 3], maxlen=3)
Deque
>>> #Rotations
>>> deq = deque([5, 2, 3], maxlen=3)
>>> deq.rotate(1)
>>> deq
deque([3, 5, 2], maxlen=3)
>>> deq.rotate(2)
>>> deq
deque([5, 2, 3], maxlen=3)
>>> deq.rotate(-3)
>>> deq
deque([5, 2, 3], maxlen=3)
>>> # Linux Tail Implementation
>>> def tail(filename, n):
... 'Return the last n lines of a file'
... with open(filename) as f:
... return deque(f, n)
Deque
>>> def moving_average(iterable, n=3):
... # Get an iterator from iterable object
... it = iter(iterable)
...
... # Create a deque as a n-size window
... deq = deque(itertools.islice(it, n-1))
... deq.appendleft(0)
... tot = sum(deq)
...
... # Iterate the list and get the mean for each window
... for elem in it:
... popped = deq.popleft()
... tot += elem - popped
... deq.append(elem)
... yield tot / float(n)
>>> raw_list = map(lambda x: x*5, range(0,10))
>>> print "input: ", raw_list
input: [0, 5, 10, 15, 20, 25, 30, 35, 40, 45]
>>> average = moving_average(raw_list)
>>> print "output: ", list(average)
output: [5.0, 10.0, 15.0, 20.0, 25.0, 30.0, 35.0, 40.0]
Deque
Defaultdict
It is a dictionary like an object which provides all methods provided by dictionary with less code and the same speed.
>>> s = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)]
>>> d = defaultdict(list)
>>> for k, v in s:
... d[k].append(v)
...
>>> d.items()
[('blue', [2, 4]), ('red', [1]), ('yellow', [1, 3])]
Defaultdict
>>> #Dict of random int
>>> rand_dict = defaultdict(lambda: randint(0,3))
>>> rand_dict
defaultdict(<function <lambda> at 0x7f9254f0b1b8>, {})
>>> for num in range(5):
... rand_dict[num]
>>> rand_dict
defaultdict(<function <lambda> at 0x7f9254f0b1b8>, {0: 1, 1: 1, 2: 0, 3: 2, 4: 1})
Namedtuple
A factory of hybrid "tuple-dictionary" object: it returns a tuple charaterized by:
- name
- accessing by name and by indexing
- does not have real keys -> no hashability issues
- immutability values
Namedtuple
>>> Point = namedtuple('Point', ['x', 'y']) # Defining the namedtuple
>>> p = Point(10, y=20) # Creating an object
>>> p
Point(x=10, y=20)
>>> p.x + p.y
30
>>> p[0] + p[1] # Accessing the values in normal way
30
>>> x, y = p # Unpacking the tuple
>>> x
10
>>> y
20
Namedtuple: in depth
>>> Point = namedtuple('Point', ['x', 'y'], verbose=True)
class Point(tuple):
'Point(x, y)'
__slots__ = ()
_fields = ('x', 'y')
def __new__(_cls, x, y):
'Create a new instance of Point(x, y)'
return _tuple.__new__(_cls, (x, y))
@classmethod
def _make(cls, iterable, new=tuple.__new__, len=len):
'Make a new Point object from a sequence or iterable'
result = new(cls, iterable)
if len(result) != 2:
raise TypeError('Expected 2 arguments, got %d' % len(result))
return result
def __repr__(self):
'Return a nicely formatted representation string'
return 'Point(x=%r, y=%r)' % self
def _asdict(self):
'Return a new OrderedDict which maps field names to their values'
return OrderedDict(zip(self._fields, self))
def _replace(_self, **kwds):
'Return a new Point object replacing specified fields with new values'
result = _self._make(map(kwds.pop, ('x', 'y'), _self))
if kwds:
raise ValueError('Got unexpected field names: %r' % kwds.keys())
return result
def __getnewargs__(self):
'Return self as a plain tuple. Used by copy and pickle.'
return tuple(self)
__dict__ = _property(_asdict)
def __getstate__(self):
'Exclude the OrderedDict from pickling'
pass
x = _property(_itemgetter(0), doc='Alias for field number 0')
y = _property(_itemgetter(1), doc='Alias for field number 1')
OrderedDict
A regular dictionary which is able to remember the order that items were inserted
>>> # regular unsorted dictionary
>>> d = {'banana': 3, 'apple':4, 'pear': 1, 'orange': 2}
>>> # dictionary sorted by key
>>> OrderedDict(sorted(d.items(), key=lambda t: t[0]))
OrderedDict([('apple', 4), ('banana', 3), ('orange', 2), ('pear', 1)])
>>> # dictionary sorted by value
>>> OrderedDict(sorted(d.items(), key=lambda t: t[1]))
OrderedDict([('pear', 1), ('orange', 2), ('banana', 3), ('apple', 4)])
Collections ABC
Collections offers many Abstract Base Classes to entice the use of these data structures, even if you want to build your own collection.
class collections.Container
class collections.Hashable
class collections.Sized
class collections.Callable
class collections.Iterable
class collections.Iterator
class collections.Sequence
class collections.MutableSequence
class collections.Set
class collections.MutableSet
class collections.Mapping
class collections.MutableMapping
class collections.MappingView
class collections.ItemsView
class collections.KeysView
class collections.ValuesView
ChainMap
It provides a quickly linking between many mappings, so they can be treated as a single unit.
Features:
- the mappings are stored in a public and updatable list (there are no other states)
- writes, updates and deletions only operate on the first mapping
- the underlying mapping is stored by references -> auto updating mapping
ChainMap
>>> dict1 = { "Name": "Andrea", "Surname": "Iuliano", "Age": 25 }
>>> dict2 = { "Name": "Valerio", "Surname": "Rossi", \
"Hobbies": ["skate", "snowboard", "reptiles", "videogames"] }
>>> chain = ChainMap(dict1, dict2)
>>> chain
ChainMap({'Name': 'Andrea', 'Age': 25, 'Surname': 'Iuliano'}, \
{'Name': 'Andrea', 'Surname': 'Iuliano', \
'Hobbies': ['skate', 'snowboard', 'reptiles', 'videogames']})
>>> chain['Name']
'Andrea'
>>> chain['Hobbies']
['skate', 'snowboard', 'reptiles', 'videogames']
UserDict, UserList e UserString
These three classes are wrappers around dictionary/list/string objects and allow you to treat it as a corrisponding class.
To get these functionality it needs to:
- create a subclass from UserDict/UserList/UserString
- declare self.data as a correspondent empty type
- invoke self.update(args) to update all the changes in self.data
UserDict
>> class User(UserDict):
...
... def __init__(self, name, surname, age):
... self.data = {}
... self.name = name
... self.surname = surname
... self.age = age
... self.update({"name":name, "surname":surname, "age":age})
...
... def changeName(self, new_name):
... self.name = new_name
... self.update({"name":new_name})
...
>>> me = User("Andres", "Iuliano", 25)
>>> me.changeName("Andrea")
>>> print(me)
{'age': 25, 'name': 'Andrea', 'surname': 'Iuliano'}
>>> print(me['age'])
25
Any
Questions?
The end, thank you!
github.com/Pausa90
andreaiuliano90@gmail.com
slides.com/pausa/deck/
Python's Data Structures: Basics and Advanced
By Andrea Iuliano
Python's Data Structures: Basics and Advanced
- 756