- 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.
- 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.
gen = accumulator() gen.send(10) # ❌ TypeError: can't send non-None value to a just-started generator
- 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.
- 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.
- lru_cache / cached_property - weakref module - asyncio / threading
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.
>>> 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?
- In slicing (advanced indexing) - As a placeholder in code (“to be implemented later”) - In type hints (PEP 484 / typing) - For custom or creative uses
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)
- functions calls, - importing modules, - execution main script (__main__), - execution classes call body. - also, eval/exec calls can cause additional Frames are created.
- PEP 697, Python 3.12 introduces a zero-cost mechanism that optimizes the way metaclasses are determined.
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?
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).
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
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.
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.
- multiple threads or processes access shared data and at least one modifies it, but the access is not properly synchronized.
- ensuring that only one thread can modify 'specific shared resource' at a time.
- fastapi, pydantic - Django simple personal website - Bigger project ideas
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
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.
>>> 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)
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
python3 sys_module/standard_input.py <<EOF > 123 > abc > 233342 >EOF
#!/usr/bin/env python3 # -*- coding: cp1252 -*-
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"
'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)
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
try:
raise NameError('HiThere')
except NameError:
print('An exception flew by!')
raise
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, initerator class exampleprint(next(square_iter2)) File "PATH/iterators.py", line 15, in __next__ raise StopIteration # No more items to iterate over StopIteration
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
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.
gen = (x**2 for x in range(5)) print(next(gen)) # Output: 0 print(next(gen)) # Output: 1
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'
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
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
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__
└──────────────────────┘
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
Foo = type('Foo', ('FooBase1', 'FooBase2'), {'attr': 100,'attr_val': lambda x : x.attr})
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 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
- 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