From 321e7196d06e7df601ca7eab334a12d6dc5668d9 Mon Sep 17 00:00:00 2001 From: Satwik Date: Sat, 2 Nov 2019 23:02:47 +0530 Subject: [PATCH] Some change of example orders --- README.md | 1060 ++++++++++++++++++++++++++--------------------------- 1 file changed, 529 insertions(+), 531 deletions(-) diff --git a/README.md b/README.md index e2111cc..04a0d1b 100644 --- a/README.md +++ b/README.md @@ -19,74 +19,79 @@ So, here we go... # Table of Contents - + + - [Structure of the Examples](#structure-of-the-examples) - + [ā–¶ Some fancy Title](#-some-fancy-title) + + [ā–¶ Some fancy Title](#%E2%96%B6-some-fancy-title) - [Usage](#usage) - [šŸ‘€ Examples](#%F0%9F%91%80-examples) * [Section: Strain your brain!](#section-strain-your-brain) - + [ā–¶ Strings can be tricky sometimes](#-strings-can-be-tricky-sometimes) - + [ā–¶ Splitsies](#-splitsies) - + [ā–¶ Hash brownies](#-hash-brownies) - + [ā–¶ Disorder within order](#-disorder-within-order) - + [ā–¶ Keep trying...](#-keep-trying) - + [ā–¶ Deep down, we're all the same.](#-deep-down-were-all-the-same) - + [ā–¶ For what?](#-for-what) - + [ā–¶ Evaluation time discrepancy](#-evaluation-time-discrepancy) - + [ā–¶ How not to use `is` operator](#-how-not-to-use-is-operator) - + [ā–¶ A tic-tac-toe where X wins in the first attempt!](#-a-tic-tac-toe-where-x-wins-in-the-first-attempt) - + [ā–¶ The sticky output function](#-the-sticky-output-function) - + [ā–¶ The chicken-egg problem](#-the-chicken-egg-problem) - + [ā–¶ `is not ...` is not `is (not ...)`](#-is-not--is-not-is-not-) - + [ā–¶ The surprising comma](#-the-surprising-comma) - + [ā–¶ Strings and the backslashes](#-strings-and-the-backslashes) - + [ā–¶ not knot!](#-not-knot) - + [ā–¶ Half triple-quoted strings](#-half-triple-quoted-strings) - + [ā–¶ Midnight time doesn't exist?](#-midnight-time-doesnt-exist) - + [ā–¶ What's wrong with booleans?](#-whats-wrong-with-booleans) - + [ā–¶ Class attributes and instance attributes](#-class-attributes-and-instance-attributes) - + [ā–¶ yielding None](#-yielding-none) - + [ā–¶ Mutating the immutable!](#-mutating-the-immutable) - + [ā–¶ The disappearing variable from outer scope](#-the-disappearing-variable-from-outer-scope) - + [ā–¶ When True is actually False](#-when-true-is-actually-false) - + [ā–¶ Yielding from... return!](#-yielding-from-return) - + [ā–¶ Lossy zip of iterators](#-lossy-zip-of-iterators) - + [ā–¶ Subclass relationships](#-subclass-relationships) - + [ā–¶ The mysterious key type conversion](#-the-mysterious-key-type-conversion) - + [ā–¶ Let's see if you can guess this?](#-lets-see-if-you-can-guess-this) + + [ā–¶ First things first!](#%E2%96%B6-first-things-first) + + [ā–¶ Strings can be tricky sometimes](#%E2%96%B6-strings-can-be-tricky-sometimes) + + [ā–¶ Splitsies](#%E2%96%B6-splitsies) + + [ā–¶ Hash brownies](#%E2%96%B6-hash-brownies) + + [ā–¶ Disorder within order](#%E2%96%B6-disorder-within-order) + + [ā–¶ Keep trying...](#%E2%96%B6-keep-trying) + + [ā–¶ Deep down, we're all the same.](#%E2%96%B6-deep-down-were-all-the-same) + + [ā–¶ For what?](#%E2%96%B6-for-what) + + [ā–¶ Evaluation time discrepancy](#%E2%96%B6-evaluation-time-discrepancy) + + [ā–¶ How not to use `is` operator](#%E2%96%B6-how-not-to-use-is-operator) + + [ā–¶ A tic-tac-toe where X wins in the first attempt!](#%E2%96%B6-a-tic-tac-toe-where-x-wins-in-the-first-attempt) + + [ā–¶ The sticky output function](#%E2%96%B6-the-sticky-output-function) + + [ā–¶ The chicken-egg problem](#%E2%96%B6-the-chicken-egg-problem) + + [ā–¶ `is not ...` is not `is (not ...)`](#%E2%96%B6-is-not--is-not-is-not-) + + [ā–¶ The surprising comma](#%E2%96%B6-the-surprising-comma) + + [ā–¶ Strings and the backslashes](#%E2%96%B6-strings-and-the-backslashes) + + [ā–¶ not knot!](#%E2%96%B6-not-knot) + + [ā–¶ Half triple-quoted strings](#%E2%96%B6-half-triple-quoted-strings) + + [ā–¶ Midnight time doesn't exist?](#%E2%96%B6-midnight-time-doesnt-exist) + + [ā–¶ What's wrong with booleans?](#%E2%96%B6-whats-wrong-with-booleans) + + [ā–¶ Class attributes and instance attributes](#%E2%96%B6-class-attributes-and-instance-attributes) + + [ā–¶ Non-reflexive class method](#%E2%96%B6-non-reflexive-class-method) + + [ā–¶ yielding None](#%E2%96%B6-yielding-none) + + [ā–¶ Nan-reflexivity](#%E2%96%B6-nan-reflexivity) + + [ā–¶ Mutating the immutable!](#%E2%96%B6-mutating-the-immutable) + + [ā–¶ The disappearing variable from outer scope](#%E2%96%B6-the-disappearing-variable-from-outer-scope) + + [ā–¶ Yielding from... return!](#%E2%96%B6-yielding-from-return) + + [ā–¶ Lossy zip of iterators](#%E2%96%B6-lossy-zip-of-iterators) + + [ā–¶ Subclass relationships](#%E2%96%B6-subclass-relationships) + + [ā–¶ The mysterious key type conversion](#%E2%96%B6-the-mysterious-key-type-conversion) + + [ā–¶ Let's see if you can guess this?](#%E2%96%B6-lets-see-if-you-can-guess-this) * [Section: Appearances are deceptive!](#section-appearances-are-deceptive) - + [ā–¶ Skipping lines?](#-skipping-lines) - + [ā–¶ Teleportation](#-teleportation) - + [ā–¶ Well, something is fishy...](#-well-something-is-fishy) - * [Section: Watch out for the landmines!](#section-watch-out-for-the-landmines) - + [ā–¶ Modifying a dictionary while iterating over it](#-modifying-a-dictionary-while-iterating-over-it) - + [ā–¶ Stubborn `del` operation](#-stubborn-del-operation) - + [ā–¶ Deleting a list item while iterating](#-deleting-a-list-item-while-iterating) - + [ā–¶ Loop variables leaking out!](#-loop-variables-leaking-out) - + [ā–¶ Beware of default mutable arguments!](#-beware-of-default-mutable-arguments) - + [ā–¶ Catching the Exceptions](#-catching-the-exceptions) - + [ā–¶ Same operands, different story!](#-same-operands-different-story) - + [ā–¶ The out of scope variable](#-the-out-of-scope-variable) - + [ā–¶ Be careful with chained operations](#-be-careful-with-chained-operations) - + [ā–¶ Name resolution ignoring class scope](#-name-resolution-ignoring-class-scope) - + [ā–¶ Needles in a Haystack](#-needles-in-a-haystack) - + [ā–¶ Wild imports](#-wild-imports) + + [ā–¶ Skipping lines?](#%E2%96%B6-skipping-lines) + + [ā–¶ Teleportation](#%E2%96%B6-teleportation) + + [ā–¶ Well, something is fishy...](#%E2%96%B6-well-something-is-fishy) + * [Section: Slippery Slopes](#section-slippery-slopes) + + [ā–¶ Modifying a dictionary while iterating over it](#%E2%96%B6-modifying-a-dictionary-while-iterating-over-it) + + [ā–¶ Stubborn `del` operation](#%E2%96%B6-stubborn-del-operation) + + [ā–¶ Deleting a list item while iterating](#%E2%96%B6-deleting-a-list-item-while-iterating) + + [ā–¶ Loop variables leaking out!](#%E2%96%B6-loop-variables-leaking-out) + + [ā–¶ Beware of default mutable arguments!](#%E2%96%B6-beware-of-default-mutable-arguments) + + [ā–¶ Catching the Exceptions](#%E2%96%B6-catching-the-exceptions) + + [ā–¶ Same operands, different story!](#%E2%96%B6-same-operands-different-story) + + [ā–¶ The out of scope variable](#%E2%96%B6-the-out-of-scope-variable) + + [ā–¶ Be careful with chained operations](#%E2%96%B6-be-careful-with-chained-operations) + + [ā–¶ Name resolution ignoring class scope](#%E2%96%B6-name-resolution-ignoring-class-scope) + + [ā–¶ Needles in a Haystack](#%E2%96%B6-needles-in-a-haystack) + + [ā–¶ Wild imports](#%E2%96%B6-wild-imports) + * [Section: Read the docs](#section-read-the-docs) + + [ā–¶ All sorted?](#%E2%96%B6-all-sorted) + + [ā–¶ All-true-ation](#%E2%96%B6-all-true-ation) * [Section: The Hidden treasures!](#section-the-hidden-treasures) - + [ā–¶ Okay Python, Can you make me fly?](#-okay-python-can-you-make-me-fly) - + [ā–¶ `goto`, but why?](#-goto-but-why) - + [ā–¶ Brace yourself!](#-brace-yourself) - + [ā–¶ Let's meet Friendly Language Uncle For Life](#-lets-meet-friendly-language-uncle-for-life) - + [ā–¶ Even Python understands that love is complicated](#-even-python-understands-that-love-is-complicated) - + [ā–¶ Yes, it exists!](#-yes-it-exists) - + [ā–¶ Ellipsis](#-ellipsis) - + [ā–¶ Inpinity](#-inpinity) - + [ā–¶ Let's mangle](#-lets-mangle) + + [ā–¶ Okay Python, Can you make me fly?](#%E2%96%B6-okay-python-can-you-make-me-fly) + + [ā–¶ `goto`, but why?](#%E2%96%B6-goto-but-why) + + [ā–¶ Brace yourself!](#%E2%96%B6-brace-yourself) + + [ā–¶ Let's meet Friendly Language Uncle For Life](#%E2%96%B6-lets-meet-friendly-language-uncle-for-life) + + [ā–¶ Even Python understands that love is complicated](#%E2%96%B6-even-python-understands-that-love-is-complicated) + + [ā–¶ Yes, it exists!](#%E2%96%B6-yes-it-exists) + + [ā–¶ Ellipsis](#%E2%96%B6-ellipsis) + + [ā–¶ Inpinity](#%E2%96%B6-inpinity) + + [ā–¶ Let's mangle](#%E2%96%B6-lets-mangle) * [Section: Miscellaneous](#section-miscellaneous) - + [ā–¶ `+=` is faster](#--is-faster) - + [ā–¶ Let's make a giant string!](#-lets-make-a-giant-string) - + [ā–¶ Explicit typecast of strings](#-explicit-typecast-of-strings) - + [ā–¶ Minor Ones](#-minor-ones) + + [ā–¶ `+=` is faster](#%E2%96%B6--is-faster) + + [ā–¶ Let's make a giant string!](#%E2%96%B6-lets-make-a-giant-string) + + [ā–¶ Minor Ones](#%E2%96%B6-minor-ones) * [~~~ That's all folks! ~~~](#-thats-all-folks-) - [Contributing](#contributing) - [Acknowledgements](#acknowledgements) @@ -351,44 +356,6 @@ Makes sense, right? --- -### ā–¶ Splitsies - -```py ->>> 'a'.split() -['a'] - -# is same as ->>> 'a'.split(' ') -['a'] - -# but ->>> len(''.split()) -0 - -# isn't the same as ->>> len(''.split(' ')) -1 -``` - -#### šŸ’” Explanation: - -- It might appear at first that the default seperator for split is a single space `' '`, but as per the [docs](https://docs.python.org/2.7/library/stdtypes.html#str.split) - > If sep is not specified or is `None`, a different splitting algorithm is applied: runs of consecutive whitespace are regarded as a single separator, and the result will contain no empty strings at the start or end if the string has leading or trailing whitespace. Consequently, splitting an empty string or a string consisting of just whitespace with a None separator returns `[]`. - > If sep is given, consecutive delimiters are not grouped together and are deemed to delimit empty strings (for example, `'1,,2'.split(',')` returns `['1', '', '2']`). Splitting an empty string with a specified separator returns `['']`. -- Noticing how the leading and trailing whitespaces are handled in the following snippet will make things clear, - ```py - >>> ' a '.split(' ') - ['', 'a', ''] - >>> ' a '.split() - ['a'] - >>> ''.split(' ') - [''] - ``` - ---- - - - ### ā–¶ Hash brownies 1\. @@ -435,6 +402,56 @@ So, why is Python all over the place? --- +### ā–¶ Deep down, we're all the same. + +```py +class WTF: + pass +``` + +**Output:** +```py +>>> WTF() == WTF() # two different instances can't be equal +False +>>> WTF() is WTF() # identities are also different +False +>>> hash(WTF()) == hash(WTF()) # hashes _should_ be different as well +True +>>> id(WTF()) == id(WTF()) +True +``` + +#### šŸ’” Explanation: + +* When `id` was called, Python created a `WTF` class object and passed it to the `id` function. The `id` function takes its `id` (its memory location), and throws away the object. The object is destroyed. +* When we do this twice in succession, Python allocates the same memory location to this second object as well. Since (in CPython) `id` uses the memory location as the object id, the id of the two objects is the same. +* So, object's id is unique only for the lifetime of the object. After the object is destroyed, or before it is created, something else can have the same id. +* But why did the `is` operator evaluated to `False`? Let's see with this snippet. + ```py + class WTF(object): + def __init__(self): print("I") + def __del__(self): print("D") + ``` + + **Output:** + ```py + >>> WTF() is WTF() + I + I + D + D + False + >>> id(WTF()) == id(WTF()) + I + D + I + D + True + ``` + As you may observe, the order in which the objects are destroyed is what made all the difference here. + +--- + ### ā–¶ Disorder within order ```py @@ -500,7 +517,7 @@ What is going on here? - The reason why intransitive equality didn't hold among `dictionary`, `ordered_dict` and `another_ordered_dict` is because of the way `__eq__` method is implemented in `OrderedDict` class. From the [docs](https://docs.python.org/3/library/collections.html#ordereddict-objects) > Equality tests between OrderedDict objects are order-sensitive and are implemented as `list(od1.items())==list(od2.items())`. Equality tests between `OrderedDict` objects and other Mapping objects are order-insensitive like regular dictionaries. -- The reason for this equality is behavior is that it allows `OrderedDict` objects to be directly substituted anywhere a regular dictionary is used. +- The reason for this equality is behavior is that it allows `OrderedDict` objects to be directly substituted anywhere a regular dictionary is used. - Okay, so why did changing the order affect the lenght of the generated `set` object? The answer is the lack of intransitive equality only. Since sets are "unordered" collections of unique elements, the order in which elements are inserted shouldn't matter. But in this case, it does matter. Let's break it down a bit, ```py >>> some_set = set() @@ -593,55 +610,6 @@ Iteration 0 --- -### ā–¶ Deep down, we're all the same. - -```py -class WTF: - pass -``` - -**Output:** -```py ->>> WTF() == WTF() # two different instances can't be equal -False ->>> WTF() is WTF() # identities are also different -False ->>> hash(WTF()) == hash(WTF()) # hashes _should_ be different as well -True ->>> id(WTF()) == id(WTF()) -True -``` - -#### šŸ’” Explanation: - -* When `id` was called, Python created a `WTF` class object and passed it to the `id` function. The `id` function takes its `id` (its memory location), and throws away the object. The object is destroyed. -* When we do this twice in succession, Python allocates the same memory location to this second object as well. Since (in CPython) `id` uses the memory location as the object id, the id of the two objects is the same. -* So, object's id is unique only for the lifetime of the object. After the object is destroyed, or before it is created, something else can have the same id. -* But why did the `is` operator evaluated to `False`? Let's see with this snippet. - ```py - class WTF(object): - def __init__(self): print("I") - def __del__(self): print("D") - ``` - - **Output:** - ```py - >>> WTF() is WTF() - I - I - D - D - False - >>> id(WTF()) == id(WTF()) - I - D - I - D - True - ``` - As you may observe, the order in which the objects are destroyed is what made all the difference here. - ---- ### ā–¶ For what? @@ -888,6 +856,22 @@ Similar optimization applies to other **immutable** objects like empty tuples as --- +### ā–¶ `is not ...` is not `is (not ...)` + +```py +>>> 'something' is not None +True +>>> 'something' is (not None) +False +``` + +#### šŸ’” Explanation + +- `is not` is a single binary operator, and has behavior different than using `is` and `not` separated. +- `is not` evaluates to `False` if the variables on either side of the operator point to the same object and `True` otherwise. + +--- + ### ā–¶ A tic-tac-toe where X wins in the first attempt! @@ -1043,19 +1027,65 @@ False --- -### ā–¶ `is not ...` is not `is (not ...)` - +### ā–¶ Subclass relationships + +**Output:** ```py ->>> 'something' is not None +>>> from collections import Hashable +>>> issubclass(list, object) True ->>> 'something' is (not None) +>>> issubclass(object, Hashable) +True +>>> issubclass(list, Hashable) False ``` -#### šŸ’” Explanation +The Subclass relationships were expected to be transitive, right? (i.e., if `A` is a subclass of `B`, and `B` is a subclass of `C`, the `A` _should_ a subclass of `C`) -- `is not` is a single binary operator, and has behavior different than using `is` and `not` separated. -- `is not` evaluates to `False` if the variables on either side of the operator point to the same object and `True` otherwise. +#### šŸ’” Explanation: + +* Subclass relationships are not necessarily transitive in Python. Anyone is allowed to define their own, arbitrary `__subclasscheck__` in a metaclass. +* When `issubclass(cls, Hashable)` is called, it simply looks for non-Falsey "`__hash__`" method in `cls` or anything it inherits from. +* Since `object` is hashable, but `list` is non-hashable, it breaks the transitivity relation. +* More detailed explanation can be found [here](https://www.naftaliharris.com/blog/python-subclass-intransitivity/). + +--- + +### ā–¶ All-true-ation + + + +```py +>>> all([True, True, True]) +True +>>> all([True, True, False]) +False + +>>> all([]) +True +>>> all([[]]) +False +>>> all([[[]]]) +True +``` + +Why's this True-False alteration? + +#### šŸ’” Explanation: + +- The implementation of `all` function is equivalent to + +- ```py + def all(iterable): + for element in iterable: + if not element: + return False + return True + ``` + +- `all([])` returns `True` since the iterable is empty. +- `all([[]])` returns `False` because `not []` is `True` is equivalent to `not False` as the list inside the iterable is empty. +- `all([[[]]])` and higher recursive variants are always `True` since `not [[]]`, `not [[[]]]`, and so on are equivalent to `not True`. --- @@ -1190,37 +1220,6 @@ SyntaxError: EOF while scanning triple-quoted string literal --- -### ā–¶ Midnight time doesn't exist? - -```py -from datetime import datetime - -midnight = datetime(2018, 1, 1, 0, 0) -midnight_time = midnight.time() - -noon = datetime(2018, 1, 1, 12, 0) -noon_time = noon.time() - -if midnight_time: - print("Time at midnight is", midnight_time) - -if noon_time: - print("Time at noon is", noon_time) -``` - -**Output (< 3.5):** - -```sh -('Time at noon is', datetime.time(12, 0)) -``` -The midnight time is not printed. - -#### šŸ’” Explanation: - -Before Python 3.5, the boolean value for `datetime.time` object was considered to be `False` if it represented midnight in UTC. It is error-prone when using the `if obj:` syntax to check if the `obj` is null or some equivalent of "empty." - ---- - ### ā–¶ What's wrong with booleans? 1\. @@ -1423,6 +1422,7 @@ True --- + ### ā–¶ yielding None ```py @@ -1455,6 +1455,72 @@ def some_func(val): --- + +### ā–¶ Yielding from... return! + +1\. + +```py +def some_func(x): + if x == 3: + return ["wtf"] + else: + yield from range(x) +``` + +**Output (> 3.3):** + +```py +>>> list(some_func(3)) +[] +``` + +Where did the `"wtf"` go? Is it due to some special effect of `yield from`? Let's validate that, + +2\. + +```py +def some_func(x): + if x == 3: + return ["wtf"] + else: + for i in range(x): + yield i +``` + +**Output:** + +```py +>>> list(some_func(3)) +[] +``` + +Same result, that didn't work either. + +#### šŸ’” Explanation: + ++ From Python 3.3 onwards, it became possible to use `return` statement with values inside generators (See [PEP380](https://www.python.org/dev/peps/pep-0380/)). The [official docs](https://www.python.org/dev/peps/pep-0380/#enhancements-to-stopiteration) say that, + +> "... `return expr` in a generator causes `StopIteration(expr)` to be raised upon exit from the generator." + ++ In case of `some_func(3)`, `StopIteration` is raised at the beginning because of `return` statement. The `StopIteration` exception is automatically catched inside the `list(...)` wrapper and the `for` loop. Therefore, the above two snippets result in an empty list. + ++ To get `["wtf"]` from the generator `some_func` we need to catch the `StopIteration` exception, + + ```py + try: + next(some_func(3)) + except StopIteration as e: + some_string = e.value + ``` + + ```py + >>> some_string + ["wtf"] + ``` + +--- + ### ā–¶ Nan-reflexivity @@ -1650,144 +1716,6 @@ NameError: name 'e' is not defined --- -### ā–¶ Yielding from... return! - -1\. - -```py -def some_func(x): - if x == 3: - return ["wtf"] - else: - yield from range(x) -``` - -**Output:** - -```py ->>> list(some_func(3)) -[] -``` - -Where did the `"wtf"` go? Is it due to some special effect of `yield from`? Let's validate that, - -2\. - -```py -def some_func(x): - if x == 3: - return ["wtf"] - else: - for i in range(x): - yield i -``` - -**Output (> 3.3):** - -```py ->>> list(some_func(3)) -[] -``` - -Same result, that didn't work either. - -#### šŸ’” Explanation: - -+ From Python 3.3 onwards, it became possible to use `return` statement with values inside generators (See [PEP380](https://www.python.org/dev/peps/pep-0380/)). The [official docs](https://www.python.org/dev/peps/pep-0380/#enhancements-to-stopiteration) say that, - -> "... `return expr` in a generator causes `StopIteration(expr)` to be raised upon exit from the generator." - -+ In case of `some_func(3)`, `StopIteration` is raised at the beginning because of `return` statement. The `StopIteration` exception is automatically catched inside the `list(...)` wrapper and the `for` loop. Therefore, the above two snippets result in an empty list. - -+ To get `["wtf"]` from the generator `some_func` we need to catch the `StopIteration` exception, - - ```py - try: - next(some_func(3)) - except StopIteration as e: - some_string = e.value - ``` - - ```py - >>> some_string - ["wtf"] - ``` - ---- - -### ā–¶ Lossy zip of iterators - - - -```py ->>> numbers = list(range(7)) ->>> numbers -[0, 1, 2, 3, 4, 5, 6] ->>> first_three, remaining = numbers[:3], numbers[3:] ->>> first_three, remaining -([0, 1, 2], [3, 4, 5, 6]) ->>> numbers_iter = iter(numbers) ->>> list(zip(numbers_iter, first_three)) -[(0, 0), (1, 1), (2, 2)] -# so far so good, let's zip the remaining ->>> list(zip(numbers_iter, remaining)) -[(4, 3), (5, 4), (6, 5)] -``` -Where did element `3` go from the `numbers` list? - -#### šŸ’” Explanation: - -- From Python [docs](https://docs.python.org/3.3/library/functions.html#zip), here's an approximate implementation of zip function, - ```py - def zip(*iterables): - sentinel = object() - iterators = [iter(it) for it in iterables] - while iterators: - result = [] - for it in iterators: - elem = next(it, sentinel) - if elem is sentinel: return - result.append(elem) - yield tuple(result) - ``` -- So the function takes in arbitrary number of itreable objects, adds each of their items to the `result` list by calling the `next` function on them, and stops whenever any of the iterable is exhausted. -- The caveat here is when any iterable is exhausted, the existing elements in the `result` list are discarded. That's what happened with `3` in the `numbers_iter`. -- The correct way to do the above using `zip` would be, - ```py - >>> numbers = list(range(7)) - >>> numbers_iter = iter(numbers) - >>> list(zip(first_three, numbers_iter)) - [(0, 0), (1, 1), (2, 2)] - >>> list(zip(remaining, numbers_iter)) - [(3, 3), (4, 4), (5, 5), (6, 6)] - ``` - The first argument of zip should be the one with fewest elements. - ---- - -### ā–¶ Subclass relationships - -**Output:** -```py ->>> from collections import Hashable ->>> issubclass(list, object) -True ->>> issubclass(object, Hashable) -True ->>> issubclass(list, Hashable) -False -``` - -The Subclass relationships were expected to be transitive, right? (i.e., if `A` is a subclass of `B`, and `B` is a subclass of `C`, the `A` _should_ a subclass of `C`) - -#### šŸ’” Explanation: - -* Subclass relationships are not necessarily transitive in Python. Anyone is allowed to define their own, arbitrary `__subclasscheck__` in a metaclass. -* When `issubclass(cls, Hashable)` is called, it simply looks for non-Falsey "`__hash__`" method in `cls` or anything it inherits from. -* Since `object` is hashable, but `list` is non-hashable, it breaks the transitivity relation. -* More detailed explanation can be found [here](https://www.naftaliharris.com/blog/python-subclass-intransitivity/). - ---- ### ā–¶ The mysterious key type conversion @@ -1901,124 +1829,10 @@ a, b = a[b] = {}, 5 ``` --- - ---- - -## Section: Appearances are deceptive! - -### ā–¶ Skipping lines? - -**Output:** -```py ->>> value = 11 ->>> valuŠµ = 32 ->>> value -11 -``` - -Wut? - -**Note:** The easiest way to reproduce this is to simply copy the statements from the above snippet and paste them into your file/shell. - -#### šŸ’” Explanation - -Some non-Western characters look identical to letters in the English alphabet but are considered distinct by the interpreter. - -```py ->>> ord('Šµ') # cyrillic 'e' (Ye) -1077 ->>> ord('e') # latin 'e', as used in English and typed using standard keyboard -101 ->>> 'Šµ' == 'e' -False - ->>> value = 42 # latin e ->>> valuŠµ = 23 # cyrillic 'e', Python 2.x interpreter would raise a `SyntaxError` here ->>> value -42 -``` - -The built-in `ord()` function returns a character's Unicode [code point](https://en.wikipedia.org/wiki/Code_point), and different code positions of Cyrillic 'e' and Latin 'e' justify the behavior of the above example. - ---- - -### ā–¶ Teleportation - - - -```py -import numpy as np - -def energy_send(x): - # Initializing a numpy array - np.array([float(x)]) - -def energy_receive(): - # Return an empty numpy array - return np.empty((), dtype=np.float).tolist() -``` - -**Output:** -```py ->>> energy_send(123.456) ->>> energy_receive() -123.456 -``` - -Where's the Nobel Prize? - -#### šŸ’” Explanation: - -* Notice that the numpy array created in the `energy_send` function is not returned, so that memory space is free to reallocate. -* `numpy.empty()` returns the next free memory slot without reinitializing it. This memory spot just happens to be the same one that was just freed (usually, but not always). - ---- - -### ā–¶ Well, something is fishy... - -```py -def square(x): - """ - A simple function to calculate the square of a number by addition. - """ - sum_so_far = 0 - for counter in range(x): - sum_so_far = sum_so_far + x - return sum_so_far -``` - -**Output (Python 2.x):** - -```py ->>> square(10) -10 -``` - -Shouldn't that be 100? - -**Note:** If you're not able to reproduce this, try running the file [mixed_tabs_and_spaces.py](/mixed_tabs_and_spaces.py) via the shell. - -#### šŸ’” Explanation - -* **Don't mix tabs and spaces!** The character just preceding return is a "tab", and the code is indented by multiple of "4 spaces" elsewhere in the example. -* This is how Python handles tabs: - - > First, tabs are replaced (from left to right) by one to eight spaces such that the total number of characters up to and including the replacement is a multiple of eight <...> -* So the "tab" at the last line of `square` function is replaced with eight spaces, and it gets into the loop. -* Python 3 is kind enough to throw an error for such cases automatically. - - **Output (Python 3.x):** - ```py - TabError: inconsistent use of tabs and spaces in indentation - ``` - ---- - --- ## Section: Slippery Slopes - ### ā–¶ Modifying a dictionary while iterating over it @@ -2101,6 +1915,45 @@ Okay, now it's deleted :confused: --- +### ā–¶ The out of scope variable + +```py +a = 1 +def some_func(): + return a + +def another_func(): + a += 1 + return a +``` + +**Output:** +```py +>>> some_func() +1 +>>> another_func() +UnboundLocalError: local variable 'a' referenced before assignment +``` + +#### šŸ’” Explanation: +* When you make an assignment to a variable in scope, it becomes local to that scope. So `a` becomes local to the scope of `another_func`, but it has not been initialized previously in the same scope which throws an error. +* Read [this](http://sebastianraschka.com/Articles/2014_python_scope_and_namespaces.html) short but an awesome guide to learn more about how namespaces and scope resolution works in Python. +* To modify the outer scope variable `a` in `another_func`, use `global` keyword. + ```py + def another_func() + global a + a += 1 + return a + ``` + + **Output:** + ```py + >>> another_func() + 2 + ``` + +--- + ### ā–¶ Deleting a list item while iterating ```py @@ -2161,6 +2014,57 @@ Can you guess why the output is `[2, 4]`? --- + +### ā–¶ Lossy zip of iterators + + + +```py +>>> numbers = list(range(7)) +>>> numbers +[0, 1, 2, 3, 4, 5, 6] +>>> first_three, remaining = numbers[:3], numbers[3:] +>>> first_three, remaining +([0, 1, 2], [3, 4, 5, 6]) +>>> numbers_iter = iter(numbers) +>>> list(zip(numbers_iter, first_three)) +[(0, 0), (1, 1), (2, 2)] +# so far so good, let's zip the remaining +>>> list(zip(numbers_iter, remaining)) +[(4, 3), (5, 4), (6, 5)] +``` +Where did element `3` go from the `numbers` list? + +#### šŸ’” Explanation: + +- From Python [docs](https://docs.python.org/3.3/library/functions.html#zip), here's an approximate implementation of zip function, + ```py + def zip(*iterables): + sentinel = object() + iterators = [iter(it) for it in iterables] + while iterators: + result = [] + for it in iterators: + elem = next(it, sentinel) + if elem is sentinel: return + result.append(elem) + yield tuple(result) + ``` +- So the function takes in arbitrary number of itreable objects, adds each of their items to the `result` list by calling the `next` function on them, and stops whenever any of the iterable is exhausted. +- The caveat here is when any iterable is exhausted, the existing elements in the `result` list are discarded. That's what happened with `3` in the `numbers_iter`. +- The correct way to do the above using `zip` would be, + ```py + >>> numbers = list(range(7)) + >>> numbers_iter = iter(numbers) + >>> list(zip(first_three, numbers_iter)) + [(0, 0), (1, 1), (2, 2)] + >>> list(zip(remaining, numbers_iter)) + [(3, 3), (4, 4), (5, 5), (6, 6)] + ``` + The first argument of zip should be the one with fewest elements. + +--- + ### ā–¶ Loop variables leaking out! @@ -2199,7 +2103,7 @@ print(x, ': x in global') 3\. **Output (Python 2.x):** -``` +```py >>> x = 1 >>> print([x for x in range(5)]) [0, 1, 2, 3, 4] @@ -2208,7 +2112,7 @@ print(x, ': x in global') ``` **Output (Python 3.x):** -``` +```py >>> x = 1 >>> print([x for x in range(5)]) [0, 1, 2, 3, 4] @@ -2401,44 +2305,6 @@ a += [5, 6, 7, 8] --- -### ā–¶ The out of scope variable - -```py -a = 1 -def some_func(): - return a - -def another_func(): - a += 1 - return a -``` - -**Output:** -```py ->>> some_func() -1 ->>> another_func() -UnboundLocalError: local variable 'a' referenced before assignment -``` - -#### šŸ’” Explanation: -* When you make an assignment to a variable in scope, it becomes local to that scope. So `a` becomes local to the scope of `another_func`, but it has not been initialized previously in the same scope which throws an error. -* Read [this](http://sebastianraschka.com/Articles/2014_python_scope_and_namespaces.html) short but an awesome guide to learn more about how namespaces and scope resolution works in Python. -* To modify the outer scope variable `a` in `another_func`, use `global` keyword. - ```py - def another_func() - global a - a += 1 - return a - ``` - - **Output:** - ```py - >>> another_func() - 2 - ``` - ---- ### ā–¶ Be careful with chained operations @@ -2622,7 +2488,7 @@ some_dict = { "key_3": 3 } -some_list = some_list.append(4) +some_list = some_list.append(4) some_dict = some_dict.update({"key_4": 4}) ``` @@ -2704,6 +2570,43 @@ def similar_recursive_func(a): --- + +### ā–¶ Splitsies + +```py +>>> 'a'.split() +['a'] + +# is same as +>>> 'a'.split(' ') +['a'] + +# but +>>> len(''.split()) +0 + +# isn't the same as +>>> len(''.split(' ')) +1 +``` + +#### šŸ’” Explanation: + +- It might appear at first that the default seperator for split is a single space `' '`, but as per the [docs](https://docs.python.org/2.7/library/stdtypes.html#str.split) + > If sep is not specified or is `None`, a different splitting algorithm is applied: runs of consecutive whitespace are regarded as a single separator, and the result will contain no empty strings at the start or end if the string has leading or trailing whitespace. Consequently, splitting an empty string or a string consisting of just whitespace with a None separator returns `[]`. + > If sep is given, consecutive delimiters are not grouped together and are deemed to delimit empty strings (for example, `'1,,2'.split(',')` returns `['1', '', '2']`). Splitting an empty string with a specified separator returns `['']`. +- Noticing how the leading and trailing whitespaces are handled in the following snippet will make things clear, + ```py + >>> ' a '.split(' ') + ['', 'a', ''] + >>> ' a '.split() + ['a'] + >>> ''.split(' ') + [''] + ``` + +--- + ### ā–¶ Wild imports @@ -2741,31 +2644,28 @@ NameError: name '_another_weird_name_func' is not defined works! ``` - If you really want to use wildcard imports, then you'd have to define the list `__all__` in your module that will contain a list of public objects that'll be available when we do wildcard imports. -```py -__all__ = ['_another_weird_name_func'] + ```py + __all__ = ['_another_weird_name_func'] -def some_weird_name_func_(): - print("works!") + def some_weird_name_func_(): + print("works!") -def _another_weird_name_func(): - print("works!") -``` -**Output** + def _another_weird_name_func(): + print("works!") + ``` + **Output** -```py ->>> _another_weird_name_func() -"works!" ->>> some_weird_name_func_() -Traceback (most recent call last): - File "", line 1, in -NameError: name 'some_weird_name_func_' is not defined -``` ---- + ```py + >>> _another_weird_name_func() + "works!" + >>> some_weird_name_func_() + Traceback (most recent call last): + File "", line 1, in + NameError: name 'some_weird_name_func_' is not defined + ``` --- -## Section: Read the docs - ### ā–¶ All sorted? @@ -2805,48 +2705,38 @@ False ([7, 8, 9], []) ``` - - --- -### ā–¶ All-true-ation - - - +### ā–¶ Midnight time doesn't exist? + ```py ->>> all([True, True, True]) -True ->>> all([True, True, False]) -False +from datetime import datetime ->>> all([]) -True ->>> all([[]]) -False ->>> all([[[]]]) -True +midnight = datetime(2018, 1, 1, 0, 0) +midnight_time = midnight.time() + +noon = datetime(2018, 1, 1, 12, 0) +noon_time = noon.time() + +if midnight_time: + print("Time at midnight is", midnight_time) + +if noon_time: + print("Time at noon is", noon_time) ``` -Why's this True-False alteration? +**Output (< 3.5):** + +```sh +('Time at noon is', datetime.time(12, 0)) +``` +The midnight time is not printed. #### šŸ’” Explanation: -- The implementation of `all` function is equivalent to - -- ```py - def all(iterable): - for element in iterable: - if not element: - return False - return True - ``` - -- `all([])` returns `True` since the iterable is empty. -- `all([[]])` returns `False` because `not []` is `True` is equivalent to `not False` as the list inside the iterable is empty. -- `all([[[]]])` and higher recursive variants are always `True` since `not [[]]`, `not [[[]]]`, and so on are equivalent to `not True`. +Before Python 3.5, the boolean value for `datetime.time` object was considered to be `False` if it represented midnight in UTC. It is error-prone when using the `if obj:` syntax to check if the `obj` is null or some equivalent of "empty." --- - --- @@ -3211,8 +3101,118 @@ AttributeError: 'A' object has no attribute '__variable' * The third snippet was also a consequence of name mangling. The name `__variable` in the statement `return __variable` was mangled to `_A__variable` which also happens to be the name of the variable we declared in outer scope. * Also, if the mangled name is longer than 255 characters truncation will happen. +--- --- +## Section: Appearances are deceptive! + +### ā–¶ Skipping lines? + +**Output:** +```py +>>> value = 11 +>>> valuŠµ = 32 +>>> value +11 +``` + +Wut? + +**Note:** The easiest way to reproduce this is to simply copy the statements from the above snippet and paste them into your file/shell. + +#### šŸ’” Explanation + +Some non-Western characters look identical to letters in the English alphabet but are considered distinct by the interpreter. + +```py +>>> ord('Šµ') # cyrillic 'e' (Ye) +1077 +>>> ord('e') # latin 'e', as used in English and typed using standard keyboard +101 +>>> 'Šµ' == 'e' +False + +>>> value = 42 # latin e +>>> valuŠµ = 23 # cyrillic 'e', Python 2.x interpreter would raise a `SyntaxError` here +>>> value +42 +``` + +The built-in `ord()` function returns a character's Unicode [code point](https://en.wikipedia.org/wiki/Code_point), and different code positions of Cyrillic 'e' and Latin 'e' justify the behavior of the above example. + +--- + +### ā–¶ Teleportation + + + +```py +import numpy as np + +def energy_send(x): + # Initializing a numpy array + np.array([float(x)]) + +def energy_receive(): + # Return an empty numpy array + return np.empty((), dtype=np.float).tolist() +``` + +**Output:** +```py +>>> energy_send(123.456) +>>> energy_receive() +123.456 +``` + +Where's the Nobel Prize? + +#### šŸ’” Explanation: + +* Notice that the numpy array created in the `energy_send` function is not returned, so that memory space is free to reallocate. +* `numpy.empty()` returns the next free memory slot without reinitializing it. This memory spot just happens to be the same one that was just freed (usually, but not always). + +--- + +### ā–¶ Well, something is fishy... + +```py +def square(x): + """ + A simple function to calculate the square of a number by addition. + """ + sum_so_far = 0 + for counter in range(x): + sum_so_far = sum_so_far + x + return sum_so_far +``` + +**Output (Python 2.x):** + +```py +>>> square(10) +10 +``` + +Shouldn't that be 100? + +**Note:** If you're not able to reproduce this, try running the file [mixed_tabs_and_spaces.py](/mixed_tabs_and_spaces.py) via the shell. + +#### šŸ’” Explanation + +* **Don't mix tabs and spaces!** The character just preceding return is a "tab", and the code is indented by multiple of "4 spaces" elsewhere in the example. +* This is how Python handles tabs: + + > First, tabs are replaced (from left to right) by one to eight spaces such that the total number of characters up to and including the replacement is a multiple of eight <...> +* So the "tab" at the last line of `square` function is replaced with eight spaces, and it gets into the loop. +* Python 3 is kind enough to throw an error for such cases automatically. + + **Output (Python 3.x):** + ```py + TabError: inconsistent use of tabs and spaces in indentation + ``` + +--- --- ## Section: Miscellaneous @@ -3327,8 +3327,6 @@ Let's increase the number of iterations by a factor of 10. > There should be one-- and preferably only one --obvious way to do it. - - --- ### ā–¶ Minor Ones