Как правильно очистить объект Python?


463
class Package:
    def __init__(self):
        self.files = []

    # ...

    def __del__(self):
        for file in self.files:
            os.unlink(file)

__del__(self)выше не удается с исключением AttributeError. Я понимаю, что Python не гарантирует существование "глобальных переменных" (данные члена в этом контексте?), Когда __del__()вызывается. Если это так, и это является причиной исключения, как я могу убедиться, что объект разрушается должным образом?


3
Чтение того, что вы связали, исчезновение глобальных переменных, кажется, здесь не применимо, если только вы не говорите о выходе из программы, во время которого, я думаю, в зависимости от того, что вы связали, может быть ВОЗМОЖНО, что сам модуль os уже удален. В противном случае, я не думаю, что это применимо к переменным-членам в методе __del __ ().
Кевин Андерсон

3
Исключение выдается задолго до выхода из моей программы. Исключением AttributeError, которое я получаю, является то, что Python говорит, что он не распознает self.files как атрибут Package. Я могу ошибаться, но если под «глобальными» они не подразумевают переменные, глобальные для методов (но, возможно, локальные для класса), то я не знаю, что вызывает это исключение. Google намекает, что Python оставляет за собой право очищать данные участников до вызова __del __ (self).
wilhelmtell

1
Код, опубликованный, похоже, работает для меня (с Python 2.5). Можете ли вы опубликовать реальный код, который дает сбой - или упрощенный (чем проще, тем лучше версия, которая все еще вызывает ошибку?
Silverfish

@ wilhelmtell не могли бы вы привести более конкретный пример? Во всех моих тестах del destructor работает отлично.
неизвестно

7
Если кто-то хочет знать: эта статья раскрывает, почему __del__не следует использовать в качестве аналога __init__. (То есть, это не «деструктор» в том смысле, что __init__это конструктор.
Франклин

Ответы:


619

Я бы рекомендовал использовать withоператор Python для управления ресурсами, которые необходимо очистить. Проблема с использованием явного close()выражения состоит в том, что вам нужно беспокоиться о людях, которые вообще не будут вызывать его или забывают поместить его в finallyблок, чтобы предотвратить утечку ресурсов при возникновении исключения.

Чтобы использовать withоператор, создайте класс с помощью следующих методов:

  def __enter__(self)
  def __exit__(self, exc_type, exc_value, traceback)

В вашем примере выше, вы бы использовали

class Package:
    def __init__(self):
        self.files = []

    def __enter__(self):
        return self

    # ...

    def __exit__(self, exc_type, exc_value, traceback):
        for file in self.files:
            os.unlink(file)

Затем, когда кто-то захочет использовать ваш класс, он сделает следующее:

with Package() as package_obj:
    # use package_obj

Переменная package_obj будет экземпляром типа Package (это значение, возвращаемое __enter__методом). Его __exit__метод будет вызываться автоматически, независимо от того, происходит ли исключение.

Вы могли бы даже сделать этот подход еще дальше. В приведенном выше примере кто-то еще может создать экземпляр Package, используя его конструктор, не используя withпредложение. Вы не хотите, чтобы это произошло. Вы можете исправить это, создав класс PackageResource , который определяет __enter__и __exit__методы. Затем класс Package будет определен строго внутри __enter__метода и возвращен. Таким образом, вызывающая сторона никогда не сможет создать экземпляр класса Package без использования withинструкции:

class PackageResource:
    def __enter__(self):
        class Package:
            ...
        self.package_obj = Package()
        return self.package_obj

    def __exit__(self, exc_type, exc_value, traceback):
        self.package_obj.cleanup()

Вы бы использовали это следующим образом:

with PackageResource() as package_obj:
    # use package_obj

35
Технически говоря, можно явно вызвать PackageResource () .__ enter __ () и таким образом создать пакет, который никогда не будет завершен ... но им действительно придется пытаться взломать код. Наверное, не о чем беспокоиться.
Дэвид З

3
Между прочим, если вы используете Python 2.5, вам нужно будет выполнить импорт with_statement в будущем, чтобы иметь возможность использовать оператор with.
Клинт Миллер

2
Я нашел статью, которая помогает показать, почему __del __ () действует так, как он делает, и придает большое значение использованию решения менеджера контекста: andy-pearce.com/blog/posts/2013/Apr/python-destructor-drawbacks
eikonomega

2
Как использовать эту красивую и чистую конструкцию, если вы хотите передать параметры? Я хотел бы быть в состоянии сделатьwith Resource(param1, param2) as r: # ...
snooze92

4
@ snooze92 вы можете дать Resource метод __init__, который хранит * args и ** kwargs в себе, а затем передает их внутреннему классу в методе enter. При использовании оператора with __init__ вызывается до __enter__
Брайан

48

Стандартный способ заключается в использовании atexit.register:

# package.py
import atexit
import os

class Package:
    def __init__(self):
        self.files = []
        atexit.register(self.cleanup)

    def cleanup(self):
        print("Running cleanup...")
        for file in self.files:
            print("Unlinking file: {}".format(file))
            # os.unlink(file)

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

Демонстрация с использованием приведенного выше кода, сохраненного как package.py :

$ python
>>> from package import *
>>> p = Package()
>>> q = Package()
>>> q.files = ['a', 'b', 'c']
>>> quit()
Running cleanup...
Unlinking file: a
Unlinking file: b
Unlinking file: c
Running cleanup...

2
Хорошая вещь в подходе atexit.register заключается в том, что вам не нужно беспокоиться о том, что делает пользователь класса (они использовали with? Они явно вызывали __enter__?) Недостатком является, конечно, если вам нужно, чтобы очистка произошла до python выходит, это не сработает. В моем случае мне все равно, когда объект выходит из области видимости или нет, пока Python не выйдет. :)
Хлонгмор

Могу ли я использовать вход и выход, а также добавить atexit.register(self.__exit__)?
Мирадио

@myradio Не понимаю, как это было бы полезно? Разве вы не можете выполнить всю логику очистки внутри __exit__и использовать контекстный менеджер? Кроме того, __exit__принимает дополнительные аргументы (т. Е. __exit__(self, type, value, traceback)), Так что вам придется учитывать их. В любом случае, похоже, что вы должны опубликовать отдельный вопрос по SO, потому что ваш вариант использования выглядит необычно?
Острокач

33

В качестве приложения к ответу Клинта , можно упростить с PackageResourceпомощью contextlib.contextmanager:

@contextlib.contextmanager
def packageResource():
    class Package:
        ...
    package = Package()
    yield package
    package.cleanup()

В качестве альтернативы, хотя, вероятно, не как Pythonic, вы можете переопределить Package.__new__:

class Package(object):
    def __new__(cls, *args, **kwargs):
        @contextlib.contextmanager
        def packageResource():
            # adapt arguments if superclass takes some!
            package = super(Package, cls).__new__(cls)
            package.__init__(*args, **kwargs)
            yield package
            package.cleanup()

    def __init__(self, *args, **kwargs):
        ...

и просто использовать with Package(...) as package.

Короче говоря, назовите свою функцию очистки closeи используйте contextlib.closing, в этом случае вы можете использовать неизмененный Packageкласс через with contextlib.closing(Package(...))или переопределить его __new__на более простой

class Package(object):
    def __new__(cls, *args, **kwargs):
        package = super(Package, cls).__new__(cls)
        package.__init__(*args, **kwargs)
        return contextlib.closing(package)

И этот конструктор наследуется, так что вы можете просто наследовать, например,

class SubPackage(Package):
    def close(self):
        pass

1
Это круто. Мне особенно нравится последний пример. Однако, к сожалению, мы не можем избежать четырехстрочного шаблона Package.__new__()метода. Или, может быть, мы можем. Возможно, мы могли бы определить декоратор класса или метакласс, обобщающий этот шаблон для нас. Пища для Питонической мысли.
Сесил Карри

@CecilCurry Спасибо, и хороший момент. Любой класс, унаследованный от, Packageтакже должен делать это (хотя я еще не проверял это), поэтому метакласс не требуется. Хотя в прошлом я нашел несколько довольно любопытных способов использования метаклассов ...
Тобиас Кинцлер

@CecilCurry На самом деле, конструктор наследуется, так что вы можете использовать Package(или, точнее, класс с именем Closing) в качестве родительского класса вместо object. Но не спрашивайте меня, как множественное наследование портится с этим ...
Тобиас Кинцлер

17

Я не думаю, что члены экземпляра могут быть удалены до __del__вызова. Я предполагаю, что причина вашего конкретного AttributeError находится где-то еще (возможно, вы ошибочно удалили self.file в другом месте).

Однако, как отметили другие, вы должны избегать использования __del__ . Основная причина этого заключается в том, что экземпляры с __del__не будут собирать мусор (они будут освобождены только тогда, когда их рефконт достигнет 0). Поэтому, если ваши экземпляры участвуют в циклических ссылках, они будут жить в памяти до тех пор, пока выполняется приложение. (Хотя я могу ошибаться во всем этом, мне придется снова прочитать документы gc, но я уверен, что это работает так).


5
Объекты с __del__могут быть собраны мусором, если их счетчик ссылок от других объектов с __del__нуля, и они недоступны. Это означает, что если у вас есть ссылочный цикл между объектами __del__, ни один из них не будет собран. Любой другой случай, однако, должен быть решен как ожидалось.
Коллин

«Начиная с Python 3.4, методы __del __ () больше не предотвращают сбор мусора в ссылочных циклах, и глобальные переменные модулей больше не вынуждены отключаться во время выключения интерпретатора. Поэтому этот код должен работать без проблем в CPython». - docs.python.org/3.6/library/…
Томаш Гандор

14

Лучшая альтернатива - это использовать weakref.finalize . См. Примеры в разделах Объекты финализатора и Сравнение финализаторов с методами __del __ () .


1
Использовал это сегодня, и он работает без нареканий, лучше, чем другие решения. У меня есть класс коммуникатора на основе многопроцессорной обработки, который открывает последовательный порт, а затем у меня есть stop()метод для закрытия портов и join()процессов. Однако, если программы выходят неожиданно stop(), не вызывается - я решил это с помощью финализатора. Но в любом случае я вызываю _finalizer.detach()метод stop, чтобы предотвратить его повторный вызов (вручную и позже снова финализатором).
Боян П.

3
ИМО, это действительно лучший ответ. Он сочетает в себе возможность уборки при вывозе мусора с возможностью уборки на выходе. Предостережение заключается в том, что Python 2.7 не имеет слабой версии.
Хлонгмор

12

Я думаю, что проблема может быть в __init__ если есть больше кода, чем показано?

__del__ будет называться даже когда __init__ он не был выполнен должным образом или выдал исключение.

Источник


2
Звучит очень вероятно. Лучший способ избежать этой проблемы при использовании __del__- это явно объявить все члены на уровне класса, гарантируя, что они всегда существуют, даже если происходит __init__сбой. В данном примере files = ()будет работать, хотя в основном вы просто назначите None; в любом случае вам все еще нужно присвоить реальное значение в __init__.
Серен Левборг

11

Вот минимальный рабочий скелет:

class SkeletonFixture:

    def __init__(self):
        pass

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass

    def method(self):
        pass


with SkeletonFixture() as fixture:
    fixture.method()

Важно: вернуть себя


Если вы похожи на меня и пропускаете return selfчасть ( правильного ответа Клинта Миллера ), вы будете пялиться на эту чушь:

Traceback (most recent call last):
  File "tests/simplestpossible.py", line 17, in <module>                                                                                                                                                          
    fixture.method()                                                                                                                                                                                              
AttributeError: 'NoneType' object has no attribute 'method'

Надеюсь, это поможет следующему человеку.


8

Просто оберните ваш деструктор оператором try / исключением, и он не выдаст исключение, если ваши глобальные переменные уже удалены.

редактировать

Попробуй это:

from weakref import proxy

class MyList(list): pass

class Package:
    def __init__(self):
        self.__del__.im_func.files = MyList([1,2,3,4])
        self.files = proxy(self.__del__.im_func.files)

    def __del__(self):
        print self.__del__.im_func.files

Он поместит список файлов в функцию del, которая гарантированно существует во время вызова. Прозрачный прокси-сервер запрещает Python или вам самим каким-либо образом удалять переменную self.files (если она будет удалена, это не повлияет на исходный список файлов). Если это не тот случай, когда он удаляется, хотя ссылок на переменную больше, вы можете удалить инкапсуляцию прокси.


2
Проблема в том, что если данные о членах уйдут, мне будет слишком поздно. Мне нужны эти данные. Смотрите мой код выше: мне нужны имена файлов, чтобы знать, какие файлы удалить. Я упростил свой код, хотя есть и другие данные, которые мне нужно очистить самому (т. Е. Переводчик не знает, как это делать).
Вильгельмтелл

4

Кажется, что идиоматический способ сделать это состоит в том, чтобы предоставить close()метод (или подобный), и вызвать его явно.


20
Это подход, который я использовал раньше, но я столкнулся с другими проблемами с ним. За исключением исключений, создаваемых другими библиотеками, мне нужна помощь Python в устранении ошибок в случае ошибки. В частности, мне нужно, чтобы Python вызывал для меня деструктор, потому что в противном случае код становится быстро неуправляемым, и я обязательно забуду точку выхода, где должен быть вызов .close ().
wilhelmtell
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.