Подтвердить что ты не робот

Возможно ли иметь реальную утечку памяти в Python из-за вашего кода?

У меня нет примера кода, но мне любопытно, можно ли писать код Python, что приводит к по существу утечке памяти.

4b9b3361

Ответ 1

Это возможно, да.

Это зависит от того, о какой утечке памяти вы говорите. В чистом коде Python невозможно "забыть освободить" память, такую как в C, но можно оставить ссылку где-нибудь висящей. Некоторые примеры таких:

необработанный объект трассировки, который поддерживает весь кадр стека, даже если функция больше не работает

while game.running():
    try:
        key_press = handle_input()
    except SomeException:
        etype, evalue, tb = sys.exc_info()
        # Do something with tb like inspecting or printing the traceback

В этом глупом примере игрового цикла, возможно, мы присвоили 'tb' локальному. У нас были благие намерения, но этот tb содержит информацию о фрейме стека всего, что происходило в нашем handle_input вплоть до того, что это вызывало. Предполагая, что ваша игра продолжается, этот 'tb' сохраняется даже при следующем вызове handle_input и, возможно, навсегда. Документы для exc_info теперь говорят об этой потенциальной проблеме циклической ссылки и рекомендуют просто не назначать tb если она вам абсолютно не нужна. Если вам нужно получить трассировку, рассмотрите, например, traceback.format_exc

хранение значений в классе или глобальной области видимости вместо области видимости экземпляра и не реализация этого.

Это может происходить коварным образом, но часто происходит, когда вы определяете изменчивые типы в области видимости вашего класса.

class Money(object):
    name = ''
    symbols = []   # This is the dangerous line here

    def set_name(self, name):
        self.name = name

    def add_symbol(self, symbol):
        self.symbols.append(symbol)

В приведенном выше примере, скажем, вы сделали

m = Money()
m.set_name('Dollar')
m.add_symbol('$')

Вы, вероятно, быстро найдете эту конкретную ошибку, но в этом случае вы помещаете изменяемое значение в область видимости класса и, даже если вы правильно __dict__ к нему в области видимости экземпляра, оно фактически "проваливается" на объект класса __dict__.

Это используется в определенных контекстах, таких как хранение объектов, потенциально может привести к тому, что куча вашего приложения будет расти вечно, и может вызвать проблемы, скажем, в производственном веб-приложении, которое иногда не перезапускает свои процессы.

Циклические ссылки в классах, которые также имеют метод __del__.

По иронии судьбы, существование __del__ делает невозможным циклический сборщик мусора для очистки экземпляра. Скажем, у вас было что-то, что вы хотели сделать деструктором для целей финализации:

class ClientConnection(...):
    def __del__(self):
        if self.socket is not None:
            self.socket.close()
            self.socket = None

Теперь это прекрасно работает само по себе, и вы можете быть уверены, что он является хорошим управляющим ресурсами ОС для обеспечения того, чтобы сокет был "утилизирован".

Однако, если ClientConnection сохранил ссылку, в которой говорилось, что User и Пользователь сохранили ссылку на соединение, вы можете испытать искушение сказать, что при очистке пусть пользователь отменяет ссылку на соединение. Однако на самом деле это недостаток: циклический сборщик мусора не знает правильный порядок операций и не может его очистить.

Решение этой проблемы состоит в том, чтобы убедиться, что вы выполняете очистку, скажем, отключаете события, вызывая какое-то закрытие, но называете этот метод чем-то иным, чем __del__.

плохо реализованные расширения C или неправильно используемые библиотеки C, как они должны быть.

В Python вы доверяете сборщику мусора выбрасывать вещи, которые вы не используете. Но если вы используете расширение C, которое упаковывает библиотеку C, большую часть времени вы несете ответственность за обеспечение явного закрытия или отмены выделения ресурсов. В основном это задокументировано, но программист на python, который привык не делать этого явного перераспределения, может отбросить дескриптор (например, возвращение из функции или чего-то еще) этой библиотеке, не зная, что ресурсы удерживаются.

Области, которые содержат замыкания, которые содержат намного больше, чем вы могли ожидать

class User:
    def set_profile(self, profile):
        def on_completed(result):
            if result.success:
                self.profile = profile

        self._db.execute(
            change={'profile': profile},
            on_complete=on_completed
        )

В этом надуманном примере мы, похоже, используем какой-то асинхронный вызов, который перезвонит нам на on_completed когда вызов БД будет выполнен (реализация могла быть обещанной, она заканчивается тем же результатом).

Чего вы не можете понять, так это того, что замыкание on_completed связывает ссылку на self, чтобы выполнить присваивание self.profile. Теперь, возможно, клиент БД следит за активными запросами и указателями на замыкания, чтобы вызывать их после завершения (поскольку он асинхронный) и сообщать о сбое по любой причине. Если клиент БД неправильно очищает обратные вызовы и т.д., В этом случае у клиента БД теперь есть ссылка на on_completed, в которой есть ссылка на пользователя, которая хранит _db - теперь вы создали циклическую ссылку, которая может никогда не быть собрана.

(Даже без циклической ссылки тот факт, что замыкания связывают локальные и даже экземпляры, иногда может привести к тому, что значения, которые, как вы думали, были собраны, живут долго, что может включать в себя сокеты, клиенты, большие буферы и целые деревья вещей)

Параметры по умолчанию, которые являются изменяемыми типами

def foo(a=[]):
    a.append(time.time())
    return a

Это надуманный пример, но один может быть заставили поверить, что значение по умолчанию быть пустой список означает, что добавление к нему, когда он на самом деле ссылка на тот же список. a Это снова может вызвать неограниченный рост, не зная, что вы это сделали.

Ответ 2

Классическое определение утечки памяти - это память, которая была использована один раз, а теперь нет, но не была исправлена. Это почти невозможно с чистым кодом Python. Но, как указывает Антуан, вы можете без труда избавиться от всей памяти, позволяя структурам данных расти без ограничений, даже если вам не нужно хранить все данные.

С расширениями C, конечно, вы вернулись на неуправляемую территорию, и все возможно.

Ответ 3

Конечно, вы можете. Типичным примером утечки памяти является создание кеша, который вы никогда не зачищаете вручную, и у которого нет политики автоматического выселения.

Ответ 4

В смысле сирота выделенных объектов после выхода из сферы действия, потому что вы забыли освободить их, нет; Python автоматически освободит объекты области видимости (Garbage Collection). Но в том смысле, о котором говорит @Antione, да.

Ответ 5

Поскольку многие модули написаны на C, да, возможны утечки памяти. Представьте, что вы используете контекст рисования с графическим интерфейсом (например, с помощью wxpython), вы можете создавать буферы памяти, но если вы забыли его освободить. у вас будут утечки памяти... в этом случае функции C++ wx api обернуты в python.

еще большее неправильное использование, представьте, что вы перегружаете эти методы wx виджетов внутри python...

Ответ 6

Я создаю объект с тяжелым атрибутом, чтобы похвастаться использованием памяти процесса.

Затем я создаю словарь, который ссылается на себя много раз.

Затем я удаляю объект и прошу GC собрать мусор. Он не собирает ничего.

Затем я проверяю процесс, занимающий оперативную память - он такой же.

Вот, пожалуйста, утечка памяти!

α python
Python 2.7.15 (default, Oct  2 2018, 11:47:18)
[GCC 4.2.1 Compatible Apple LLVM 10.0.0 (clang-1000.11.45.2)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import gc
>>> class B(object):
...     b = list(range(1 * 10 ** 8))
...
>>>
[1]+  Stopped                 python
~/Sources/plan9port [git branch:master]
α ps aux | grep python
alexander.pugachev 85164   0.0 19.0  7562952 3188184 s010  T     2:08pm   0:03.78 /usr/local/Cellar/[email protected]/2.7.15_1/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python
~/Sources/plan9port [git branch:master]
α fg
python

>>> b = B()
>>> for i in range(1000):
...     b.a = {'b': b}
...
>>>
[1]+  Stopped                 python
~/Sources/plan9port [git branch:master]
α ps aux | grep python
alexander.pugachev 85164   0.0 19.0  7579336 3188264 s010  T     2:08pm   0:03.79 /usr/local/Cellar/[email protected]/2.7.15_1/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python
~/Sources/plan9port [git branch:master]
α fg
python


>>> b.a['b'].a
{'b': <__main__.B object at 0x109204950>}
>>> del(b)
>>> gc.collect()
0
>>>
[1]+  Stopped                 python
~/Sources/plan9port [git branch:master]
α ps aux | grep python
alexander.pugachev 85164   0.0 19.0  7579336 3188268 s010  T     2:08pm   0:05.13 /usr/local/Cellar/[email protected]/2.7.15_1/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python

Ответ 7

Пример:

Сделайте функцию foo которая вызывает функцию bar, которая вызывает функцию foo и так далее...

Как это:

>>> def foo():
    bar()


>>> def bar():
    foo()


>>> foo()

Концепция заключается в том, что он продолжает работать до максимальной глубины рекурсии, которая (для меня):

>>> import sys
>>> sys.getrecursionlimit()
1000
>>> 

И если вы установите его, скажем, 10000, он будет продолжаться и продолжаться, пока не достигнет десяти тысячного, очень долгого времени, вы можете изменить его, выполнив:

>>> import sys
>>> sys.setrecursionlimit(10000)
>>> # test to see if it did change.
>>> sys.getrecursionlimit()
10000
>>> 

Итак, теперь вы запустите первый пример, это будет очень долго...