# Python

***

### Logical operators

What is acceptable in what cases

is vs == and so on.

***

Exception handling

frozenset

***

try except finally

***

Namespaces

Python has several types of namespaces:

**Built-in namespace**: Contains built-in functions and exceptions like `print()`, `len()`, `ValueError`, etc. This is created when Python starts and exists until the interpreter quits.

**Global namespace**: Created when a module is loaded. Each module has its own global namespace containing all the names defined at the module level.

**Local namespace**: Created when a function is called and contains the function's parameters and local variables. It's deleted when the function returns.

**Enclosing namespace**: Relevant for nested functions - it's the namespace of the enclosing function.

Python follows the **LEGB rule** when looking up names: Local → Enclosing → Global → Built-in. When you reference a variable, Python searches in that order until it finds the name.

***

**Function signature**

{% embed url="<https://developer.mozilla.org/en-US/docs/Glossary/Signature/Function>" %}

***

**LRU cache**

[`lru_cache` from the `functools` module](https://docs.python.org/3/library/functools.html#functools.lru_cache) is an example of a decorator and an example of memoization.

`lru_cache` memoizes the inputs and outputs of the decorated function in a size-restricted dictionary. It speeds up repeated calls to a slow function with the same inputs. For instance, if the function reads from disk, makes network requests, or requires a lot of computation AND it is used repeatedly with the same inputs.

Here's an example from the Python documentation that perfectly illustrates how and why to use the `lru_cache` decorator:

```py
from functools import lru_cache

@lru_cache()
def factorial_r(x):
    if x == 0:
        return 1
    else:
        return x * factorial_r(x - 1)

factorial_r(10) # no previously cached result, makes 11 recursive calls
# 3628800
factorial_r(5)  # just looks up cached value result
# 120
factorial_r(12) # makes two new recursive calls, the other 11 are cached
# 479001600
```

Since the `factorial` function is recursive and the inputs are sequential numbers, it's called repeatedly with the same inputs. Without the cache, the function would be called 30 times. With `lru_cache`, the function is only called 13 times. While you don't often need to compute factorials, this example ties together how to use a decorator *and* memoization *and* recursion.

***

### Shallow vs Deep copies of Python data structures

***

***

### f-strings

f-string tricks: <https://www.youtube.com/watch?v=9saytqA0J9A>

***

What is a \_\_pycache\_\_ directory?

***
