Python Nation🐍🇺🇦
1.93K subscribers
1 link
Канал для всіх, хто вивчає Python: поради, приклади коду, бібліотеки, лайфхаки та актуальні новин👨‍💻🐍

По всім питанням👉 @giorg_jr
Download Telegram
🔍 Інтроспекція в Python: як програмі бачити себе зсередини

Інтроспекція — це здатність програми досліджувати свою структуру під час виконання. У 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 додає 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 - не завжди

У пайтоні є оператор 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