#!/usr/bin/env python # coding: utf-8 # # Useful utilites and functionalities in Python for IN3120/IN4120 # ## Python iterators # # Every iterable datastructure has an `__iter__()` method. # In[1]: # To get the iterator we call the iter() function mylist = ["Welcome", "to", "search", "technology"] my_iterator = iter(mylist) for i in my_iterator: print(i) # In[2]: # The iterator can be exhausted for i in my_iterator: print(i) # Note: when you for loop through an iterable, python calls `iter` and `next` under the hood # #### Manual traversal of iterator # we call next to get the next element from the iterator # In[3]: # Load a fresh iterator myit = iter(mylist) myit # In[4]: # When the iterator is empty, StopIteration is raised # In for loops it is caught and makes the loop terminate current = next(myit) current # In[5]: next(myit) # In[6]: next(myit) # In[7]: next(myit) # In[8]: next(myit) # In[10]: # Problem: we don't want an error raised when the iterator is empty # Solution: add a default value to the next function # i.e. iter(, ) myit = iter(mylist) # In[11]: next(myit, "Iterator is empty") # In[12]: next(myit, "Iterator is empty") # In[13]: next(myit, "Iterator is empty") # In[14]: next(myit, "Iterator is empty") # In[15]: next(myit, "Iterator is empty") # In[16]: # None is a typical default value # That makes it easy to check if the iterator is empty current = next(myit, None) if current: print("Iterator is not empty") else: print("Iterator is empty") # ### Generators # A generator is an iterator, but not all iterators are generators # In[17]: def my_iterator(): return iter(range(5)) def my_generator(): for i in range(5): yield i # Note: use of nonlocal variable for i in ["Welcome", "to", "search", "technology"]: yield i my_generator # In[18]: my_gen = my_generator() my_gen # In[19]: next(my_gen) # In[20]: for i in my_gen: print(i) # In[21]: # Extra: if you want to yield all elements from an iterable, use "yield from syntax" def example(): yield from range(1, 10) yield from range(10, 0, -1) list(example()) # ## Zip function # Ever wanted to iterate from two iterables simultaneously? # Or perhaps wrap two lists into a list of pairs? # Or perhaps wrap n lists into a list of n-tuples? # In[22]: numbers = [5, 6, 7] chars = ["b", "c", "d"] # Basic method for i in range(len(numbers)): print(numbers[i], chars[i]) # Equivalent with zip for n, c in zip(numbers, chars): print(n, c) # In[23]: # Zip returns an iterator it = zip(numbers, chars) # In[24]: # Note that the pairs are tuples and not lists next(it) # In[25]: list(zip(numbers, chars)) # ## List comprehensions # In[26]: # % is the modulo opreand: it returns the remainder of the left number divided by the right number def is_odd(n): return n % 2 # In[27]: # say we want a list of squares of 0 through 9 [i*i for i in range(10)] # In[28]: # say we only want odd [i*i for i in range(10) if not is_odd(i)] # In[29]: # say if the number is odd, it is swapped out with 0 [i*i if i % 2 == 0 else 0 for i in range(10)] # # syntax: # ```Python # [(expression) for i in (iterable)] # [(expression) for i in (iterable) if (condition)] # [(expression) if (condition) else (expression) for i in (iterable)] # [(expression) for (iterable) in (nestediterable) for i in (iterable)] # ``` # ## Dict comprehensions # In[30]: {i:i.upper() for i in mylist} # Syntax: # ```Python # {(key expression):(value expression) for i in (iterable)} # ``` # ## Generator comprehensions # Like list comprehension, but uses parentheses instead of brackets # In[31]: myit = (i*i if (i%2==0) else 0 for i in range(10)) # ## Passing generator comprehensions # you dont need to put parentheses if the generator comprehension is the only argument for a function # This lets us create comprehensions for any datastrucutre that can take iterables as inputs. # Very elegant and pythonic # In[32]: sum(i*i for i in range(10)) # In[33]: set(i*i for i in range(10)) # ## Counters # # Counter is a subclass of dict in python. It takes in an iterable of anything hashable and creates each unique element as key and its frequency as value # In[34]: from collections import Counter documents = [ "I am a document", "I am an an an as", "I'm very very happy", "Ha ha ha ha" ] Counter(documents) # In[35]: # Normalization and tokenization tokenized_documents = [doc.lower().split() for doc in documents] tokenized_documents # In[36]: c1 = Counter(token for tokens in tokenized_documents for token in tokens) c1 # In[37]: c2 = Counter([0,1,1,2,6,6,5,4, "i", "i"]) c2 # In[38]: # Counters support additions, so that you can merge two counters c1 + c2 # In[39]: mylist = [[1,2,3], [4,5,6], [7,8,9]] # In[40]: flatlist = [] for sublist in mylist: for i in sublist: flatlist.append(i) # In[41]: [i for sublist in mylist for i in sublist] # ## Type hints # Type hints can be great for readability and is used quite a lot in the assignments. # It is a quite new python feature, but does not actually affect how the code runs # In[42]: def numerical_function(n: int, k: float) -> complex: return n + k + 1j def numerical_function_without_type_hints(n, k): return n + k + 1j # In[43]: # You can call the help function and see the type hints help(numerical_function) help(numerical_function_without_type_hints) # In[44]: # Type hints are only for the reader of the code. There is no type checking def numerical_function(n: int, k: float) -> complex: return "kødda" # In[45]: # For some typing, you will have to import auxiliary typing classes # Say if you'd like to know what is in the list that you are returning def list_of_letters() -> list: return ["a", "b", "c"] from typing import List def list_of_letters() -> List[str]: return ["a", "b", "c"] # ## Abstract classes # Unlike java, abstract classes are not built in the syntax of python. # However, there is a built in module that makes this possible # In[46]: from abc import ABC, abstractmethod # In[47]: class Shape(ABC): def foo(self): return 42 @abstractmethod def area(self): pass # In equivalent java code: # ```Java # abstract class Shape { # public int foo() { # return 42; # } # # public abstract double area(); # } # ``` # In[48]: class Square(Shape): def __init__(self, length): self.length = length Square(5) # In[49]: class Square(Shape): def __init__(self, length): self.length = length def area(self): return self.length ** 2 Square(5).area() # In[ ]: # In[ ]: