🔍 Інтроспекція в Python: як програмі бачити себе зсередини
Інтроспекція — це здатність програми досліджувати свою структуру під час виконання. У Python це зручно реалізовано через вбудовані функції.
Ось декілька корисних прикладів:
Це корисно для:
🔹дебагінгу та аналізу об'єктів
🔹динамічного створення функцій чи класів
🔹розробки IDE, автозаповнення або генерації документації
Інтроспекція — це здатність програми досліджувати свою структуру під час виконання. У Python це зручно реалізовано через вбудовані функції.
Ось декілька корисних прикладів:
type(obj) — визначає тип об’єкта.dir(obj) — показує доступні атрибути та методи.getattr(obj, attr) — отримує значення атрибута.isinstance(obj, class) — перевіряє, чи об'єкт є екземпляром класу.help(obj) — виводить документацію об'єкта.Це корисно для:
🔹дебагінгу та аналізу об'єктів
🔹динамічного створення функцій чи класів
🔹розробки IDE, автозаповнення або генерації документації
Пам’ятайте, що надмірне використання інтроспекції може ускладнити код. Використовуйте її з розумом! 🐍✨
Який у вас зараз рівень як Python Developer?
Anonymous Poll
42%
Ще вчуся / пет-проекти
10%
Junior
31%
Middle
9%
Senior
7%
Team Lead / Architect
🔥2👍1
Python 3.15 додає
Якщо ти хоч раз запускав CLI-утиліту або скрипт і думав "чому воно так довго стартує" - ця фіча для тебе.
Проблема проста. Python при запуску завантажує всі модулі одразу, навіть якщо половина з них потрібна тільки в одному з десяти сценаріїв. Великий проєкт з pandas, numpy і ще купою залежностей - і ти вже чекаєш секунди просто щоб побачити --help.
Раніше це вирішували переносом імпортів всередину функцій. Працює, але код стає брудним і незрозумілим.
Тепер у Python 3.15 є нормальне рішення - ключове слово
Модуль не завантажується поки ти його не використаєш. Перший раз звернувся - тоді й завантажується.
Якщо користувач не запитав звіт - pandas взагалі не торкнувся пам'яті.
Пара моментів які варто знати:
—
—
— помилку в імпорті побачиш не одразу, а коли вперше використаєш модуль
За даними з PEP - CLI-утиліти стартують на 50-70% швидше, великі застосунки споживають на 30-40% менше пам'яті.
Python 3.15 ще не вийшов фінально, але PEP вже прийнятий. Якщо цікаво розібратись детальніше то ось першоджерело: https://peps.python.org/pep-0810
lazy імпорти і це реально корисноЯкщо ти хоч раз запускав CLI-утиліту або скрипт і думав "чому воно так довго стартує" - ця фіча для тебе.
Проблема проста. Python при запуску завантажує всі модулі одразу, навіть якщо половина з них потрібна тільки в одному з десяти сценаріїв. Великий проєкт з pandas, numpy і ще купою залежностей - і ти вже чекаєш секунди просто щоб побачити --help.
Раніше це вирішували переносом імпортів всередину функцій. Працює, але код стає брудним і незрозумілим.
Тепер у Python 3.15 є нормальне рішення - ключове слово
lazy:lazy import pandas
lazy from json import dumps
Модуль не завантажується поки ти його не використаєш. Перший раз звернувся - тоді й завантажується.
lazy import pandas
if user_wants_report:
df = pandas.DataFrame(data) # тільки тут pandas реально завантажиться
Якщо користувач не запитав звіт - pandas взагалі не торкнувся пам'яті.
Пара моментів які варто знати:
—
lazy працює тільки на рівні модуля, не всередині функцій—
from module import * не підтримується— помилку в імпорті побачиш не одразу, а коли вперше використаєш модуль
За даними з PEP - CLI-утиліти стартують на 50-70% швидше, великі застосунки споживають на 30-40% менше пам'яті.
Python 3.15 ще не вийшов фінально, але PEP вже прийнятий. Якщо цікаво розібратись детальніше то ось першоджерело: https://peps.python.org/pep-0810
🔥6
True - це насправді 1
Багато хто не знає, але
З цього виходить зручна штука. Треба порахувати, скільки елементів підходять під умову? Лічильник не потрібен:
Кожен
Але є нюанс. Раз
Ключ лишився
Коротко: сумувати булеві це нормально, а мішати їх з числами як ключі не варто.
Багато хто не знає, але
bool у пайтоні це підклас int. Тобто True дорівнює 1, а False дорівнює 0. Ними навіть можна рахувати:True + True # 2
False * 10 # 0
True == 1 # True
isinstance(True, int) # True
З цього виходить зручна штука. Треба порахувати, скільки елементів підходять під умову? Лічильник не потрібен:
orders = [120, 0, 45, 0, 300]
paid = sum(x > 0 for x in orders) # 3
Кожен
x > 0 це True або False, а sum складає їх як 1 і 0. Швидко і без зайвого коду.Але є нюанс. Раз
True == 1, то в словнику вони стають одним ключем:d = {1: "one", True: "boolean"}
print(d) # {1: 'boolean'}Ключ лишився
1, а значення перезаписалось. Такий баг знайти непросто.Коротко: сумувати булеві це нормально, а мішати їх з числами як ключі не варто.
🔥3
__slots__ - коли обʼєктів багато
За замовчуванням кожен обʼєкт у пайтоні тримає свій
Що це дає:
- менше памʼяті на кожен обʼєкт (немає
- трохи швидший доступ до атрибутів
Ціна - гнучкість. Новий атрибут вже не додаси:
І ще пара нюансів: наслідування працює, але кожен клас у ланцюгу має свої
Коротко: якщо клас створюється багато разів і набір полів фіксований -
За замовчуванням кожен обʼєкт у пайтоні тримає свій
__dict__, тобто словник з атрибутами. Це зручно бо можна додати будь-який атрибут на льоту, але словник займає памʼять. Коли обʼєктів кілька то це не є проблемою, а от коли їх багато, то вже стає помітно по пам'яті.__slots__ каже класу: атрибути тільки ці, словник не потрібен.class Receipt:
__slots__ = ("id", "total", "paid")
def __init__(self, id, total, paid):
self.id = id
self.total = total
self.paid = paid
Що це дає:
- менше памʼяті на кожен обʼєкт (немає
__dict__)- трохи швидший доступ до атрибутів
Ціна - гнучкість. Новий атрибут вже не додаси:
r = Receipt(1, 100, True)
r.discount = 10 # AttributeError
І ще пара нюансів: наслідування працює, але кожен клас у ланцюгу має свої
__slots__, інакше __dict__ повернеться. З @dataclass теж дружить - просто @dataclass(slots=True).Коротко: якщо клас створюється багато разів і набір полів фіксований -
__slots__ економить реальну памʼять. Для звичайних класів це зайве.🔥2
Чому 256 is 256 - True, а 257 is 257 - не завжди
У пайтоні є оператор
Значення однакові, а результат різний. Причина в тому, що CPython при старті створює й тримає в кеші всі малі цілі від -5 до 256. Вони використовуються постійно, тож інтерпретатор не робить нові обʼєкти щоразу - віддає ті самі.
Число 257 у кеш не потрапляє, тому кожне таке присвоєння - новий обʼєкт. Звідси
І ще нюанс: якщо написати
Висновок простий:
У пайтоні є оператор
is, який перевіряє не рівність значень, а те, чи це один і той самий обʼєкт у памʼяті. І тут ховається несподіванка:a = 256
b = 256
a is b # True
a = 257
b = 257
a is b # False
Значення однакові, а результат різний. Причина в тому, що CPython при старті створює й тримає в кеші всі малі цілі від -5 до 256. Вони використовуються постійно, тож інтерпретатор не робить нові обʼєкти щоразу - віддає ті самі.
Число 257 у кеш не потрапляє, тому кожне таке присвоєння - новий обʼєкт. Звідси
False.І ще нюанс: якщо написати
257 is 257 в одному рядку чи в одній функції, компілятор може сам згорнути константи, і вийде True. Тобто поведінка залежить від того, де саме стоїть код.Висновок простий:
is для чисел використовувати не треба взагалі. Він про ідентичність обʼєкта, а не про значення. Для порівняння чисел завжди ==.👍3🔥1
Чому None перевіряють через is, а не ==
У пайтоні прийнято писати
Обʼєкт очевидно не
Коротко: для
У пайтоні прийнято писати
if x is None, а не if x == None. І це не просто стиль.None - це єдиний обʼєкт на всю програму. Скільки б разів ти не написав None, це завжди той самий обʼєкт у памʼяті. Тому is тут найточніша й найшвидша перевірка: порівняння за ідентичністю, без зайвих викликів.== працює інакше - він викликає метод __eq__. А його можна перевизначити. І тоді == None починає працювати неочікувано:class Weird:
def __eq__(self, other):
return True
w = Weird()
w == None # True
w is None # False
Обʼєкт очевидно не
None, але == каже, що None. З is такого не станеться ніколи бо його не можна перевизначити.Коротко: для
None (як і для True/False) завжди is. Це швидше, надійніше і не залежить від того, що хтось накрутив в __eq__.👍5
finally мовчки затирає твій return
Погляньмо на функцію:
Здавалося б,
Те саме з винятками:
Помилка сталася, але про неї ніхто не дізнається. Такий баг ловиться довго, бо код виглядає цілком робочим.
Висновок:
Погляньмо на функцію:
def f():
try:
return "from try"
finally:
return "from finally"
f() # "from finally"
Здавалося б,
return "from try" мав спрацювати. Але finally виконується завжди - навіть після return. І якщо в ньому є свій return, він перекриває той, що був у try.Те саме з винятками:
return у finally проковтне навіть виняток, який мав вилетіти назовні.def g():
try:
raise ValueError("boom")
finally:
return "all good"
g() # "all good", виняток зник
Помилка сталася, але про неї ніхто не дізнається. Такий баг ловиться довго, бо код виглядає цілком робочим.
Висновок:
finally тримай тільки для очищення (закрити файл, зʼєднання, відпустити лок). Ніяких return чи raise всередині - інакше він тихо перепише всю логіку вище.👍2