> For the complete documentation index, see [llms.txt](https://www.noobcoding.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://www.noobcoding.com/languages/fluent_python.md).

# Fluent Python

My notes when reading Luciano Ramalho's Fluent Python.

## The Python Data Model

By implementing special methods (A.K.A. magic methods, like `__len__`, `__getitem__`, `__repr__` etc.. Often pronounced *dunder xx*), your objects can utilize built-in functions and syntax like `len()`, `[]`, `for ... in ...` and thus be considered Pythonic.

Understanding the Pythonic `len(xx)` over `xx.len()`: Think of these functions as unary operators.

## An Array of Sequences

* List Comprehension vs Generator Expression

  * List Comprehension: `list_a = [i + j for i in ... for j in ...]`

  This is a cartesian products example. `for i in ...` part is the outer loop.

  * Generator Expression: `xx(i + j for i in ... for j in ...)`

  The syntactic difference is `()` vs `[]`. But under the hood it saves space by yielding item one by one so a full list is never constructed. Also it can be used to build many other containers.
* Tuples
  * Tuples (Iterable) Unpacking
    * Use case: parallel assignment (can be nested), swap, `%` string formatting `print('%s %s' % tup)`, passing function parameter `f(*tup)`.
    * Works for any iterable as long as the iterable yields exactly one item per variable in the receiving end. The only exception is using `*` dicussed below.
    * `a, *b, c = range(5)` and `b` is `[1, 2, 3]`. Only one `*` prefix variable is allowed.
  * Named Tuples `collections.namedtuple`
    * Construction: Passing construct parameters by name or position. `NamedTup._make(iterable)`. `NamedTup(*iterable)`.
    * Accessing field by name or position.
    * `._asdict()` return a `collections.OrderedDict`
  * Methods and attributes as an "immutable list": No appending/poping/inserting nor any inplace ops.
* Slicing
  * `[:3]` exclude the last item.
  * Slice object.

    ```
    s = slice(begin, end, stride)
    line[s]
    ```
  * Under the hood:

    ```
    # v[a]
    v.__getitem__(a)
    # v[a, b]. Multidimensional. Used in Numpy.
    v.__getitem__((a, b))
    ```
  * Ellipsis object: function parameters `f(a, ..., z)` and slice `a[i:...]`. If `a` is four dimentional, this is a shortcut for `a[i, :, :, :]`. It is mostly used in Numpy.
  * Assignment using slices. Some interesting example from the book:

    ```
    >>> l = list(range(10))
    >>> l
    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    >>> l[2:5] = [20, 30]
    >>> l
    [0, 1, 20, 30, 5, 6, 7, 8, 9]
    >>> del l[5:7]
    >>> l
    [0, 1, 20, 30, 5, 8, 9]
    >>> l[3::2] = [11, 22]
    >>> l
    [0, 1, 20, 11, 5, 22, 9]
    >>> l[2:5] = 100  
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: can only assign an iterable
    >>> l[2:5] = [100]
    >>> l
    [0, 1, 100, 22, 9]
    ```
* `+` and `*` and augmented assignment on sequences
  * `my_list = [[]] * 3` will result in a list with three references to the same inner list. List comprehension avoids this problem. `[[] for i in range(3)]`.
  * `+=` and `*=` will first try to use `__iadd__` and fall back to `__add__` and create a new object.
* `list.sort` and `sorted`
  * `list.sort` sorts inplace and returns `None`.
  * `sorted` library function accepts any iterable object.
  * Both accept two parameters: `reverse` bool and `key` for the name of a function that produces sorting keys.

## Misc

* [Online Python Tutor](http://www.pythontutor.com/)
