Як правильно очистити об’єкт Python?


462
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 працює чудово.
Невідомо

7
Якщо хтось хоче знати: Ця стаття пояснює, чому __del__його не слід використовувати як аналог __init__. (Тобто, це не "деструктор" в тому сенсі, що __init__це конструктор.
franklin,

Відповіді:


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__метод буде автоматично викликаний, незалежно від того, відбудеться виняток чи ні.

Можна навіть зробити такий підхід на крок далі. У наведеному вище прикладі хтось все-таки може створити пакет, використовуючи його конструктор, не використовуючи 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 () .__ ввести __ () явно і створити пакет, який ніколи не буде доопрацьований ... але вони справді повинні намагатись зламати код. Напевно, не про що турбуватися.
David Z

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 ви можете надати ресурсу метод __init__, який зберігає * args та ** kwargs у self, а потім передає їх внутрішнього класу методу 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 виходить, він не працюватиме. У моєму випадку мені байдуже, чи це коли об'єкт виходить за межі сфери дії, чи не так, поки пітон не вийде. :)
hlongmore

Чи можу я ввести та вийти, а також додати atexit.register(self.__exit__)?
myradio

@myradio Я не бачу, як це було б корисно? Ви не можете виконати всю логіку очищення всередині __exit__і не використовувати контекстного менеджера? Крім того, __exit__приймає додаткові аргументи (тобто __exit__(self, type, value, traceback)), тому вам потрібно буде використати їх. Так чи інакше, це здається, що ви повинні поставити окреме запитання щодо ТА, адже ваш випадок використання виглядає незвично?
ostrokach

33

Як додаток до відповіді Клінта , ви можете спростити, PackageResourceвикористовуючи contextlib.contextmanager:

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

Як варіант, хоча це, мабуть, не так пітонічно, ви можете перекрити 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також повинен це робити (хоча я ще цього не перевіряв), тому ніяких метакласів не потрібно. Хоча я вже знайшов деякі досить цікаві способи використання метаклассом в минулому ...
Тобіас Kienzler

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

17

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

Однак, як зазначали інші, слід уникати використання __del__. Основна причина цього полягає в тому, що екземпляри з __del__не збиратимуть сміття (вони будуть звільнені лише тоді, коли їхня знижка досягне 0). Тому, якщо ваші екземпляри залучаються до кругових посилань, вони будуть жити в пам'яті стільки, скільки запускається програма. (Я, можливо, помиляюсь у всьому цьому, хоч би мені довелося ще раз прочитати документи gc, але я впевнений, що він працює так).


5
Об'єкти з __del__можна зібрати сміття, якщо їх кількість посилань від інших об'єктів __del__дорівнює нулю, і вони недоступні. Це означає, що якщо у вас є еталонний цикл між об'єктами __del__, жоден із них не збирається. Однак будь-який інший випадок має бути вирішений, як очікувалося.
Колін

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

14

Кращою альтернативою є використання slabref.finalize . Дивіться приклади в " Об'єкти фіналізатора" та порівняння фіналізаторів з методами __del __ () .


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

3
ІМО, це справді найкраща відповідь. Він поєднує в собі можливість прибирання при збиранні сміття з можливістю прибирання на виході. Застереження полягає в тому, що python 2.7 не має слабкої переробки.
hlongmore

12

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

__del__буде називатися навіть тоді, коли __init__вона не була виконана належним чином або викинула виняток.

Джерело


2
Звучить дуже вірогідно. Найкращий спосіб уникнути цієї проблеми при використанні __del__- це чітко оголосити всіх членів на рівні класу, переконавшись, що вони завжди існують, навіть якщо вони __init__не вдається. У наведеному прикладі files = ()буде працювати, хоча в основному ви просто призначите None; в будь-якому випадку вам все одно потрібно призначити реальне значення в __init__.
Søren Løvborg

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

Просто оберніть свій деструктор за допомогою оператора спробу / за винятком, і він не викине виключення, якщо ваші глобальні компанії вже утилізовані.

Редагувати

Спробуйте це:

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
Проблема полягає в тому, що якщо дані про члени зникли, для мене занадто пізно. Мені потрібні ці дані. Дивіться мій код вище: мені потрібні імена файлів, щоб знати, які файли потрібно видалити. Я спростив свій код, хоча є й інші дані, які мені потрібно очистити (тобто перекладач не знатиме, як очистити).
wilhelmtell

4

Здається, що ідіоматичний спосіб зробити це - надати close()метод (або подібний) і виразно назвати його.


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