Python questions / observations:




Python - supported paradigms.
Python - Wspierane paradygmaty.

  • Python jest językiem wysokiego poziomu (jego syntax jest zrozumiały dla ludzi).
  • Ponadto, Python jest językiem dynamicznie typowanym o silnym systemie typów.
Python - good practices.
  • Czytelność vs. złożoność?
    • W Pythonie zasada „Simple is better than complex” (Zen of Python) działa w 90% przypadków. Pytanie pomocnicze => Czy mogę to napisać prościej, bez utraty czytelności?
  • Generator czy comprehension?
    • Reguła: Jeśli nie potrzebujesz leniwego generowania → użyj comprehension.
  • [Py Zen2] Updated Python Zen list.


Generators.
  1. The truth about generators, when they are really useful and when not? How generators cooperate with Python Frame based Stack? Do Class-based Iterators use Python Frame based Stack?
  2.  - Generatory są świetne, gdy nie znamy górnej granicy zbioru lub przetwarzamy tylko część wyników,
       np. sekwencję Fibonacciego, liczby pierwsze czy duże pliki CSV linia po linii. Generatory są niepraktyczne, gdy
       potrzebujemy przechowywać i przetwarzać cały zbiór naraz – np. jeśli chcemy posortować duży plik danych,
       lepiej wczytać go do listy, bo generatory nie pozwalają na swobodny dostęp do elementów.
     - W przeciwieństwie do zwykłych funkcji, generatory nie usuwają swojej ramki po zakończeniu wywołania yield.
       Zamiast tego, stan wykonania (w tym zmienne lokalne i wskaźnik kodu) jest przechowywany w obiekcie generatora.
       Mechanizm yield powoduje, że zamiast usunięcia ramki stosu (w normalnej funkcji), Python zapisuje jej stan i zawiesza wykonanie.
       Kiedy generator jest wznawiany, ramka zostaje przywrócona na stos i kontynuuje działanie od miejsca yield.
    
     - Trzy przykłady przepływów w Generatorach:
        1)
            * x = yield (<---)
            * Ta forma oznacza "czekaj na wartość z send(value)".
              coroutine zostaje uruchomiona i napotka 'x = yield', wtedy "zawiesza się" i oczekuje na dane z GEN_OBJ.send(value).
        2)
            * yield x (--->)
            * Ta forma oznacza: Zwróć wartość x na zewnątrz — to właśnie wynik next() lub send(...)
        3)
            * y = yield x (<--->)
            * Mix 1 i 2.
    
    
    Dwa główne sposoby patrzenia na generatory?
        1. Jako leniwe iteratory – najczęstszy use case: wydajne generowanie wartości w pętli (for, next()).
           np. range(), open(...).readline() w tle używają mechanizmów iteracyjnych.
        2. Jako prymitywy współprogramowania (coroutines)
        – dzięki send() i yield można pisać funkcje, które wymieniają dane w obie strony
          i przypominają prymitywną formę „asynchroniczności”.
          Właśnie na tym mechanizmie oparto async/await (to tylko rozszerzona składnia do korutyn).
    
    Ale można też dołożyć perspektywę "ramek" i tego, że generatory to nie są tylko wyjątkowe funkcje:
        Na pocztku normalny obiekt funkcji kiedy się go wywoła zwraca obiekt generatora (nie generorwaną wartość od razu).
        Ten obiekt generatora to właściwie "zatrzymana" ramka stosu, która może być wznowiona.
    
  3. yield from ITERABLE_OBJECT
  4.   - The one benefit of yield from is to allow easy refactoring of generators.
      - The second "performance optimization" is even more important. Simulation showed "yield from" speeds up execution about 10-30%.
    
      - O tej funkcjonalności można powiedzieć sporo więcej:
    
         -- Przykład: Rekurencyjna iteracja po strukturze drzewiastej
         -- Przykład: Dwukierunkowa komunikacja z yield from
    
         -- Python poszedł tu nieco pod prąd i dodał coś, czego brakuje w innych językach.
            Można to uznać za niekonwencjonalne, ale to dobrze wpisuje się w filozofię Pythona: mniej kodu, większa czytelność.
         -- Python często dodaje konstrukcje, które upraszczają kod, nawet jeśli wydają się „magiczne”.
            Czasem wydaje się, że Python chce być bardziej jak DSL (Domain-Specific Language)
            dla każdej możliwej sytuacji, a nie czysto „przewidywalnym” językiem ogólnego przeznaczenia.
    
         y = yield from sub_generator()
            - automatycznie iteruje przez cały sub_generator(), wszystkie yield z tamtego trafiają "na zewnątrz",
            - a jeśli sub_generator() zakończy się return coś, to yield from zwraca tę wartość do y.
    
     
  5. Why and when such error happened with generators? TypeError: can't send non-None value to a just-started generator
    • It occurs when you attempt to send a non-None value into a generator that has not yet been started (i.e., before its first yield statement).
    • By design, the first .send(value) call must be None or the generator must be primed using next() first.
    • gen = accumulator()
      gen.send(10)  # ❌ TypeError: can't send non-None value to a just-started generator




Python miscellaneous.
  1. What are main differences in Functions/Classes nature?
  2.  - Classes can be seen more like containers/structures/blueprint when functions are set of instructions (executable code blocks).
     - A class definition is a 'Declaration of a Structure', not a sequence of instructions (like functions).
     - While you can inspect the bytecode of class methods, the class object doesnt have bytecode associated to be disassembled.
        * That code can be disassembled, but it's part of the higher-level object and it is not attached to the final class object
     - Python classes are highly dynamic (add, remove, modify attributes at runtime). This flexibility comes at performance cost.
     - Classes due protocols/hooks are very modifiable during creation time. It also costs.
    Then:
     - In Classes Nature is to be very modifiable containers/blueprints. When Functions Nature is to be lightweight set of instructions.
  3. It was possible to compare some very specific solutions in terms of speed (by not realistic micro-benchmarking, but at least something). First solution based on functions and second based on classes. In the result class-based solution was 100 times slower. Why classes could be so slow? What are the biggest factors here? Additionally, in tests were seen that __slot__ classes feature did not improve speed, but tests were limited so this result could be not reliable.
  4.  - Creating a function is a simpler operation than creating a class. Python's interpreter is optimized for function creation.
     - Classes, even empty ones, have more overhead due to their inherent complexity.
     - Class creation flow is much more complex: Metaclass handling, namespace creation or executing class body code. It all takes time.

  5. Why immutable types were created in Python? There can be mentioned few reasons, but one can be that "it was needed for more complex data structures". The word about Hash maps, dictionaries, scopes/namespaces.
  6. What are cases in Python where internal state (implementation details) are hidden from users?
  7.  - lru_cache / cached_property
     - weakref module
     - asyncio / threading
    

  8. MappingProxy / FrozenSet / NamedTuple / Arrays / Deque / SimpleNamespace - why/when these structure can be useful?

  9. Unicode and UTF-8 are default in Python 3, but what does it really mean?
  10. Unicode is universal symbol representation system
     - it contains all world alphabets and also other symbols like emojis.
    When UTF-8 is Unicode symbols coding system to bytes form.
    UTF-8 is a variable-width encoding that supports all 1,112,064 valid Unicode code points using one to four bytes.
     - UTF = Unicode Transformation Format
    
    Unicode / UTF-8 example:
        Unicode mapping:
          chr(300) => Return a Unicode string of one character with ordinal i; 0 <= i <= 0x10ffff.
          'Ĭ'
          ord('Ĭ') => Return the Unicode code point for a one-character string.
          300
        UTF-8 encoding:
          bytes('Ĭ', "UTF-8") # Zwraca bajty reprezentujące znak Ĭ w kodowaniu UTF-8.
          b'\xc4\xac'         # 0xC4 0xAC => 11000100 10101100 => To są 2 bajty reprezentujące znak Ĭ w UTF-8.
    
  11. Python float implements IEEE 754, so float64 precision (53 bit mantis). 2**53 is the biggest number represented accurately.

  12. In Python str class there are maketrans (static method) and translate methods. It can be useful for converting text and removing specific symbols.
  13. >>> sentence = "Ann has a great day! What about you?"
    >>> sentence_output = sentence.translate(str.maketrans("!", "."))
    >>> sentence_output
    'Ann has a great day. What about you?
    

  14. Ellipsis it is unique constant accessed globally in Python (instance of EllipsisType). When we can use ellipsis object?
  15.  - In slicing (advanced indexing)
     - As a placeholder in code (“to be implemented later”)
     - In type hints (PEP 484 / typing)
     - For custom or creative uses
  16. Why Python/CPython is so slow? Many reasons? One is that PVM interprets bytecode line by line - there are no native CPU instructions (like for example in JVM).
  17. Running object creation in infinite loop, but without assigning to reference. Memory consumption did not increase. Garbage collector doesn't show any symptoms of working behaviour. This is edge case when Python potentially ignored such object memory allocations. But spotted possible GC related Memory leak when running object creation in infinite loop, with constantly re-assigning reference (gc_objects = gc.get_objects()). Memory consumption increases. GC tracked objects increases.

Python Execution model view (with importing modules system). More details attached.
  1. Python 'import module_A' and Python Execution model.
    - In terms of module importing what happens during Parsing Stage, Compilation Time and Runtime?
    - What if there is second the same import?
  2.  1. Python binary startup stage
            - Uruchomienie procesu/programu CPython w OS (np. Linuxie)
            - sprawdzenie/pobranie zmiennych środowiskowych i przełączników (PYTHONPATH, "-i", itd)
            - import modułów wewnętrznych (wkompilowanych w binarkę CPython-a, ale też Frozen modułów)
    
     2. Parsing phase (kod źródłowy Pythona -> obiekt/drzewo AST)
            - Co tutaj się dzieje?
              Najpierw wykonywane jest parsowanie "python code" na części zwykle nazywane tokenami. (Tokenizer/tokenizer.c)
              ...
              Ostatecznie, finalna AST struktura jest generowana
    
            - Komponenty fazy parsowania: Tokenizer / Lexer, Parser
    
            - Najpierw Python czyta kod źródłowy uruchamianego / głównego pliku np 'script.py'
              (na tym etapie nawet jeśli istnieje aktualny bytecode / plik .pyc to nie jest brany pod uwagę).
            - Na tym etapie nie wykonuje 'import module_A' tzn NIE wczytuje module_A.py i nawet nie sprawdza, czy ten plik istnieje.
              Tj. domyślne zachowanie w CPython, ale przy innych implementach lub bardziej skomplikowanych importach może być inaczej.
            - Linia import module_A po prostu zostaje zapisana w AST (potem bytecode) jako instrukcja do późniejszego wykonania.
    
            - Podsumowując, na tym etapie Python NIE parsuje kodu module_A.py podczas parsowania script.py.
              Parsowanie każdego modułu dzieje się dopiero, gdy interpreter faktycznie wykona instrukcję import w czasie Runtime.
    
     2. Compilation phase
            - Import nie jest wykonywany podczas kompilacji script.py — tylko zostaje przygotowany do wykonania.
              Python traktuje import module_A jak zwykłą instrukcję.
              W AST jest reprezentowana jako np. 'ast.Import'.
              W bytecode pojawi się instrukcja typu IMPORT_NAME.
    
     3. Execution context preparation
              module Frame, frame links, globals, locals, Python stack
              namespace model (LEGB) — to logiczna część kontekstu
    
     4. Runtime
           - Gdy interpreter napotyka instrukcję 'import module_A', to wtedy jest wykonywana pełna obsługa importu modułu module_A.
             Jej przebieg:
             Runtime(import flow 1) - Python sprawdza sys.modules czy dany moduł jest już załadowany
             (Nie jest.)
             Runtime(import flow 2) - Python szuka module_A.py w sys.path
             (Jeśli plik 'module_A.py' istnieje a .pyc jest nieaktualny lub brak.)
    
             Runtime(import flow 3) - Python wczytuje źródło 'module_A.py', sprawdza składnię kodu i parsuje go do AST,
             Runtime(import flow 4) - Następnie Python kompiluje kod do bytecode`u.
    
             Runtime(import flow 5) - Teraz Python:
                                         Tworzy obiekt modułu (types.ModuleType)
                                         Rejestruje go w sys.modules
    
             Runtime(import flow 6) - W końcu Python wykonuje bytecode w namespace modułu.
                                      To tak naprawdę uruchomienie wszystkiego, co nie jest ukryte w funkcji lub klasie.
    
                                      Krok po kroku:
    
                                      6a)
                                      Wykonywany jest cały tzw. kod na poziomie globalnym modułu
                                        definicje klas i funkcji
                                        przypisania zmiennych
                                        instrukcje warunkowe, pętle
                                        dowolny kod imperatywny
                                        blok if __name__ == "__main__"  jest ignorowany, nigdy warunek nie będzie spełniony
    
                                      6b)
                                      To działanie (6a) ma konkretne efekty strukturalne w Pythonie:
                                        Tworzy się przestrzeń nazw (namespace) modułu
                                        Wszystko, co zostanie zdefiniowane (funkcje, klasy, zmienne), trafia do mod.__dict__.
                                        To właśnie ta przestrzeń jest potem dostępna jako module_A.some_function.
    
                                        Tworzone są obiekty klas i funkcji
                                        Podczas wykonywania def i class tworzony jest odpowiedni obiekt typu function lub type.
                                        Każdy def to instancja function, a class to wywołanie metaklasy.
    
                                        Wszystkie efekty uboczne się wykonują
                                        Jeśli w module jest np. print("Uruchamiam się!"), to ten print się wykona.
                                        Jeśli jest połączenie z bazą danych — ono też może się wykonać (jeśli umieszczone globalnie).
    
                                        Importy zależne też są wykonywane
                                        Jeśli module_A importuje inne moduły (import module_B), to zaczyna się kaskada importów.
    
    
    Other things: importlib.reload()
        - Python re-executes the module (skipping parsing/compiling if bytecode is cached and valid).
        - All top-level code runs again, which can reset state or redefine classes/functions.
    
    import importlib
    importlib.reload(module_A)

Python Call Stack / Python Frames.
  1. Cases when Python creates Frames objects?
  2.  - functions calls,
     - importing modules,
     - execution main script (__main__),
     - execution classes call body.
     - also, eval/exec calls can cause additional Frames are created.
    





CLASSES
  1. Weak references, unpopular Python feature (class __weakref__ attribute that does almost nothing. Weak references logic is managed by Python interpreter). Advanced projects with critically important memory management aspect (for example: help for Garbage Collector).

  2. Python class level '__slots__' mechanism - it predefines possible instance attributes (there is no __dict__ on instance level). Many instance as performance optimization Use Case. Simulation showed 150-300 MB memory saving when 1 million objects are handled.

  3. dir() - there is easy explanation why dir command does not return always all attributes. Simply, it calls __dir__ underneath and __dir__ can be implemented differently. And often it is. Python documentation says "The default dir() mechanism behaves differently with different types of objects, as it attempts to produce the most relevant, rather than complete ..."

  4. When obj = object() then obj is instance of class (named object), but object is class and classes are instances of type metaclass (unless you use your own new metaclass). In Python everything inherits from object class apart from type metaclass that doesn't inherit from anything.

  5. Python 3.7 introduced a concept known as "forward references," which allows us to use the class name as a string when defining type hints. Starting from Python 3.7, you can also use the from __future__ import annotations import, which automatically treats all annotations as forward references.

  6. '__subclasshook__' - interesting feature that allows to dynamically deduct if subclass belong to base class (even without real inheritance).

  7. What does mean that super technically can be called "proxy object"? What are details of functioning super?
    • super only redirects lookup-calls to the correct parent class.
    • For this super uses Descriptor protocol - it has own implementation of __get__.
    • super handles creating temporary binding objects for instance and class methods (this binding is only relevant for the duration of the method call).

  8. Bound methods. Base Concept for different type of functions in Class definitions. For example, @staticmethod decorator role is to revert/bypass this machinery.

  9. What are details about class creation flow/protocol in Python? How can it be tuned?

  10. What it is zero-cost metaclassing mechanism?
  11.  - PEP 697, Python 3.12 introduces a zero-cost mechanism that optimizes the way metaclasses are determined.
    
  12. Implementing metaclass __call__ method causes that this method is called during class instance creation (flow is changed).

  13. The word about 'object' class - concretely instances - and when object() structure could be useful. object() doesnt have __dict__ and is immutable. (Sentinel, Locks)

PROTOCOLS (Descriptors, Attribute lookup)

  1. Python as protocol oriented language. There is set of behaviours implemented as Protocols in Python, but to be Protocol oriented language? It is probably not enough.

  2. [Protokół Deskryptorów]: Python Deskryptor obiekt - to jest Pythonowy obiekt, który implementuje Protokół Deskryptorów i jest przy okazji atrybutem klasowym innej klasy (kontekst).
    • Obiekt ten (Python Deskryptor obiekt) może posiadać różnoraką implementację jego dynamicznych zdolności zdolności, ale wymaga implementacji przynajmniej '__get__'.
    • Gdy Deskryptor implementuje tylko "__get__" to mówimy o Non-Data Descriptorze.
    • Gdy Deskryptor implementuje "__get__" i "__set__/__delete__" to mówimy o Data Descriptorze.
    • Python jest głęboko związany z Desktryptorami (fe: functions, methods, properties, class methods, static methods, super).
      • The mechanizm deskryptorów jest schowany w __getattribute__ methodach (object, type, and super). Dodatkowo __getattr__ jest użyty, gdy atrybut/deskryptor rzuca AtributeError wyjątkiem.
      • __set_name__ hook jest wołany podczas "tworzenia obiektu klasy" i rozszerza możliwości deskryptorów.


  3. [Attribute lookup protocol]: Attribute lookup for Class, Instance, Super:
  4. 
    In high level it is such mechanism:
        try:
            [type, object, super].__getattribute__(...)
        except AttributeError:
            [type, object, super].__getattr__(...)
    
    Potential modifiers:
     - Descriptors, Metaclasses, __slots__
     - re-writing: __getattribute__, __getattr__, AttributeError
    
    Use cases (different __getattribute__):
    1. Instance objects
      - lookup for instance attribute
      - lookup for class attribute
      - object.__getattribute__
    2. Class objects:
      - lookup for class attribute
      - type.__getattribute__
    
    3. Super object executes lookup:
      - by default super().__getattribute__ is taken
            
Other things:
  - it can be said that module objects use Attribute Lookup protocol too
  - Summary: Why Attribute Lookup is needed for Python?
        

Functions Deep Dive

  1. Functions are objects, but not so-typical. What makes functions not typical objects?
  2. 
    1. Functions local scope is built different:
     - Function local scope exists in runtime and is built based on temporarily created Stack Frame,
     - this Frame leverages the data from funct.__code__ for creating local scope
     - If we have look into temporarily created Frame (named F) then:
        - we can check that: F.f_locals is locals() => gives True
        - so Python Frame Stack is used to build Function local scope
    2. Functions have a __code__ object storing function bytecode.
    3. Functions have a __dict__ used bit different, only for storing attributes (what is kind of specific functions behaviour).
    4. Functions support closure pattern (__closure__ remembers variables from outer scope). Fundamental mechanism for predictability.
    5. Every function written in Python is Non-Data Descriptor (when C based not).

  3. Tricky edge case for imports in Class when code is C-based. How creating Bound Method works here?
  4. class A:
        from json import dumps as g # written in Python and defines __get__ (however there can be 2 implementations: Python/C)
        from itertools import repeat as h   # it is callable class written in C, doesn't define __get__
    
    Notes:
     * During Runtime when 'class A' is created, class body-content is being executed line by line (like function).
     * A.g("xx")   - ok
     * A.h("xx")   - ok
     * A().g("xx") - TypeError: dumps() takes 1 positional argument but 2 were given
     * A().h("xx") - ok
     * @classmethod impact
    

Testing things: Testing things:
  1. Mock and Stub what is the difference? Other test doubles: Dummy, Fake, Spy
  2. Mock:
      - It should be used for verifying interactions, ensuring the correct methods are called with the expected parameters.
      - Mocks can be configured to throw exceptions, return different values for different calls, and so on (can be complex).
    Stub:
      - this test double focuses on providing data (predefined responses) to control the test environment
      - often used to simulate the behavior of external services or components (isolating dependencies)
    
        => Both stubs and mocks help isolate the code under test by replacing external dependencies with controlled behavior.
    
    Dummy
     - the simplest form of a test double. It is an object that is passed around but is never actually used (serves as a placeholder)
     - Dummies are used when an argument is required by the code being tested, but it’s not actually used during the test.
    Fake
     - test double that has working implementations but is simplified or not as performant as the real system
     - Fakes are used when a real system might be too complex or slow, but you still want some kind of working implementation
    Spy
     - more advanced type of test double that not only helps verify interactions (like a mock) but also records the calls made to it
    Fixture
     - In the context of testing, fixtures can be considered as part of the broader test double or test setup family.
     - Any setup or configuration required to run your tests. Ensuring that the necessary context is available for the tests to execute.
     - Typically includes the creation of test data, that can be even related to setting up a connection to a database or web server.
     - Fixtures are about test setup and teardown — while test doubles isolate/control behavior of components within that environment.
    
    Test Doubles (Mocks, Stubs, Fakes, etc.):
     - Used to replace or simulate specific parts of a system, like dependencies or external services, that your code interacts with.
     - They help isolate/control the code under test and allow you to control behaviors and verify interactions.
    
    
  3. Mocking frameworks: unittest.mock, when to use MagicMock and when Mock?.
  4. Use Mock when:
     - You only need to mock regular methods or attributes that are not related to magic methods (i.e., methods you define yourself).
     - You don't need support for special behavior like indexing, iterating, or arithmetic operations.
    
    Use MagicMock when:
     - You need to mock magic methods (__getitem__, __setitem__, __call__, __iter__, etc.) or objects involved in such operations.
     - You deal with objects that emulate builtin containers like lists, or objects that need to support special methods in your tests.

  5. Mocking vs patching? What is difference?


Concurrency things:
  1. When 'race condition' situation happens?
  2.  - multiple threads or processes access shared data and at least one modifies it, but the access is not properly synchronized.
    
  3. What means that operations are 'thread-safe'?
  4.  - ensuring that only one thread can modify 'specific shared resource' at a time.
    

TODO:
 - fastapi, pydantic
 - Django simple personal website
 - Bigger project ideas

Python Data Model / Python Data Types.
Python Data Model:
 - wewnętrzna architektura i zestaw zasad określających, jak obiekty w Pythonie zachowują się i współdziałają ze sobą.

Python, rodzaje typów danych:
 - typy proste
 - typy sekwencyjne
 - typy zbiorów
 - typy słownikowo-mapujące
 - typy binarne
 - typy funkcjonalne i użytkownika
Iterators, Iterables, Sequences.
  1. Iterator and Iterable objects implementing certain protocols. What are details of these protocols?
  2. Iterator protocol:
     - Iterator object needs to implement __iter__ and __next__.
       Main feature of Iterators is allowing for sequence access of data stream - no need to maintain all data in memory.
    
    Iterable protocol:
     - Iterable object should implement __iter__, but not it is not requirement if there is no __iter__ then __getitem__ is required.
       In the other words iter(Iterable_object) should always produce valid Iterator object.
    
    Also, there are 2 different contracts about Iterator/Iterable protocols:
    1. Iterable
       Always, when Client calls 'iter()' I will give "new iterator" object.
    2. Iterator
       I am Iterator already, I maintain state - dont copy me.
    
    
    [!!!] __getitem__ edge case:
     - Still it is possible to create a class that have only __getitem__ and not __iter__ and such object will be Iterable.
     - It is rather hack than common practice, but handling __getitem__ in Iterable protocol still works.
    

  3. What are builtin Iterable data structures that are also Iterators (have __next__ and __iter__)?
  4. - map, filter, reduce, zip - map example:
    >>> L = [0, 2, 4, 6, 8]
    >>>
    >>> mapped_L = map(lambda x: x + 1, L)
    >>>
    >>> mapped_L
    map object at 0x70d609f84a60
    >>>
    >>> hasattr(mapped_L, "__iter__")
    True
    >>> hasattr(mapped_L, "__next__")
    True
    >>> hasattr(mapped_L, "__getitem__")
    False
        => map objects are Iterables and Iterators (but not Sequences)
    
  5. What are details of sequence protocol in Python?
  6. Sequence objects in the most basic version need to implement two dunder methods: __getitem__, __len__:
     - __getitem__, __len__ are base for Indexing and slicing functionalities.
     - __getitem__ from dict doesnt allow Indexing and Slicing, so having __getitem__ is not enough, implementation matters.
     - In more complete version also methods: __contains__, __iter__, __reversed__, index and count.
    
    All Sequences are also Iterable, but not opposite.
    
    Range object example:
    >>> R = range(3)
    >>> type(R) #
    <class 'range'>
    >>> hasattr(R, "__getitem__")
    True
    >>> hasattr(R, "__len__")
    True
    >>> R[1]
    1
    >>> hasattr(R, "__iter__")
    True
    >>> hasattr(R, "__next__")
    False
        => range objects are Sequences and Iterables, but not Iterators
    
import this # PEP 20, Zen of Python

Narzędzia lintujące (wymuszają dobre praktyki):
  flake8 — sprawdzanie zgodności ze stylem PEP8
  black — automatyczne formatowanie kodu
          (według standardu)
  pylint — bardziej szczegółowa analiza
           jakości kodu
  mypy — sprawdzanie poprawności typów

Zalecenia:
  1. Stosuj PEP 8, PEP 20
  2. Czytelność ważniejsza niż spryt
  3. Używaj typów i adnotacji
  4. List/dict/set comprehensions
     zamiast pętli budujących listy
  5. Enumerate zamiast ręcznego licznika
  6. Zip do łączenia list
  7. Unpacking
  8. Context manager (with)
     zamiast ręcznego otwierania/zamykania
  9. Małe, czytelne funkcje –
     każda powinna robić jedną rzecz.
  10. Docstringi, PEP-257
  11. „DRY” – don’t repeat yourself.
  12. Testy jednostkowe
      (moduł unittest/pytest).
  13. Zasada EAFP:
      „Easier to Ask for Forgiveness than
      Permission”
  14. Zasada SOLID


2. Using the Python Interpreter
  1. Invoking the Interpreter
  2. $ whereis python3 / interactive Mode (run python3)
    $ python3 -c 'from time import sleep;sleep(2);print("I have slept 2 seconds, wow.")'
  3. Argument Passing:
    • sys.argv
    • argparse module
  4. sys.stdin:
    • $ cat sys_module/short_text_file.txt | python3 sys_module/standard_input.py
    • standard_input.py
    • python3 sys_module/standard_input.py <<EOF > 123 > abc > 233342 >EOF
  5. Source Code Encoding: by default UTF-8
    • To declare an encoding other than the default one, a special comment line should be added as the first line of the file. Example:
    • # -*- coding: cp1252 -*-
    • One exception to the first line rule is when the source code starts with a UNIX “shebang” line.
    • #!/usr/bin/env python3 # -*- coding: cp1252 -*-
3. An Informal Introduction to Python
  • floor div (//) returns INT
  • raw string r"..." => raw_string.py
  • revert: s = '123456789' =>s[-1::-1]
  • print(a, end='X', sep=';')
  • iterable object
  • An iterable is any Python object capable of returning its members one at a time, permitting it to be iterated over in a for-loop.
  • sequence object
  • The iterable objects implementing 2 specific methods: '__getitem__()', '__len__()'. So sequences can be Indexed/Sliced.
  • immutable object
  • Values of the object cant be changed (First order values in terms of container like tuple).
    Examples of python immutable data structures: int, float, decimal, bool, string, byte, range, tuple
  • mutable object
  • Values of the object can be changed after the object creation.
    Examples of python mutable data structures: list, dict, set, custom editable classes
4. More Control Flow Tools
  • Python loops:
    • if-elif-else, for-else, while-else
  • Loops related keywords:
    • break, continue, pass
  • match statement (added in 3.10):
  • def http_error(status): match status: case 400: return "Bad request" case 404: return "Not found" case 418: return "I'm a teapot" case _: return "Something's wrong with the internet"
  • Defining Functions:
  • def funct(pos_arg1, pos_arg2=7, /, some_arg3, some_arg4=2, *, k_arg1, k_arg2="dwa")
    def funct(argx, argy, argz, *args, **kwargs):
    default value: the default value is evaluated only once - during parsing stage
    keyword '*'
    • arguments after the keyword have to be keyword-like arguments
    keyword '/'
    • arguments before the keyword have to be positional-like arguments
  • Annotations:
    • funct(a: str, number: int, dogs: tuple[str]) -> list[str]:
    • funct.__annotations__
      {'a': <class "str">, 'number': <class "int">, 'dogs': tuple[str], 'return': list[str]}
  • Docsrings (object.__doc__):
  • ALL modules should normally have docstrings, and ALL functions and classes exported by a module should also have docstrings
    Public methods (including the __init__ constructor) should also have docstrings.
    A package may be documented in the module docstring of the __init__.py file in the package directory.
    PEP-0257 , Google Style Python Docstrings,
6. Modules
  • Python module search algorithm:
    1. built-in modules
    2. sys.path directories
    3. PYTHONPATH env directory
    4. Python installation directories
  • __all__
    • from package import *
    • the list of module names when 'from package import *' is encountered.
  • python -B [python_file]
    • will not write compiled py files like: __pycache__/modx.pyc
  • Namespaces in Python:
  • namespace is the structure used to organize the symbolic names assigned to objects in a Python program
    LEGB rule (can be changed by: global/nonlocal keywords)
    • LEG are implemented as dictionaries / B as module
    • How to see LEGB contents? LE: locals(), G: globals(), dir() and dir(imported_module), B: dir(__builtins__)
    • namespaces creation lifetime:
      • B: created when the Python interpreter starts up, and is never deleted. It lives as __builtins__
      • G (also for module): is created when the module definition is read in; normally, module namespaces also last until the interpreter quits
      • LE: is created when the function is called, and deleted when the function returns or raises an exception that is not handled within the function.
7. Input and Output
  • 'open' usage examples:
    
    with open(file=input_file, mode="r+b") as rb_file:
        ...
        # rb_file object has type: _io.BufferedReader
        # r+ - otwórz plik do odczytu i zapisu, ale plik musi istnieć (nie zostanie utworzony)
        # b  – otwórz w trybie binarnym, czyli bez konwersji znaków i końców linii
    
    with open(file=input_file, encoding="utf-8") as file:
         ...
         # file object has type: _io.TextIOWrapper
         #  - __iter__, __next__
         #  - mode="rt" (default)
8. Errors and Exceptions
  • try / except / else / finally:
  • try:
        ~ code to execute
    except Error1:
        ~ code to handle Error1
    except (Error2, Error3, Error4):
        ~ code to handle Errors2-4
    except Error5 as e5:
        ~ code to handle Error5
        ~ print(e5.args) -> printing Exception arguments
    except:
        ~ code to handle unexpected errors
    else:
        ~ code to be executed if the try clause does not raise an exception
    finally:
        ~ code to be executed in very end
        ~ often for cleanup activities
        ~ ... print("Unexpected error:", sys.exc_info()[0])
        ~ ... raise -> we can even read type of exception and handle it
  • raise ExceptionName

  • builtin exceptions, there are (at least) two distinguishable kinds of errors: syntax errors and exceptions:
    • NameError, OSError, KeyboardInterrupt, SyntaxError (Parsing error)
    • ImportError, IndexError, KeyError, RuntimeError
    • TypeError, AssertionError, ValueError, ZeroDivisionError

  • If you need to determine whether an exception was raised but don’t intend to handle it:
  • try:
      raise NameError('HiThere')
    except NameError:
      print('An exception flew by!')
      raise
    
  • raise NewException(..) from OriginalException(..)
9. Classes

    Class object creation flow (without __slots__ impact):

    • When Interpreter sees 'class' then calling __build_class__(func, name, *bases, **kwargs). Class body is treated as special-function.
      • If metaclass has defined __prepare__ (must return a mapping) then it is called else empty dict is passed further as namespace.

    • Class body is executed and a new namespace is created and used as the local scope.
      • If Class body contains super/__class__ calls then Python creates a cell variable (__classcell__) with __class__ object.
        • This ensures that methods inside the class can access __class__, even before the class is fully defined.

    • Calling [metaclass/type].__new__(name, bases, namespace)
      • custom metaclass can set here own logic like creating attributes, methods (by default type metaclass)
      • If class have descriptor methods then '__set_name__(owner, name)' is called (if __set_name__ is defined).

    • Calling type.__init__(self, name, bases, namespace).
      • For subclasses: type.__init__ calls '__init_subclass__'


  • - isinstance(obj, int)
    - issubclass(float, int)

  • super()
    - technically, it is called "proxy object"
    - allows us to avoid using the base class name explicitly
    - working with Multiple Inheritance

  • Multiple Inheritance
    - class DerivedClass(Base1, Base2, Base3)
    - Diamond problem solved by: DerivedClass.__mro__

  • Privateness
    - “Classic Private” resources accessed only from inside an object don’t exist in Python.
    - Everything about privacy in Python is "Convention":
    - "Pseudo-privacy resource" starts from __ like: __siup are and not accessible directly.
      - but due to Name Mangling the given resource will be available under "_ClassX__siup"
    - "Protected resource" starts from _ like: '_bubu'


  • Iterators:
  • An iterator is a class that implements two special methods: __iter__() and __next__().
    • __iter__(): This method is called when an iterator is required for an object. It should return the iterator object itself.
    • ___next__(): This method should return the next value in the iteration sequence. When there are no more items to return, it should raise the StopIteration exception.

    class SquareIterator:
        def __init__(self, limit):
            self.limit = limit
            self.current = 0
    
        def __iter__(self):
            return self  # The iterator object itself
    
        def __next__(self):
            if self.current < self.limit:
                result = self.current ** 2
                self.current += 1
                return result
            else:
                raise StopIteration  # No more items to iterate over
    
    # Using the custom iterator
    square_iter = SquareIterator(5)  # Squares from 0^2 to 4^2
    
    for square in square_iter:
        print(square)
    
    square_iter2 = SquareIterator(3)  # Squares from 0^2 to 3^2 and raises StopIteration
    print(next(square_iter2))
    print(next(square_iter2))
    print(next(square_iter2))
    print(next(square_iter2))
    
    0
    1
    4
    9
    16
    0
    1
    4
    Traceback (most recent call last):
      File "PATH/iterators.py", line 22, in 
        print(next(square_iter2))
      File "PATH/iterators.py", line 15, in __next__
        raise StopIteration  # No more items to iterate over
    StopIteration
    
    iterator class example

  • Generators:
  • Generators are a functions which return a generator-iterator objects.
    Generator-iterator objects follow Iterator protocol, because of implementing __iter__ and __next__ methods.
    Anything that can be done with generators can also be done with class-based iterators.
    What makes generators so compact is that the __iter__() and __next__() methods are created automatically.
    In addition to automatic method creation and saving program state, when generators terminate, they automatically raise StopIteration.
    These features make it easy to create iterators with no more effort than writing a regular function.

    Simple generator:
    
    def reverse(data):
        for index in range(len(data)-1, -1, -1):
            yield data[index]
    
    for char in reverse('golf'):
        print(char)
    
    f
    l
    o
    g
    
    Great example of usage generators - processing big file:
    def read_large_file(file_path):
        with open(file_path) as file:
            for line in file:
                yield line.strip()
    
    for line in read_large_file("big_file.txt"):
        print(line)  # Processing line without loading full file to memory.
    
    Generators advanced features:
    • sending data to generators: send(value)
    • Send starts generator and sends 'value' to yield expression.
      First generator call has to be via next or send(None).
    • sending exception to generators: throw(Exception, 'Message')
    • closing generators by: close()
    • delegating to other generators by: yield from
    • generator expressions:
    • gen = (x**2 for x in range(5))
      print(next(gen))  # Output: 0
      print(next(gen))  # Output: 1
      



10. Brief Tour of the Standard Library
  • 10.1. Operating System Interface
  • os, shutil modules
  • 10.2. File Wildcards
  • glob module
  • 10.3. Command Line Arguments
  • sys.argv
    argparse module
    example
  • 10.4. Error Output Redirection and Program Termination
  • sys.stderr.write('Warning, log file not found starting a new one\n')
  • 10.5. String Pattern Matching
  •     import re
        re.findall(r'\bf[a-z]*', 'which foot or hand fell fastest')
        re.sub(r'(\b[a-z]+) \1', r'\1', 'cat in the the hat'
        ['foot', 'fell', 'fastest']
        'cat in the hat'
  • 10.6. Mathematics
  • modules: math, random, statistics, secrets
  • 10.7. Internet Access
  • modules: urllib.request, smtplib
  • 10.8. Dates and Times
  • modules: datetime, time
  • 10.9. Data Compression
  • modules: zlib, bx2, lzma, zipfile, tarfile
  • 10.10. Performance Measurement
  • modules: timeit, profile, pstats
  • 10.11. Quality Control
  • modules doctests:
        def average(values):
            """Computes the arithmetic mean of a list of numbers.
    
            >>> print(average([20, 30, 70]))
            40.0
            """
            return sum(values) / len(values)
        import doctest
        doctest.testmod()   # automatically validate the embedded tests
    module unittest:
        import unittest
    
        class TestStatisticalFunctions(unittest.TestCase):
            def test_average(self):
                self.assertEqual(average([20, 30, 70]), 40.0)
                self.assertEqual(round(average([1, 5, 7]), 1), 4.3)
                with self.assertRaises(ZeroDivisionError):
                    average([])
                with self.assertRaises(TypeError):
                    average(20, 30, 70)
    
        unittest.main()  # Calling from the command line invokes all tests
  • 10.12. Batteries Included
  • Python has a “batteries included” philosophy. This is best seen through the sophisticated and robust capabilities of its larger packages. For example:
    • The xmlrpc.client and xmlrpc.server modules make implementing remote procedure calls into an almost trivial task.
    • email, json, csv, sqlite3 modules
    • XML processing is supported by the xml.etree.ElementTree, xml.dom and xml.sax packages
    • Internationalization is supported by a number of modules including gettext, locale, and the codecs package.
11. Brief Tour of the Standard Library — Part II
  • 11.1. Output Formatting
  • reprlib, pprint, textwrap, locale
  • 11.2. Templating
  • string.Template
  • 11.3. Working with Binary Data Record Layouts
  • struct pack and unpack functions
  • 11.4. Multi-threading
  • threading module
    CPython implementation detail: In CPython, due to the Global Interpreter Lock, only one thread can execute Python code at once (even though certain performance-oriented libraries might overcome this limitation). If you want your application to make better use of the computational resources of multi-core machines, you are advised to use multiprocessing or concurrent.futures.ProcessPoolExecutor. However, threading is still an appropriate model if you want to run multiple I/O-bound tasks simultaneously.
    threading examples

Unicode and UTF-8 - default in Python

  • Unicode - text/symbol representation standard (also emoji and special codes)
    • also defined as symbol coding representation system
  • UTF-8 - symbol encoding (to bytes) system
    • every ASCII text is UTF-8 text
    • UTF-8 is default for JSON, XML
  • ASCII - symbol encoding (to bytes) system


Python interpreter, it is python-program-responsible for:

  • Python source code parsing
  • Python source code compiling to bytecode
  • Python bytecode execution.

Python execution model:


Parsing stage:
- the goal is to create specific, custom AST structure
- Python interpreter components: Tokenizer / Lexer, Parser

  • The first step of a parsing process is splitting up 'python code' into a list of pieces usually called tokens. (done by tokenize/tokenizer.c module).
  • The raw list of tokens is input of a Parser component that transforms them to build an Abstract Syntax Tree (based on Python grammar).
  • An AST is a collection of nodes which are linked together based on the grammar of the Python language.
  • AST is a later input for a compiling process when a lower level form of instructions called bytecode is generated.

Compilation stage:
- the goal is to transform AST structure to Python bytecode
- Python interpreter components: Symtable, Compiler (AST → Bytecode), Bytecode emitter, Code objects

  • During compilation, certain constant expressions, such as default argument values, are evaluated.
  • This is where the compile() function works – it transforms the AST into bytecode. Python objects such as functions or classes are created at runtime, but their structure is defined during compilation.

Execution stage (runtime):
  • Python interpreter components: Python Virtual Machine (PVM), Frame Stack / Call Stack, Namespaces: Builtins, Globals, Locals.
  • PVM — conceptual layer realized by function _PyEval_EvalFrameDefault() in ceval.c (Bytecode interpreter is main part).

Runtime is the phase in Python Execution model when:
    - PVM/PVM Interpreter interprets/executes bytecode (instructions saved in stack frame)
        - knowing that PVM / other C-Function are compiled code = machine code (inside python program) that understands bytecode
        - then Python bytecode is read and executed "instruction after instruction" by PVM (compiled CPython machine code)
          (*** important to see that Python source code is never translated to CPU machine code - at least in CPython)
    + mentioned stack frame mechanism functionalities are performed
    + other mechanisms required functions are executed: GC, exception handling, import system




1. Komponenty fazy parsowania (kod źródłowy Pythona -> AST)
 - Tokenizer / Lexer, Parser


2. Komponenty kompilacji (AST → Bytecode)
    [AST]
       ↓
    [symtable.c] → tworzy mapę nazw
       ↓
    [compile.c] → przekształca AST do bytecode
       ↓
    [codeobject.c] → tworzy finalny PyCodeObject


Bytecode w CPythonie to:
 - zestaw instrukcji wirtualnej maszyny Pythona (PVM) [nie są kodem maszynowym CPU, więc nie są bezpośrednio wykonywane przez CPU]
 - lecz są instrukcjami rozumianymi przez interpreter PVM (czyli część softwarej maszyny PVM).


3. Runtime (Execution stage)
        ┌────────────────────────┐
        │                        │
        │   Python VM (ceval.c)  │ ← główny silnik wykonania
        │     ↓ eval loop        │
        │                        │
        └────────┬───────────────┘
                 │
                 │

    PVM w CPythonie to funkcja _PyEval_EvalFrameDefault() z pliku ceval.c,
     - jej zadaniem jest analiza bytecode krok po kroku, realizując go przez funkcje w C.
     - jej podstawową częścią jest interpreter bytecode'u Pythona.

                 │
                 │
                 │
        ┌────────▼──────────────┐
        │   Call Stack / Frames │ ← wykonanie funkcji, zakresy zmiennych
        └────────┬──────────────┘
                 │
     ┌───────────▼─────────────┐
     │ Memory Mgmt + GC        │ ← alokacja, refcount, GC
     └───────────┬─────────────┘
                 │
        ┌────────▼─────────────┐
        │ Namespaces & Builtins│ ← globals, locals, __builtins__
        └──────────────────────┘



Computational complexity of algorithms:

Python memory usage


In Linux it proceeds to understanding below notions / aspects:
    - all resources are eventually provided by kernel
    - virtual memory and other memory management functions
    - concurrency in Python from perspective of memory management
    - Call Stack in Python, Heap memory in Python / processor stack

    - OS level memory optimizations - like using processor cache memory for often used objects:
    - what are other processor / OS memory optimizations technics?
        - Paging and Swapping, Lazy Allocation, Transparent Huge Pages, Memory Overcommit
        - Copy-on-Write (CoW) - kiedy proces tworzy kopię (np. fork), Linux stosuje Copy-on-Write:
            - Pamięć współdzielona między procesami nie jest kopiowana od razu.
        - Kernel Same-page Merging (KSM), NUMA (Non-Uniform Memory Access), Asynchronous I/O i Direct Memory Access (DMA)
        - Reclaiming Memory (odzyskiwanie pamięci), HugeTLB (Huge Translation Lookaside Buffer),
        - Pamięć mapowana na pliki (Memory-Mapped Files), Linux pozwala na mapowanie plików bezpośrednio do pamięci (mmap)
        - Cgroups i limity pamięci, parametr swappiness kontroluje, jak agresywnie Linux korzysta z pamięci wymiany (swap)
        - Linux używa technik takich jak prefetching i readahead, aby przewidywać, jakie dane mogą być potrzebne i ładować je wcześniej

    - Python memory optimization technics:
        - object interning / object sharing
        - freelist - reusing memory - or object caching (often used objects), empty dict are reused
        - constant pool (for constant variables in functions)
        - dynamic memory allocation for mutable objects
        - optimization in hash tables (dicts)
        - Lazy Evaluation i View Objects (Python User level)


Python Call Stack (Pythonowy Stos wywołań):
    - W językach niskopoziomowych, takich jak C, stos wywołań jest zarządzany bezpośrednio przez system operacyjny i procesor.
    - W CPython stos wywołań jest zarządzany interpreter:
        - dodaje to narzut, ale umożliwia np dynamiczne zarządzanie pamięcią (stos odpowiada za zarządzanie pamięcią przy wywoływaniu funkcji)

    Jak to umożliwia dynamiczne zarządzanie pamięcią?
     - Ramki wywołań są dynamiczne i nie muszą mieć z góry określonego rozmiaru — stos wywołań dopasowuje się do rozmiaru wywołań funkcji
     - mniejsze zaangażowanie Garbage Collector
         - GC nie działa bezpośrednio na stosie, gdy funkcja kończy działanie, jej lokalne zmienne są usuwane natychmiast (bez interwencji GC).
         - Liczniki referencji Garbage Collector pozwalają na automatyczne usuwanie obiektów, na które wskazywały referencje ze stosu.
    Narzut związany z dynamicznym zarządzaniem pamięcią:
        - Zarządzanie pamięcią przez licznik referencji - działanie Garbage collectora jest kosztowne pod względem czasowym
        - Zastosowanie dynamicznych ramek wywołań również wiąże się z pewnym kosztem pamięci i czasu.
          Tworzenie i usuwanie ramek wywołań przy każdym wywołaniu funkcji ma swój narzut czasowy.

Heap memory in Python:
    - dynamic virtual memory assigned to the given process (logically it is continual address space)
    - used automatically when objects in Python are created
    - global memory space (subspace of heap memory)
    - Garbage Collector dba o zwalnianie pamięci obiektów przechowywanych na stercie.
    - Kiedy obiekt jest nieosiągalny (nie ma referencji do niego), GC usuwa go z pamięci. Jest to zarządzanie dynamiczne.


More about Call Stacks in Python:

- dokładniej w Pythonie/OS funkcjonują dwa stosy:
  - klasyczny stos procesora. C stack (nowy per proces / wątek)
    - Jest to stos na poziomie systemowym, zarządzany przez system operacyjny i procesor.
    - W przypadku Pythona (zwłaszcza CPython), ten stos jest używany przy wywoływaniu funkcji niskopoziomowych napisanych w C.
    - Interpreter Pythona (który jest implementowany w C w CPythonie) korzysta z tego stosu przy wykonywaniu kodu Pythona.

  - Pythonowy Stos wywołań (nowy per proces / wątek)

    - Stos Pythonowy jest realizowany przy pomocy ramek wywołań (PyFrameObject) - zarządzany przez interpreter
    - dokładniej, w CPython stos wywołań i ramki funkcji są zaimplementowane na poziomie kodu źródłowego interpretera w języku C
    - dzieje się to w: Python/ceval.c: w głównej pętla interpretera, gdzie wykonywany jest bytecode i zarządzane są ramki
        - short-living data like local variable references in function scope that are packed in Frames (when functions are called)
        - upraszaczając Stos wywołań bazuje na polu f_back w PyFrameObject, który wskazuje na poprzednią ramkę (rodzica)
        - Każda ramka odpowiada jednemu wywołaniu funkcji (lub blokowi kodu wykonywanemu przez interpreter, np. w module, klasie, itp.)
        - Dzięki tej konstrukcji Python śledzi historię wywołań i pozwala debugować kod (np. tracebacks w przypadku błędów).

        - Ramki funkcji: PyFrameObject - Include/frameobject.h - kluczowe elementy struktury:
            - f_back: Wskaźnik na poprzednią ramkę. Łączy ramki w stos wywołań
            - f_code: Odniesienie do obiektu kodu (bytecode) odpowiadającego funkcji.
            - f_valuestack: Wskaźnik na stos wartości używanych przez bytecode (np. zmienne, wyniki operacji).
            - f_blockstack: Stos bloków try/except i pętli.

            Generatory, yield:
                - struktura PyGenObject, przechowuje wszystkie informacje potrzebne do wznowienia wykonania generatora po każdym yield
                - działanie ramek funkcji oraz stosu wywołań różni się nieco od standardowego działania funkcji
                - dzięki mechanizmowi yield realizowane jest zatrzymywanie i wznawianie funkcji
                  a ramki funkcji generatora są przechowywane między kolejnymi wywołaniami,
                  co pozwala na efektywne zarządzanie pamięcią i realizację leniwego przetwarzania danych
                - Kiedy interpreter napotyka yield, wykonuje następujące kroki:
                    - Zapisuje stan funkcji (ramkę: PyFrameObject) do struktury obiektu generatora (PyGenObject).
                    - Zwraca kontrolę do wywołującego kodu i wartość, którą zdefiniowano przy "yield var_value"

                - Generatory są przydatne w sytuacjach, gdy nie musisz przechowywać wszystkich wartości w pamięci na raz:
                  (Kiedy generator naprawdę oszczędza pamięć?)
                    - Use Case 1: Iterowanie przez wartości bez ich przechowywania (przetwarzanie dużych zbiorów danych)
                    - Use Case 2: Przetwarzanie danych w strumieniu (stream processing)
                    - Use Case 3: Obsługa nieskończonych sekwencji
                    - Use Case 4: Przetwarzanie dużych plików z logami
                        - dzięki generatorowi możesz wczytywać/przetwarzać plik linia po linii bez przechowywania całego pliku w pamięci
                - Istotne - Kiedy generatory nie są potrzebne?
                    - gdy musisz przechowywać wszystkie generowane wartości w pamięci
                    - gdy potrzebujesz wielokrotnego dostępu do wyników (np. musisz iterować przez te same dane wiele razy)

            asyncio, korutyny:
                - Stos Pythonowy, w przypadku korutyn (async def) działa to trochę inaczej
                - każda korutyna ma swój własny kontekst wykonania, który przechowuje dane lokalne i stan wywołania
                - Zamiast typowego stosu ramek funkcji, korutyna "zamraża" swój stan w obiekcie CoroWrapper (zarządzanym przez pętlę zdarzeń)
                    - gdy korutyna jest "zamrażana" Pętla zdarzeń usuwa jej ramkę ze stosu Pythonowego
                    - wznowienie korutyny powoduje również przywrócenie jej ramki na stos Pythonowy
The Python Standard Library
  • Introduction
  • Built-in Functions
    • abs, aiter, all, anext, any, ascii, bin, bool, breakpoint, callable, chr, classmethod, compile, complex,
    • delattr, dir, divmod, enumerate, eval, exec, filter, float, format, getattr, globals, hasattr, hash, help,
    • id, input, int, isinstance, issubclass, iter, len, locals, map, max, min, next, object, oct, open, ord,
    • pow, print, property, repr, reversed, round, setattr, slice, sordeted, staticmethod, sum, super, type, vars, zip, __import__
  • Built-in Constants
  • Built-in Exceptions
  • Text Processing Services
  • Binary Data Services
  • Data Types
  • Numeric and Mathematical Modules
  • Functional Programming Modules
  • File and Directory Access
  • Data Persistence
  • Data Compression and Archiving
  • File Formats
  • Cryptographic Services
  • Generic Operating System Services
  • Concurrent Execution
  • Networking and Interprocess Communication
  • Internet Data Handling
  • Structured Markup Processing Tools
  • Internet Protocols and Support
  • Multimedia Services
  • Internationalization
  • Program Frameworks
  • Graphical User Interfaces with Tk
  • Development Tools
  • Debugging and Profiling
  • Software Packaging and Distribution
  • Python Runtime Services
  • Custom Python Interpreters
  • Importing Modules
  • Python Language Services
  • MS Windows Specific Services
  • Unix Specific Services
  • Modules command-line interface (CLI)
  • Superseded Modules
  • Removed Modules
  • Security Considerations

Python garbage collection (GC)

It is builtin mechanism for memory management. Main goal is memory recovery.
From practical point of view it is process of auto-deleting not-used objects from memory. For detecting that objects Python uses:
  • Reference counting, mechanism of counting how many times object is used.
  • Cycle detection, mechanism for finding object referencing each other.

GIL (Global Interpreter Lock)

Global Interpreter Lock - mechanism used in CPython, it ensures that in one moment only pone thread can execute Python code.
  • What problems GIL solves for Python?
    • Multi-threading access potential issues.
    • Race Conditions in Reference Counting (Garbage Collection).
    • Complexity of Fine-Grained Locks. The GIL ensures safety and simplicity.
  • Why Was the GIL Chosen as the Solution?
    • Simplicity and performance aspects.
  • The Impact on Multi-Threaded Python Programs.
    • Multi-threading in Python doesn't achieve true parallelism on multi-core processors (except in specific cases, like I/O-bound operations or extensions written in C that release the GIL).
  • Why Hasn’t the GIL Been Removed Yet?
    • Due to several reasons being a combination of technical complexity, backward compatibility concerns, and trade-offs in performance and simplicity.

Metaprogramming / Metaclass

Key Concepts in Metaprogramming:
  • Dynamic Class Creation: Creating classes at runtime.
  • Foo = type('Foo', ('FooBase1', 'FooBase2'), {'attr': 100,'attr_val': lambda x : x.attr})
    
  • Decorators: Functions that modify the behavior of other functions or methods or classes.
  • Descriptors are objects that manage the access to attributes of other objects.
    • objects with manipulated dunder methods: __get__, __set__, __delete__
  • Magic Methods: Special methods that let you control how Python interacts with objects (e.g., __getattr__, __setattr__).
  • exec() and eval(): Functions for dynamic execution of Python code.
Other aspect of metaprogramming in Python are Custom Metaclasses:
    class Meta(type):
        def __new__(cls, name, bases, dct):
            x = super().__new__(cls, name, bases, dct)
            x.attr = 100
            return x

    class Foo(metaclass=Meta):
        pass

>>> Foo.attr
100

Descriptors

Descriptors are a powerful, general purpose protocol.
    - If an object defines __set__() or __delete__(), it is considered a data descriptor.
    - They are the mechanism behind properties, methods, static methods, class methods, and super()
        - If an object defines __set__() or __delete__(), it is considered a data descriptor.
        - Descriptors that only define __get__() are called non-data descriptors (they are often used for methods but other uses are possible).
    - Descriptor invocation logic: The mechanism for descriptors is embedded in the __getattribute__() methods for object, type, and super().
        - Overriding __getattribute__() prevents automatic descriptor calls because all the descriptor logic is in that method.
    - __set_name__ - Customized names - The implementation details are in type_new() and set_names() in Objects/typeobject.c

Basic Descriptor protocol:
    __get__(self, obj, objtype) – returns attribute value (can be calculated).
    __set__(self, obj, value) – set attribute value
    __delete__(self, obj) – delete attribute

Mechanics of Descriptor invocation logic:
    - Descriptors are invoked by the __getattribute__() method.
    - Classes inherit this machinery from object, type, or super().
    - object.__getattribute__() and type.__getattribute__() make different calls to __get__().
    - All functions written in Python are descriptors (have __get__), when functions written in C are not.

So, Descriptors Use Cases:
    - mechanism behind properties, bound methods, static methods, class methods, and super()
    also:
    - Dynamics lookups
    - Logged Access
    - Custom Validators
    - Customized attribute names
    - data descriptors could be used to implement an ORM

Python protocols:

    - Can Python be named protocol oriented language?

    - Class creation protocol
    - Sequence protocol
    - Attribute access protocol
    - Iterable Protocol
    - Numeric Protocol
    - Context Manager protocol
    - Callable protocol
    - Descriptor protocol
    - Buffer protocol
    - Set protocol
    - Async Protocols