Напишем крохотную программку. Ничего, на первый взгляд, примечательного.
from dataclasses import dataclass
@dataclass(slots=True)
class Book:
title: str
description: str
cost: float
ParseBookException = Exception("parse book failed!")
def validate_book(book: Book) -> Book:
if book.cost % 2 == 0:
return book
raise ParseBookException
def process_books() -> list[Book]:
books = []
for book in (
Book(
title="title",
cost=i,
description="lorem" * 10**6,
)
for i in range(2000)
):
try:
books.append(validate_book(book=book))
except Exception:
continue
books.append(book)
return books
def process_and_print_books():
books = process_books()
print(len(books))
def main():
process_and_print_books()
print("done")
if __name__ == "__main__":
main()
Создаём 2к объектов, отфильтровываем половину.
slots=True
, чтобы использовать меньше памяти.
Но она выжрала 9 гигабайт памяти:
pip install scalene
scalene --memory run.py
Странно. Может просто 1000 книг столько занимают. Закомментируем строки:
# if book.cost % 2 == 0:
# return book
Пусть ни одна книга не пройдёт проверку.
Эм, что?
Scanele, где течёт?
В программе!
pip install memray
memray run run.py
memray flamegraph ...
open ...
Memray, где течёт?
Сказали уже тебе, в программе!
Ещё пять модных либ для профилирования спустя вооружаемся вилкой:
import tracemalloc
...
def process_and_print_books():
books = process_books()
print(len(books))
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
print("[ Top 10 ]")
for stat in top_stats[:10]:
print(stat)
def main():
tracemalloc.start()
process_and_print_books()
...
...
╰─➤ python run.py
0
[ Top 10 ]
run.py:27: size=9537 MiB, count=2000, average=4883 KiB
run.py:17: size=406 KiB, count=4000, average=104 B
run.py:32: size=110 KiB, count=2001, average=56 B
run.py:24: size=109 KiB, count=2000, average=56 B
run.py:23: size=54.5 KiB, count=1743, average=32 B
run.py:39: size=208 B, count=1, average=208 B
done
Книг в списке нет, а аллокации есть. Причём на 10 гигов. Ну дела.
Возьмём к вилке ещё и ножик:
pip install objgraph
def main():
process_and_print_books()
print("done")
import objgraph
objgraph.show_growth()
Запускаем.
╰─➤ python run.py
0
done
traceback 4000 +4000
function 2347 +2347
frame 2002 +2002
Book 2000 +2000
wrapper_descriptor 1200 +1200
dict 1081 +1081
tuple 1020 +1020
method_descriptor 819 +819
builtin_function_or_method 813 +813
ReferenceType 778 +778
Ничего не понятно, но очень интересно. Кто эти вещи, и что они здесь делают?
Ну ладно “вещи”, но книги-то? Книги-то куда?! Тут же на них ни одной ссылки. Ведь правда..?
def main():
process_and_print_books()
print("done")
import random
import objgraph
objgraph.show_growth()
objgraph.show_chain(
objgraph.find_backref_chain(
random.choice(objgraph.by_type('Book')),
objgraph.is_proper_module,
),
filename='chain.png',
)
Эээ, что? Висит объект, на которых нет рефов?
Что ж. Слабоумие и отвага - нарисуем их всех.
for number, obj in enumerate(objgraph.by_type('Book')):
objgraph.show_chain(
objgraph.find_backref_chain(
obj,
objgraph.is_proper_module,
),
filename=f'chain{number}.png',
)
Ага! Попался, злодей.
Но трейсбек? Что это зна… о, чёрт…
ParseBookException = Exception("parse book failed!")
def validate_book(book: Book) -> Book:
if book.cost % 2 == 0:
return book
raise ParseBookException
Не мог же один жалкий глобальный эксепшн всех съесть?
def validate_book(book: Book) -> Book:
# if book.cost % 2 == 0:
# return book
raise Exception("parse book failed!")
...
def main():
process_and_print_books()
print("done")
import objgraph
objgraph.show_growth()
Или мог.
╰─➤ python run.py
0
done
function 2347 +2347
wrapper_descriptor 1200 +1200
dict 1081 +1081
tuple 1023 +1023
method_descriptor 819 +819
builtin_function_or_method 813 +813
ReferenceType 778 +778
getset_descriptor 455 +455
type 413 +413
member_descriptor 361 +361
Scalene, мог?
А если раскомментить строчки cost % 2
?
Ожидаемые 50% памяти - 5 гигов от 10.
Но почему так?
A traceback object is normally created automatically when an exception is raised and attached to it as the
__traceback__
attribute.
Так как объект глобальный, то рефы не чистятся при выходе. И память висит.
Если распечатать ещё один графов объектов, то станет нагляднее:
Следите за руками:
В общем, пишите на питоне, как будто это раст и учитесь понимать инструмент, которым вы пользуетесь. Ковырять вилкой тоже учитесь - модные библиотеки не сделают всё за вас.