Interpreter
Інтерпретатор⚑
Що таке інтерпретатор і як він працює в Python?⚑
Інтерпретатор - це програма, яка зчитує і аналізує код вихідного тексту мови програмування та виконує його рядок за рядком.
У Python інтерпретатор виконує такі дії: - спочатку він аналізує код, перетворюючи його на проміжний байт-код - потім виконує цей байт-код за допомогою вбудованої віртуальної машини Python (Python Virtual Machine, CPython).
Які є види інтерпретаторів Python?⚑
- CPython — основний і найпоширеніший інтерпретатор Python, написаний на C. Використовується за замовчуванням і підтримується Python Software Foundation. Підходить для більшості застосувань та інтеграцій.
- Jython — версія Python, написана на Java, яка дозволяє запускати Python-код у середовищі JVM (Java Virtual Machine). Використовується коли потрібно інтегрувати Python з Java-бібліотеками.
- IronPython — реалізація Python для .NET, написана на C#. Це дозволяє використовувати Python у проектах з технологіями .NET, інтегруючи його з бібліотеками .NET.
- PyPy — це високопродуктивна реалізація мови програмування Python. Підтримує JIT-компіляцію (just-in-time). PyPy часто швидший, ніж CPython, оскільки він компілює код безпосередньо під час виконання.
Що таке Cython?⚑
Cython — це оптимізатор та мова програмування, яка дозволяє писати Python-подібний код, що компілюється у C/C++ для прискорення роботи.
Cython дозволяє використовувати можливості мови Python та C/C++ для ефективного написання розширень модулів мовою Python. Він дозволяє писати код на Python, який доступний із C/C++, і навпаки. Cython забезпечує швидкість виконання, порівнянну зі швидкістю виконання мовою C/C++, при цьому зберігаючи простоту та зручність використання мови Python. Cython compiler компілює вихідний код C/C++ і потім переводить його в машинний код, що дає швидкий доступ до низькорівневих ресурсів операційної системи, таким як пам'ять і введення-виведення. Cython також надає можливість використовувати додаткові функції, такі як статична типізація та паралельне програмування для додаткового збільшення продуктивності.
Які механізми використовує Python для очищення пам'яті?⚑
Python має 2 основних механізмів для очищення пам'яті - Підрахунок посилань (Reference Counting). Python відстежує кількість активних посилань на об'єкт. Коли лічильник посилань об'єкта зменшується до нуля (тобто на об'єкт більше ніхто не посилається), пам'ять, зайнята цим об'єктом, автоматично звільняється. - Збирання сміття (Garbage Collection). Для об'єктів, що можуть утворювати циклічні посилання, Python використовує garbage collector, який автоматично видаляє такі об'єкти. Це важливо для структур даних, які можуть мати взаємні посилання, що не дозволяють зменшити лічильник посилань до нуля.
У Python алгоритм підрахунку посилань є фундаментальним і не може бути вимкнений, тоді як GC є необов'язковим і може бути вимкнений. - Модуль gc
надає програмістам інструменти для управління процесом збирання сміття, включаючи можливість примусового запуску gc.collect()
для звільнення пам'яті або зміни налаштувань частоти запуску garbage collection. Модуль також дозволяє тимчасово вимкнути автоматичне збирання сміття, якщо є необхідність у мануальному контролі.
- Оптимізація пам'яті. Python має внутрішні механізми кешування, зокрема для малих об'єктів (як, наприклад, малі цілі числа чи короткі рядки), які повторно використовують пам'ять, запобігаючи зайвим витратам.
Що таке збирач сміття (garbage collector)?⚑
Збирач сміття (generational garbage collector або GC) - це механізм, створений для виявлення та видалення циклічних посилань. gc
є вбудованим модулем в Python і може бути вимкнений або запущений вручну (або не запущений) за потреби.
На відміну від інших популярних мов програмування, Python не повністю звільняє всю пам'ять назад до операційної системи, як тільки він видаляє якийсь об'єкт. Замість цього, він використовує додатковий менеджер пам'яті для малих об'єктів (розміром менше 512 байтів). Для роботи з такими об'єктами він виділяє великі блоки пам'яті, в яких в подальшому будуть зберігатися багато малих об'єктів.
Коли один з малих об'єктів видаляється, пам'ять під ним не повертається операційній системі, Python залишає її для нових об'єктів з таким самим розміром. Якщо в одному з виділених блоків пам'яті не залишилося об'єктів, Python може звільнити його операційній системі. Звільнення блоків зазвичай відбувається, коли скрипт створює багато тимчасових об'єктів.
Таким чином, якщо довгоживучий процес Python з часом починає споживати більше пам'яті, це зовсім не означає, що в коді є проблема з витоком пам'яті.
Стандартний інтерпретатор Python (CPython) використовує два алгоритми збирання сміття: підрахунок посилань (reference counting) та збирач сміття (generational garbage collector або GC), більш відомий як стандартний модуль gc
в Python.
Алгоритм підрахунку посилань дуже простий і ефективний, але він має один великий недолік. Він не може виявити циклічні посилання. Саме тому в Python існує додатковий збирач, який називається GC і відстежує об'єкти з потенційними циклічними посиланнями. Коли об’єкти посилаються один на одного, утворюючи цикли (A посилається на В, В посилається на С, С посилається на А). У таких випадках, навіть якщо всі зовнішні посилання на ці об’єкти видалені, їхні лічильники посилань не стають нульовими, і пам’ять не звільняється.
На відміну від алгоритму підрахунку посилань, збирач сміття не працює в режимі реального часу і запускається періодично. Кожен запуск збирача створює мікропаузи в роботі коду.
Збирач сміття розділяє всі об'єкти на 3 покоління. Нові об'єкти потрапляють до першого покоління. Якщо новий об'єкт виживає після процесу збирання сміття, він переміщується до наступного покоління. Чим вище покоління, тим рідше воно сканується на сміття. Оскільки нові об'єкти часто мають дуже короткий термін життя (є тимчасовими), то їх варто опитувати частіше, ніж ті, які пройшли кілька етапів збирання сміття.
У кожному поколінні є спеціальний лічильник і поріг спрацювання, які визначають початок процесу збирання сміття. Кожен лічильник зберігає кількість алокацій мінус кількість deallocations (звільнень) у даному поколінні. Як тільки в Python створюється який-небудь контейнерний об'єкт, він перевіряє ці лічильники. Якщо умови спрацьовують, починається процес збирання сміття.
Якщо одразу кілька або більше поколінь перевищили поріг, вибирається найстарше покоління. Це зроблено через те, що старі покоління також сканують всі попередні. Щоб зменшити кількість пауз збирання сміття для довгоживучих об'єктів, найстарша генерація має додатковий набір умов.
Стандартні пороги спрацювання для поколінь встановлені на значення 700, 10 і 10 відповідно, але завжди можна змінити їх за допомогою функцій gc.get_threshold
та gc.set_threshold
.
import gc
print(gc.get_threshold()) # (700, 10, 10)
gc.set_threshold(500, 10, 10)
print(gc.get_threshold()) # (500, 10, 10)
Links
- Всё, что нужно знать о сборщике мусора в Python
- CPython Garbage Collection: The Internal Mechanics and Algorithms
Як працює підрахунок посилань (Reference Counting)?⚑
Підрахунок посилань (Reference Counting) - механізм, який допомагає очищувати пам'ять інтерпретатору Python.
Приклад роботи Reference Counting - Коли ми присвоюємо змінній якесь значення a = 1000
, Python не зберігає число 1000
у змінній а
, натомість він створює об’єкт (у цьому випадку ціле число 1000
) в пам’яті та зберігає посилання на цей об’єкт у змінній а
. - Якщо ми присвоїмо a
нове значення, наприклад а = 2000
, Python створить новий об’єкт (число 2000
) і змінить посилання в а
, щоб воно вказувало на цей новий об’єкт. Попередній об’єкт (число 1000
) залишається в пам’яті, поки на нього будуть існувати інші посилання. Як тільки посилань не залишиться, його пам’ять може бути звільнена. - Якщо ми присвоїмо b = a
, то змінна b
тепер теж буде посилатися на той самий об’єкт, на який посилається a
. Тобто і a
, і b
вказуватимуть на одне і те ж число 20
в пам’яті.
У Python кожен об’єкт у пам’яті містить поля ob_refcnt
та ob_type
. Вони є частинами внутрішньої структури об’єктів і відіграють ключову роль в управлінні пам’яттю та визначенні типу об’єкта. - ob_refcnt
(Reference Count) — це лічильник посилань на об’єкт. Він зберігає кількість посилань, які вказують на цей об’єкт. Коли ми створюємо нове посилання на об'єкт (наприклад, присвоюємо його іншій змінній), ob_refcnt
інкрементується. Коли одне з посилань видаляється або змінюється (наприклад, коли змінна, яка посилалася на об’єкт, змінює своє значення), ob_refcnt
зменшується. Коли лічильник досягає нуля (тобто на об’єкт більше немає посилань), Python автоматично звільняє пам’ять, зайняту цим об’єктом. Цей процес є частиною механізму збирання сміття (garbage collection). - ob_type
(Type Information) — це вказівник на структуру, яка описує тип об’єкта. Цей вказівник посилається на відповідний тип об’єкта, наприклад ціле число, рядок, список тощо. ob_type
визначає, які операції можна виконувати над об’єктом і як ці операції слід реалізовувати. Наприклад, для числа і рядка визначаються різні способи обчислення, копіювання та перетворення.
ob_refcnt
забезпечує автоматичне керування пам’яттю, допомагаючи Python звільняти пам’ять, коли вона більше не потрібна. ob_type
гарантує, що кожен об’єкт має правильний тип і що всі операції з ним виконуються відповідно до цього типу.
Перевірити скільки посилань має той чи інший об’єкт у пам’яті, можна за допомогою бібліотеки sys
.
import sys
print(sys.getrefcount('new object')) # 3
print(sys.getrefcount(0)) #1000000791
a = 0
b = 'new object'
print(sys.getrefcount('new object')) # 4
print(sys.getrefcount(0)) # 1000000792
Функція sys.getrefcount
сама посилається на об’єкти, тому на тільки-но створений об’єкт маємо одразу три посилання. На невеликі числа часто посилається сам Python-інтерпретатор, тому кількість посилань на них буде досить великою.
Stack, heap⚑
Стек (Stack) - область оперативної пам'яті, яка створюється для кожного потоку. Він працює в порядку LIFO (Last in, First out), тобто кожен останній доданий в стек кусок пам'яті буде першим в черзі на вивід зі стеку. Кожен раз, коли функція оголошує нову змінну, вона додається в стек, а коли змінна пропадає з області видимості (коли функція завершується), вона автоматично видаляється зі стеку.
Купа (Heap) - сховище пам'яті, яке розміщене в оперативній пам'яті, де пам'ять виділяється та звільняється динамічно, за потреби. Об’єкти в купі можуть створюватися в будь-який момент, і Python зберігає їх у купі, поки вони потрібні. Купа - це просто склад для змінних. Пам’ять під об’єкти в купі може бути звільнена в будь-який час, коли ці об’єкти більше не використовуються. Ця гнучкість дозволяє зберігати об’єкти з довшим життям і різними розмірами, що не завжди зручно або можливо зробити в стеку. Коли виділяється в купі ділянка пам'яті для зберігання змінної, до неї можна звернутись не тільки в потоці, але й в усьому додатку. Саме так визначаються глобальні змінні.
Взаємодія з купою відбувається за допомогою посилань-вказівників. Створюючи вказівник, ви вказуєте на місцезнаходження пам'яті в купі, що задає початкове значення змінної, і говорить програмі, де отримати доступ до цього значення.
Як Python працює з пам'ятью?⚑
В Python диспетчер пам'яті відповідає за розподіл і звільнення пам'яті. Він виділяє пам'ять у вигляді простору в купі, в якому зберігаються всі об'єкти Python та структури даних. Пам’ять у Python можна розділити на 4 категорії, кожна з яких відповідає за зберігання різних типів даних і структур. Цей простір недоступний для програміста безпосередньо, проте базовий API дозволяє розробнику отримати доступ до деяких інструментів для написання коду.
- Об'єктно-специфічна пам’ять — це частина пам’яті, яка виділяється для об’єктів, що використовують спеціалізовані механізми управління пам’яттю, відмінні від стандартних. Це дозволяє оптимізувати зберігання та доступ до даних для певних типів об’єктів.
- Необ'єктна пам’ять ядра Python — це частина пам’яті, яка виділяється для зберігання даних та структур, що не є об’єктами Python. Вона використовується самим інтерпретатором для забезпечення своєї роботи та управління виконанням програм. Вона містить дані, необхідні для роботи самого інтерпретатора Python, такі як стеки викликів, таблиці символів, інформація про потоки виконання, конфігураційні параметри, налаштування середовища виконання.
- Пам'ять об’єктів — це частина пам’яті, яка виділяється для зберігання самих об’єктів Python. Кожен об’єкт у Python (числа, рядки, кастомні класи тощо) займає певний обсяг пам’яті, який необхідний для зберігання його даних та метаданих. Ця частина пам’яті містить у собі реальні дані, які зберігає об’єкт (наприклад, значення числа, текст рядка тощо).
- Внутрішні буфери — це області пам’яті, які виділяються інтерпретатором Python для тимчасового зберігання даних під час виконання різних операцій. Вони потрібні для оптимізації роботи та забезпечення ефективності різних процесів.
- Буфери вводу/виводу зберігають дані, що читаються з файлів або мережі, а також дані, що готуються для запису. Це дозволяє оптимізувати операції вводу/виводу, зменшуючи кількість системних викликів.
- Буфери інтерпретації коду зберігають проміжні результати під час компіляції та виконання байт-коду Python.
- Кеші слугують для зберігання даних, що часто використовуються, або результатів обчислень, щоб уникнути повторних витратних операцій.
Коли програма запитує пам’ять, CPython-інтерпретатор використовує malloc
метод для запиту цієї пам’яті в операційної системи, і розмір приватної купи збільшується. Щоб уникнути виклику malloc
і free
для кожного створення і видалення невеликого об’єкта, CPython визначає кілька алокаторів і деалокаторів для різних цілей. Алокатор пам’яті — це «менеджер», що відповідає за виділення і звільнення пам’яті для програм, які її потребують.
Коли об’єкт потребує пам’яті і для нього визначені специфічні алокатори, виділення пам’яті відбувається саме через ці спеціальні алокатори. Якщо для об’єкта немає визначених алокаторів і потрібен обсяг пам’яті понад 512 байтів, менеджер пам’яті Python безпосередньо звертається до алокатора необробленої пам’яті. Якщо ж обсяг запитуваної пам’яті менший за 512 байтів, то для її виділення використовуються стандартні алокатори, оптимізовані для роботи з дрібними об’єктами.
Стандартний алокатор складається з трьох компонентів: арени, пули та блоки.
- Арена — це велика область пам’яті (зазвичай 256 кБ), яка виділяється операційною системою. Python керує кількома аренами, кожна з яких може містити кілька пулів.
- Пул — це фрагмент пам’яті фіксованого розміру (зазвичай 4 кБ), що виділяється з арени. Кожен пул спеціалізується на виділенні пам’яті під об’єкти певного розміру. Наприклад, один пул може використовуватися для об’єктів розміром 16 байтів, інший — для об’єктів розміром 32 байти тощо. Це дозволяє ефективно використовувати пам’ять і зменшити фрагментацію.
- Блок — це найменша одиниця пам’яті, яка виділяється для об’єктів. Кожен пул ділиться на блоки однакового розміру. Коли створюється об’єкт, пам’ять під нього виділяється у вигляді одного або кількох блоків з відповідного пулу.
Коли потрібно виділити пам’ять під новий об’єкт, Python шукає потрібну арену. Якщо не знаходить, запитує нову в операційної системи. Для того щоб додати новий об’єкт, обирається найбільш заповнена арена для компактнішого розміщення об’єктів у пам’яті. Далі вибирається необхідний пул (відповідно до розміру об’єкта) і виділяється потрібна кількість блоків. Якщо в пулі немає вільних блоків, виділяється новий пул із наявної арени або створюється нова арена. Така структура дозволяє ефективно управляти пам’яттю, зменшуючи фрагментацію та прискорюючи виділення пам’яті. Це особливо корисно для малих об’єктів, які часто створюються і знищуються в Python.
Стандартний інтерпретатор Python (CPython) використовує два алгоритми збирання сміття: підрахунок посилань (reference counting) та збирач сміття (generational garbage collector або GC).
Також важливо відзначити, що в Python є різні види об'єктів, такі як незмінні (immutable) і змінні (mutable), і вони мають різні правила використання пам'яті та керування нею.
Links
- Мистецтво управління пам’яттю в Python: розуміння, використання та оптимізація
- Как работает память в Python
- Память и Python
Як дізнатись, скільки пам'яті займають об'єкти в Python?⚑
Модуль sys
у стандартній бібліотеці Python надає функцію getsizeof
, яка дозволяє дізнатися, скільки пам’яті займає певний об’єкт у байтах. Це може бути корисно для аналізу використання пам’яті в програмі, особливо якщо працювати з великими даними або оптимізувати використання пам’яті. Функція викликає дандер-метод __sizeof__
об’єкта.
У випадку з вбудованими типами даних (списки, словники, кортежі тощо), __sizeof__
визначає основний розмір об’єкта, але не включає розмір об’єктів, на які він посилається. Тому для складних об’єктів (наприклад, вкладених списків) getsizeof
може не повністю відображати весь використовуваний обсяг пам’яті.
import sys
from decimal import Decimal
print(sys.getsizeof(42)) # 28
print(sys.getsizeof(Decimal(5.3))) # 104
print(sys.getsizeof([1, 2, 3, 4, 5])) # 104
print(sys.getsizeof('Hello, World!')) # 62
print(sys.getsizeof('')) # 49
print(sys.getsizeof('Hi')) # 51
print(sys.getsizeof('Hello, World!')) # 62
print(sys.getsizeof([])) # 56
print(sys.getsizeof([1])) # 64
print(sys.getsizeof([1, 2, 3, 4])) # 88
print(sys.getsizeof([1, 2, 3, 4, 5])) # 104
print(sys.getsizeof([1, 2, 3, 4, 5, 6])) # 104
print(sys.getsizeof(())) # 40
print(sys.getsizeof((1, ))) # 48
print(sys.getsizeof((1, 2, 3, 4))) # 72
Порожній список має накладні витрати на зберігання метаданих, таких як інформація про тип об’єкта, розмір, кількість елементів і посилання на самі елементи. Кортежі займають менше місця, ніж списки. Це відбувається тому, що кортежі є незмінними структурами даних. Оскільки їхній вміст не може змінюватися після створення, Python може оптимізувати їх зберігання. Списки, на відміну від кортежів, є змінними, і для підтримки цієї гнучкості їм потрібно більше накладних витрат.
Яка різниця між файлами .py та .pyc?⚑
Розширення .py
вказує на файли з вихідним кодом Python, в яких міститься зрозумілий для розробників текст програми. В той час як файли .pyc
містять байт-код цих вихідних файлів Python. Інтерпретатор перетворює файли .py
в файли .pyc
при першому виконанні програми, коли імпортується модуль, та зберігає їх у папці __pycache__
. Цей каталог зазвичай знаходиться в тому ж каталозі, що й файли .py
, але може також знаходитися в тимчасовому каталозі системи, якщо вихідний каталог доступний тільки для читання.
Скомпільований байт-код дозволяє зменшити час запуску програми та прискорити завантаження модулів при наступних запусках програми, оскільки інтерпретатор може використовувати вже скомпільований байт-код замість повторної компіляції вихідного коду. Взаємодія з каталогом pycache
або .pyc
файлами в ньому автоматично керуються інтерпретатором Python.
Файли .pyc
є специфічними для версії Python, тому файли, створені для однієї версії Python, не працюватимуть з іншою версією.
PEP 3147 описує логіку імпорту модулів з використанням .pyc
файлів - https://peps.python.org/pep-3147/#flow-chart
Як зробити скрипт Python для запуску в Unix ?⚑
Для цього повинні виконуватися дві умови: - Скриптований файл повинен мати відповідні права на виконання (наприклад, за допомогою команди chmod +x script.py
). - Перший рядок повинен починатися зі знаку хеша (решітки, hash(#)), та повинен містити шлях до встановленої версії Python, наприклад: #!/usr/local/bin/python
Що таке PYTHONPATH?⚑
PYTHONPATH - це змінна середовища в якій зберігаються шляхи, які інтерпретатор Python проходить для пошуку модулів. Коли імпортується модуль у своєму коді, Python шукає модулі спочатку в поточній директорії, а потім в PYTHONPATH
або sys.path
.
Що таке віртуальне оточення та venv
?⚑
Віртуальне оточення – це механізм, який дозволяє створювати ізольовані оточення для встановлення та використання пакетів Python. Це корисно, коли потрібно встановити певну версію пакета або коли потрібно мати одночасно доступ до різних версій бібліотек в залежності від проекту.
Створення віртуального оточення дозволяє ізолювати залежності проекту від системних залежностей та інших проектів, що працюють на тій самій машині. Це допомагає уникнути конфліктів залежностей, що може призвести до помилок та збоїв. Створити віртуальне оточення Python можна за допомогою модуля venv
, який постачається у стандартній бібліотеці Python.
Створити віртуальне оточення у поточній директорії
python3 -m venv myenv # myenv – the name of the virtual environment
source myenv/bin/activate # activate virtual environment for Unix system
Після активації віртуального оточення можна інсталювати та використовувати пакети Python без впливу на глобальне оточення комп'ютера.
pyenv⚑
pyenv — це зручний інструмент для керування різними версіями інтерпретатора Python. Він дозволяє легко встановлювати, перемикатися та налаштовувати різні версії Python для різних проектів, що допомагає уникнути конфліктів між залежностями та версіями.
Основні можливості pyenv
- Встановлення різних версій Python - Перемикання між версіями - Локальні версії - Глобальні версії
tox⚑
tox — це інструмент для автоматизації та стандартизації тестування Python-програм. Він допомагає встановлювати та управляти віртуальними середовищами, виконувати тести в різних версіях Python та забезпечувати їхню сумісність.
tox
часто використовується разом з pytest
для автоматизації тестування. Цей інструмент корисний для розробників, які хочуть забезпечити, що їхні програми працюють коректно в різних версіях Python.