---
# System prepended metadata

title: Python

---

# Python

## Main characteristics

### Types:

* Dynamic typed: not specify types in advance and a variable can be assigned to different types.
* Strongly typed: types are not coerced to match.

### “Interpreted language”

* Compiled before executed transparently

### Implementations of the Python language:

* CPython:
	* In C runs native. 
	* Commonly used, standard de facto.
* Jython:
	* In Java runs on JVM.
* IronPython: 
	* In C# runs in .NET.
* pypy: 
	* RPython runs native.

### Python standard library

* Extensive. Most of the cases you won’t need anything else.
* Good documentation.
* Simplicity, readability and consistency.

```
>>> import math
>>> help()
>>> help(math)
>>> help(math.factorial)
>>> math.factorial(6)
>>> from math import factorial as fact
>>> fact(6)
```

### Python main release streams:

* Python 2. Broadly used. Support ends in 2020.
* Python 3. Around for more than 10 years. 
* Incompatible.

### Installation

Virtual environments: light-weight, self-contained Python installation.

venv or virtualenv

```
$ python3 -m venv venv3
$ source venv3/bin/activate
(venv3) $
(Venv3) $ deactivate
$
```

### Python Interpreter

*REPL: Read Evaluate Print Loop*

_ =>  Last return value. (Only in REPL).

Exit: Ctrl + D in Linux and Mac

```
$ python3
>>> 2+2
4
>>> _ + 1
5
>>> print("Hello World!")
Hello World!
>>> 
```

### Other important things to consider

**Important!** Indentation level defines the structure of the program. Instead of braces like many other languages. Not convinced. Be careful with using different IDE setups: NEVER mix spaces and tabs! 

### Python Enhancement Proposals (PEPs)
Python language guidances:

* PEP8: Python Style Guide. E.g. Use 4 spaces for indentation.
* PEP20: The Zen of Python. 
* ...

```
>>> import this
```
## Python data types

### Basic Types

#### int
UNLIMITED precision signed integer

```
>>> 10 (decimal)
>>> 0b10 (binary)
>>> 0o10 (onto)
>>> 0x10 (hexadecimal)
>>> int(-3.5)
>>> int(“496")
```

#### float
IEEE-754 double precision (64 bits) => 53 bits of binary precision and 15-16 bits of decimal precision.

```
>>> 3.123
3.123
>>> 3e0
3.0
>>> 1.245e-190
1.245e-190	
>>> float("1.3")
1.3
>>> 3E8
300000000.0
>>> float(7)
7.0
>>> float("nan")
nan
>>> float("inf")
inf
>>> import sys
>>> sys.float_info
sys.float_info(max=1.7976931348623157e+308, max_exp=1024, max_10_exp=308, min=2.2250738585072014e-308, min_exp=-1021, min_10_exp=-307, dig=15, mant_dig=53, epsilon=2.220446049250313e-16, radix=2, rounds=1)
```

#### Decimal
The standard library module decimal contains the class Decimal, which represents decimals in base 10 instead of base 2 and has a configurable (although finite) precision, which defaults to 28 digits of decimal precision.

```
>>> import decimal
>>> decimal.getcontext()
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[InvalidOperation, DivisionByZero, Overflow])
>>> from decimal import Decimal
>>> Decimal(7)
Decimal('7')
>>> Decimal('0.8')
Decimal('0.8')
>>> 0.8 - 0.7
0.10000000000000009
>>> Decimal(0.8) - Decimal(0.7)
Decimal('0.1000000000000000888178419700')
>>> Decimal('0.8') - Decimal('0.7')
Decimal('0.1')
>>> Decimal('Infinity')
Decimal('Infinity')
>>> Decimal('-Infinity')
Decimal('-Infinity')
>>> Decimal('Nan')
Decimal('NaN')
>>> Decimal('Nan') + Decimal('1.414')
Decimal('NaN')
```
Decimal is preferred in financial applications such as accounting.

#### Fraction
Rational numbers: Fraction class from fractions module.

```
F = Fraction(“2/3”)
```

#### None
Special type that represents the absence of a value.

```
>>> None
>>> a = None
>>> a is None
True
>>> a = 1
>>> a is None
False
```

#### Bool
Logical value. True or False

```
>>> True
True
>>> False
False
>>> bool(0)
False
>>> bool(42)
True
>>> bool(-1)
True
>>> bool(0.0)
False
>>> bool(-0.3)
True
>>> bool([])
False
>>> bool([1])
True
>>> bool("")
False
>>> bool("kk")
True
>>> bool("False")
True
```

**NOTE:** Comparison:

* `==` : reference equality
* `is` : identity equality

### Collection types

#### Tuple
Heterogeneous immutable collections.

```
>>> t = ("kk", 4.5, 3)
>>> t
('kk', 4.5, 3)
>>> t[0]
'kk'
>>> len(t)
3
>>> h = (1)
>>> type(h)
<class 'int'>
>>> k = (1,)
>>> type(k)
<class 'tuple'>
```

#### Str
Immutable sequences of unicode codepoints.

* Simple or doble quotes.
* Multiline strings: `“””…”””` or `‘…\n…’`
* Raw strings: `r”…”` (doesn’t interpret the `\`}})

Strings are enriched with many methods:

```
>>> c.capitalize()
```

To concatenate invoke join on empty text (more efficient):

```
>>> "".join(["He", "llo"])
'Hello'
```

The partition() method divides a string into 3 around a separator: prefix, separator, suffix

```
>>> origin, _, destination = "Seattle-Boston".partition('-')
>>> origin
'Seattle'
>>> destination
'Boston'
Tuple unpacking - return values and idiomatic swap.
```
Use format() to insert values into strings.

```
>>> "The age of {0} is {1}".format('Jim', 33)
'The age of Jim is 33'
>>> "Reticulating spline {} of {}".format(4,23)
'Reticulating spline 4 of 23'
>>> "Math constants: pi={m.pi:.3f}, e={m.e:.3f}".format(m=math)
'Math constants: pi=3.142, e=2.718'
```

#### Bytes 

Sequences of bytes.

```
>>> d = b"Hello"
```

#### Range 

Arithmetic progression of integers.

```
>>> for i in range(5):
...     print(i)
... 
0
1
2
3
4
range(5): 0,1,2,3,4
range(5,10): 5,6,7,8,9
range(10,20,2): 10,12,14,16,18
```

Prefer enumerate() for counters:

```
>>> t = ["a", "b", "c", "d"]
>>> for p in enumerate(t):
...     print(p)
... 
(0, 'a')
(1, 'b')
(2, 'c')
(3, 'd')
>>> for i, v in enumerate(t):
...     print("i = {}, v = {}".format(i, v))
... 
i = 0, v = a
i = 1, v = b
i = 2, v = c
i = 3, v = d
```

#### List

Mutable, heterogeneous sequence of objects.

```
>>> [1,2,3]
[1, 2, 3]
>>> a = []
>>> a.append(1.2)
>>> a
[1.2]
>>> list("hello")
['h', 'e', 'l', 'l', 'o']
```

You can an access elements of the list with positive indexes from the beginning and negative from the end.

```
>>> s = "show how to index into sequences".split()
>>> s
['show', 'how', 'to', 'index', 'into', 'sequences']
>>> s[5]
'sequences'
>>> s[-1]
'sequences'
```

Slicing extracts part of a list: slice = seq[start:stop]

```
>>> s[1:2]
['how']
>>> s[:3]
['show', 'how', 'to']
>>> s[3:]
['index', 'into', 'sequences']
```

Copy a list:

```
>>> copy_of_s = s[:]
```
Shallow copy (references are copied by objects are not).

```
>>> s.sort(key=len)
>>> s
['to', 'how', 'show', 'into', 'index', 'sequences']
```

#### Dictionaries

Key, value pairs. Order is arbitrary!

```
{1: “a”, 2: “b”}
```

* Keys must be immutable
* Values may be mutable

```
>>> m = { 'H': [1,2,3],'He': [3,4], 
... 'Li': [6,7], 'Be': [7,9,10], 'B': [10,11], 'C': [11,12,13,14] }
>>> 
>>> m
{'H': [1, 2, 3], 'He': [3, 4], 'Li': [6, 7], 'Be': [7, 9, 10], 'B': [10, 11], 'C': [11, 12, 13, 14]}
>>> m['H'] += [4,5,6,7]
>>> m
{'H': [1, 2, 3, 4, 5, 6, 7], 'He': [3, 4], 'Li': [6, 7], 'Be': [7, 9, 10], 'B': [10, 11], 'C': [11, 12, 13, 14]}
>>> from pprint import pprint as pp
>>> pp(m)
{'B': [10, 11],
 'Be': [7, 9, 10],
 'C': [11, 12, 13, 14],
 'H': [1, 2, 3, 4, 5, 6, 7],
 'He': [3, 4],
 'Li': [6, 7]}
```

#### Set
Unordered collection of unique, immutable objects.

```
>>> p = {6, 28, 496, 81282, 473824}
>>> s = set()
>>> s
set()
```

Adding elements:

```
>>> k = {1,2}
>>> k.add(3)
>>> k
{1, 2, 3}
>>> k.add(1)
>>> k
{1, 2, 3}
>>> k.update([4,5,6])
>>> k
{1, 2, 3, 4, 5, 6}
```

Removing elements:

```
>>> k.remove(1)
>>> k.remove(11)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 11
>>> k
{2, 3, 4, 5, 6}
>>> k.discard(2)
>>> k.discard(22)
```

#### Implementing Collections

Protocols such as sized, iterable, sequence and container characterise the collections.

* Container protocol:
    * `__contains__()`
* Sized protocol:
	 * ` __len__()`
* Iterable protocol:
    *  `__iter__()`
* Sequence protocol:
    * `__getitem__()`
    * `__reversed__()`
    * `index()`
    * `count()`
    * `__add__()`
    * `__mul__()`
    * `__rmul__()`
* String representation:
    * `__repr__()`
* Value equality:
    * `__eq__()`
    * `__ne__()`

`collections.abc`: abstract base classes that define and assist on the collection protocols:

* Container: container protocol
* Sized: sized protocol
* Sequence: sequence protocol
* Set
* …

## Modularity

Reusability. Files with *.py extension. They can be executed directly with:

```
$ python module_name.py
```
Imported into the REPL or other modules with:

```
import module_name
```

Use `__name__` to determine how the module is being executed. `__main__` means that it is being executed directly as an executable.

```
if __name__ == '__main__':
    fetch_words()
```

This allows the function to be executed automatically when invoking the module as a script but not when importing it as a library.

## Objects

Everything is an Object, including modules and functions (they can be treated just like other objects). Variables are named references to objects. The GC reclaims unreachable objects.`id()` returns the unique identifier of the object (debugging purpose).

Function arguments are passed by object-reference, so functions can modify mutable arguments. Return statement also is passed by reference.

Function arguments can be specified with defaults. Default argument expressions are evaluated once, when def is executed.

Scopes:

* Local
* Enclosing
* Global
* Built-ins.

Use global keyword to assign to global references from a local scope.

* `type()` can be used to determine the type of an object.
* `dir()` can be used to introspect an object and get its attributes.

## Handling Exceptions

Exception handling is a mechanism for stopping “normal” program flow and continuing at some surrounding context or code block. Raising an exception interrupts normal program flow and transfers control to an exception handler. Exception handlers defined using the `try…except` construct.

Use common or existing exception types when possible (*IndexError*, *KeyError*, *ValueError*, *TypeError*, etc).

Avoid protecting against *TypeErrors*. Just let the code fail.

2 philosophies when dealing with failures:

* Look Before You Leap (LBYP).
* It’s Easier to Ask Forgiveness than Permission (EAFP). This is Python's culture.

Resource cleanup with `finally` block.

## Iterables

### Comprehensions

Concise syntax for describing lists, sets or dictionaries in a declarative or functional way.

* List comprehension: `[ expr(item) for item in iterable]`
* Set comprehension: `{ expr(item) for item in iterable}`
* Dictionary comprehension: `{ key_expr:value_expr for item in iterable }`

```
>>> words = "Why sometimes I have believed as many as six impossible things before breakfast".split()
>>> words
['Why', 'sometimes', 'I', 'have', 'believed', 'as', 'many', 'as', 'six', 'impossible', 'things', 'before', 'breakfast']
>>> [len(word) for word in words]
[3, 9, 1, 4, 8, 2, 4, 2, 3, 10, 6, 6, 9]
>>> from math import factorial
>>> f = [len(str(factorial(x))) for x in range(20)]
>>> f
[1, 1, 1, 1, 2, 3, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 18]
>>> type(f)
<class 'list'>
>>> s = {len(str(factorial(x))) for x in range(20)}
>>> s
{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 18}
>>> type(s)
<class 'set'>
>>> from pprint import pprint as p
>>> from pprint import pprint as pp
>>> country_to_capital = {'UK': 'London',
...                       'Spain': 'Madrid',
...                       'Morocco': 'Rabat',
...                       'France': 'Paris'}
>>> capital_to_country = {capital:country for country,capital in country_to_capital.items()}
>>> capital_to_country
{'London': 'UK', 'Madrid': 'Spain', 'Rabat': 'Morocco', 'Paris': 'France'}
```

Comprehensions operate on an iterable source object and apply an optional predicate filter and a mandatory expression, both of which are usually in terms of the current item.

Iterables are objects over which we can iterate item by item. They have to implement the `__iter__()` method.

We retrieve an iterator from an iterable object using the built-in `iter()` function.

Iterators produce items one-by-one-from the underlying iterable series each time they are passed to the built-in `next()` function. When there are not more elements to retrieve, `StopIteration` exception is raised.

Comprehensions can have several sequences (nested loops), conditions and other comprehensions.

### Generators 

Provide means of describing iterable series in a descriptive way.

Generator functions contain at least one use of the yield keyword.

Generators are iterators. When advanced with `next()` the generator starts or resumes execution up too and including the next yield.

Each call to a generator function creates a new generator object.

Generators can maintain explicit state in local variables between iterations.

Generators are lazy, and so can model infinite series of data.

Generator expressions have a similar syntactic form to list comprehensions and allow for a more declarative and concise way of creating generator objects. Replace [] by ()

```
(expr(item) for item in iterable)
```

```
>>> first_squares = (x*x for x in range(1, 101))
>>> list(first_squares)
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361, 400, 441, 484, 529, 576, 625, 676, 729, 784, 841, 900, 961, 1024, 1089, 1156, 1225, 1296, 1369, 1444, 1521, 1600, 1681, 1764, 1849, 1936, 2025, 2116, 2209, 2304, 2401, 2500, 2601, 2704, 2809, 2916, 3025, 3136, 3249, 3364, 3481, 3600, 3721, 3844, 3969, 4096, 4225, 4356, 4489, 4624, 4761, 4900, 5041, 5184, 5329, 5476, 5625, 5776, 5929, 6084, 6241, 6400, 6561, 6724, 6889, 7056, 7225, 7396, 7569, 7744, 7921, 8100, 8281, 8464, 8649, 8836, 9025, 9216, 9409, 9604, 9801, 10000]
>>> list(first_squares)
[]
>>> sum(x*x for x in range(1, 101))
338350
```

### Iteration tools

Built-ins such as:

* `map(func, *iterables)`: lazy
* `filter(func, iterable)`: lazy

```
>>> positives = filter(lambda x: x>0, [1,-5,0,6,-2,8])
>>> positives
<filter object at 0x1044fff60>
>>> list(positives)
[1, 6, 8]
Passing None as the first argument to filter() will remove elements which evaluate too False.
```

**NOTE:** Difference with Python 2.x, where `map()` and `filter()` are eagerly evaluated and return list objects.

* `sum()`
* `any()`
* `zip()`
* `all()`
* `min()`
* `max()`
* `enumerate()`

Standard library `itertools` module:

* `chain()`
* `islice()`
* `count()`
* …

Standard library functools module:

* `reduce(func, sequence[, initial_value]) -> value`: repeatedly apply a function to the elements of a sequence, reducing them to a single value. 

```
>>> from functools import reduce
>>> import operator
>>> reduce(lambda x, y: x+y, [1, 2, 3, 4, 5])
```

## Classes

All types in Python have a class. Class define the structure and behaviour of objects.
An object’s class controls its initialisation.

Instance methods must always accept as first argument a reference to the instance, by convention called `self`.

```
>>> from airtravel import Flight
>>> f = Flight()
>>> f.number()
'SN060'
>>> Flight.number(f)
```

Method to initialise new objects: `__init__()`
**NOTE:** It is not a constructor. The object has been already created by the time this method is called. It is an initialiser.

In Python everything is public, names starting with `_` indicate that they are internal implementation and they shouldn’t be accessed directly.

```
>>> from airtravel import *
>>> f = Flight("BA748", Aircraft("G-EUPT", "Airbus A319", num_rows=22, num_seats_per_row=6))
>>> from pprint import pprint as pp
>>> pp(f._seating)
[None,
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
…]
```

Class attributes: value shared by all the instances of the class. Defined in the scope of the class. Can be access by class name or self. 

**WARNING:** Trying to assign a new value to a class attribute with self will create a new instance attribute instead.

Class methods: methods defined at the class level instead of the instance level.

* `@staticmethod` decorator:  Use when no access needed to either class or instance objects. 

* `@classmethod` decorator: use it when the method requires access to the class object to call other class methods or the constructor. Typical use case: named constructors.

```
	@classmethod
	def a_anamed_constructor(cls):
		return cls()
```

object: the core of the Python object model, is the ultimate base class for every class. It is automatically added as a base class.

### Polymorphism

Using objects of different types through a common interface.

Achieved through Duck Typing: 

*“When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.” James Whitcomb Riley.*

**NOTE:** For static methods to have polymorphic behaviour you have to invoke them via the instance with self.method_name instead of class_name.method_name.

### Inheritance

A sub-class can derive from a base-class, inheriting its behaviour and making behaviour specific to the sub-class.

* `isinstance()`: determines if an object is of a specific type.
* `issubclass()`: determines if one type is a subclass of another.

Multiple inheritance: defining a class with more than one base class.

```
  class SubClass(BaseClass1, BaseClass2, …)   
```

If a class has multiple base classes and defines no initialiser, only the initialiser of the first base class is automatically called.

`__bases__` : tuple of base classes.

**Method resolution order (MRO):** ordering that determines method name lookup. Looks for the method name in order of the base classes definition. **C3 algorithm**:
	
* subclasses come before base classes.
* base classes order from class definition is preserved.
* First 2 qualities are preserved no matter where you start in the inheritance graph.

```
ClassName.__mro__
```

`super():` given a method resolution order and a class C, super() gives you an object which resolves methods using only the part of the MRO which comes after C. It returns a proxy object which routes method calls.

* `super(base-class, derived-class)` => class super proxy
* `super(class, instance-of-class)` => instance super proxy
* `super() == super(class-of-method, self)` or `super(class-of-method, class)`

`super()` uses everything after a specific class in an MRO to resolve method calls.

In Python inheritance is not needed to achieve polymorphism, so it is mainly used for sharing implementation.

### Encapsulation

`@property` and `@p.setter` decorators.

```
class Example:

	@property
	def p(self):
		return self._p
	
	@p.setter
	def p(self, value):
		self._p = value
```

To overwrite properties in a hierarchy follow the Template Method design pattern.

## Files

**With block:** resource cleanup for objects that support the context-manager protocol.

## Shipping Working and Maintainable Code

### Testing

Python Standard library contains the unittest module, provides support for:

* Unit tests.
* Integration tests.
* Acceptance tests.

`TestCase`: basic unit of test organisation in unittest framework, groups together related test functions.

**Fixtures**: code run before and/or after each test function.

**Assertions**: specific tests for conditions and behaviours.

tests have to start with `test_` to be detected and executed automatically.

**PDB**: Python DeBugger.

```
>>> import pdb
>>> pdb.set_trace()
```

```
$ python3 -m pub palindrome.py
```

### Packaging

Packaging and distributing the Python code use standard distutils module.

```
$ python3 -m venv palindrome_env
$ source palindrome_env/bin/activate
(palindrome_env) $
(palindrome_env) $ python setup.py intall
(palindrome_env) $  python
…
>>> import palindrome
>>> palindrome.__file__
‘…/palindrome/palindrome.py'
```

Packaging to a Source distribution:

```
$ python setup.py sdist —format zip
```

Installing packages with pip

Common tools for installing third-party packages are distutils and pip. 

The central repository for Python packages is the Python Package Index, also called PyPI or “cheeseshop”.
 
### Organising larger programs

Package: special type of module which can contain other modules and/or packages. Packages are generally directories. Modules are generally files.

`sys.path`: list of directories Python searches for modules.

```
sys.path = [‘’, dir_1, dir_2, dir_3]
```

```
>>> import sys
>>> sys.path.append(’new_package’)
```

**PYTHONPATH:** environment variable listing paths added to sys.path. 

To create a package:

* Create a directory
* Create `__init__.py` file inside the directory. (Executed when the package is imported)

### Relative imports

Imports which use a relative path to modules in the same package.

```
from .b import B (same directory)
from ..a import A (parent directory)
```
    
**Namespace packages:** packages that split across several directories. Useful for splitting large packages into multiple parts. They don’t have the `__init__.py` files, avoiding that way complex initialisation ordering problems.

**Executable directories:** directories containing an entry point for Python execution. Place a `__main__.py` file.

### Recommended project structure

```
project_name
|— __main__.py
|— project_name
|	|— __init__.py
|	|— more_source.py
|	|— subpackage1
|	|	|— __init__.py
|	|— test
|		|— __init__.py
|		|— test_code.py
|— setup.py
```

## Misc

### Conditional expression

```
result = true_value if condition else false_value
```

### Lambda functions

Lambda is an expression which evaluates to a function.

```
 lambda name: name.split()[-1]
```
```
>>> is_odd = lambda x: x % 2 == 1
```

### Extended formal arguments

```
def extended(*args, **kwargs):
```

```
>>> list_of_lists = [list1, list2, list3]
>>> transposed = list(zip(*list_of_lists))
```

### Closures

Maintain references to objects from earlier scopes.

### Function factory

Function that returns new, specialised functions.

### Decorators

Modify or enhance functions without changing their definition. Implemented as callables that take and return other callable.

```
def escape_unicode(f):
    def wrap(*args, **kwargs):
        x = f(*args, **kwargs)
        return ascii(x)
    return wrap

@escape_unicode
def vegetable():
	return ascii(‘blomkål’)

@escape_unicode
def animal():
	return ascii(‘bjørn’)

@escape_unicode
def mineral():
	return ascii(’stål’) 
```

Classes can be decorators if the implement the `__call__()` method. Instances can be decorators. Multiple decorators are processed in inverse order.

```
@decorator1
@decorator2
@decorator3
def my_function():
```

Decorators can be used to decorate methods in classes as well as functions.

functools.wraps() properly update metadata on wrapped functions.

Typical use case for decorators: validate arguments.

### String representations

Python has 2 primary string representations for objects:

* `str(obj)`: calls `__str__(self)` method. Default are more concise representation. Fallback to repr().
* `repr(obj)`: calls `__repr__(self)` method. It should provide an unambiguous and precise representation of the object (including types).

Standard library includes reprlib library:

```
>>> import reprlib
>>> reprlib.repr(obj)
```

### Exceptions

Always specify an exception type with except, but don’t be too general.

```
BaseException(args, __context__, __cause__, __traceback__)
   |- KeyboardInterrupt
   |- Exception
   |- SystemExit
```

```
class MyException(Exception):
	pass
```

## Context Manager (PEP343)

Object designed to be used in a with-statement. A context-manager ensures that resources are properly and automatically managed.

```
with context-manager:
	ENTER: prepares the context to be used.
	body
	EXIT: cleans up the context.
```

```
>>> with open(‘file.txt’, ‘w’) as f:
		f.write(“blah”)
```

Context Manager protocol:

* `__enter__()`: executed before entering the with-statement body. If it throws an exception the body is never executed. The return value is bound to as variable. Commonly they return themselves.
*  `__exit__(self, exc_type=None, exc_val=None, exc_tb=None)`: executed when with-statement body exists. Exception information is passed to `__exit__()`:
	
	* `exc_type`: exception type.
	* `exc_val`: exception object
	* `exc_tb`: exception traceback
	
By default `__exit__()` propagates exceptions thrown from the with-statement’s code block. If `__exit__()` returns False, the exception is propagated. `__exit__()` should never explicitly re-raise exceptions. It should only raise exceptions if it fails itself.

`contextlib`: standard library module for working with context managers. It provides utilities for common tasks involving the with statement.

* context `lib.contextmanager`: decorator you can use to create new context managers.

```
@contextlib.contextmanager
def my_context_manager():
	# <ENTER>
	try:
		yield [value]
		# <NORMAL EXIT>
	except:
		# <EXCEPTIONAL EXIT>
		raise

with my_context_manager() as x:
```

Unlike standard context managers, those created with the context manager decorator have to use standard exception handling to propagate exceptions. 

Multiple context managers (= nested):

```
with cmd1() as a, cmd2() as b:
	BODY
```

## Introspection

Ability of a program of examine its own structure or state.

```
>>> i = 5
>>> type(i)
<class 'int'>
>>> dir(i)
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']
>>> getattr(i, 'denominator')
1
>>> hasattr(i, 'bit_length')
True
```
Introspecting scopes

```
>>> globals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'i': 7}
>>> a = 42
>>> globals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'i': 7, 'a': 42}
>>> locals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'i': 7, 'a': 42}
```

Handy for debugging purposes:

```
>>> name = "Joe Bloggs"
>>> age = 28
>>> country = "New Zealand"
>>> "{name} is {age} years old and is from {country}".format(**locals())
'Joe Bloggs is 28 years old and is from New Zealand'
```

The inspect module in the Python standard library provides with advance tools for introspection.

# Python for Data Science and Machine Learning 

## Main tools

### Anaconda

Distribution of Python with “all-in-one” install very popular in data science and machine learning:
* Python
* Libraries
* Virtual Environment

```
$ conda create -n myds anaconda
$ conda activate myds

$ conda env list
$ conda list --explicit > spec-file.txt
$ conda deactivate
```

### Jupyter

Development environment where we can write code, display images and write down markdown notes.

## Most popular libraries

### NumPy

Linear algebra Library for Python.

NumPy Arrays: 

```
import numpy as np
np.arange(0,10)
np.zeros(3)
np.zeros((2,3))
np.ones(4)
np.linspace(0,5,10)
np.eye(4)
np.random.rand(5)
np.random.randn(4,4)
np.random.randint(1,100)
````
Array methods/attributes:
```
arr.reshape(5,5)
arr.max()
arr.min()
arr.argmax()
arr.shape
arr.dtype

arr_copy = arr.copy()
```
**NOTE:** slice only creates a new reference to the existing list, it doesn’t copy the values.

```
arr_2d[1][2] == arr_2d[1,2] 
arr_2d[:2,1:]
```

arr[boolean_expression] Only returns instances with the same index of those boolean array elements that are true.
```
arr[arr>5]
```


