2013-03-15 2 views
7

Как известно, метод python __del__ не должен использоваться для очистки важных вещей, так как не гарантируется, что этот метод вызывается. Альтернативой является использование диспетчера контекста, как описано в нескольких потоках.Как использовать диспетчер контекста, чтобы избежать использования __del__ в python?

Но я не совсем понимаю, как переписать класс для использования диспетчера контекстов. Чтобы разработать, у меня есть простой (нерабочий) пример, в котором класс-оболочка открывает и закрывает устройство, и который должен закрыть устройство, в любом случае экземпляр класса выходит из своей области (исключение и т. Д.).

Первый файл mydevice.py представляет собой стандартный класс обертку, чтобы открыть и закрыть устройство:

class MyWrapper(object): 
    def __init__(self, device): 
     self.device = device 

    def open(self): 
     self.device.open() 

    def close(self): 
     self.device.close() 

    def __del__(self): 
     self.close() 

этот класс используется другим классом myclass.py:

import mydevice 


class MyClass(object): 

    def __init__(self, device): 

     # calls open in mydevice 
     self.mydevice = mydevice.MyWrapper(device) 
     self.mydevice.open() 

    def processing(self, value): 
     if not value: 
      self.mydevice.close() 
     else: 
      something_else() 

Мой вопрос: Когда я реализовать контекстный менеджер в mydevice.py с методами __enter__ и __exit__, как этот класс может обрабатываться в myclass.py? Мне нужно сделать что-то вроде

def __init__(self, device): 
    with mydevice.MyWrapper(device): 
     ??? 

но как с этим обращаться? Может, я забыл что-то важное? Или я могу использовать диспетчер контекста только внутри функции, а не как переменную внутри области класса?

ответ

4

Проблема заключается не в том, что вы используете его в классе, а в том, что вы хотите оставить устройство «открытым» способом: вы его открываете, а затем просто оставляете его открытым. Диспетчер контекста предоставляет возможность открыть некоторый ресурс и использовать его в относительно коротком, ограниченном виде, убедившись, что он закрыт в конце. Ваш существующий код уже небезопасен, потому что, если происходит некоторая авария, вы не можете гарантировать, что ваш __del__ будет вызываться, поэтому устройство может быть оставлено открытым.

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

def processing(self, value): 
    with self.device: 
     if value: 
      something_else() 

Если self.device является надлежащим написанный менеджер контекста, он должен открыть устройство __enter__ и закрыть его в __exit__. Это гарантирует, что устройство будет закрыто в конце блока with.

Конечно, для некоторых видов ресурсов это невозможно сделать (например, поскольку открытие и закрытие устройства теряет важное состояние или работает медленнее). Если это ваш случай, вы застряли с использованием __del__ и живете с его подводными камнями. Основная проблема заключается в том, что нет надежного способа оставить устройство «открытым», но при этом гарантировать, что он будет закрыт даже в случае некоторого необычного сбоя программы.

+0

Тот же комментарий, что и раньше: я не знаю заранее, что происходит с устройством, но он будет открыт и использован в нескольких других методах, классах и т. Д. Является ли это тем, что я должен использовать '__del__' вместо этого? – Alex

+0

Я думаю, что '__del __()' является вашим единственным открытием для этого открытого поведения. Имейте в виду, что '__del __()' только не вызывается, если сборщик мусора находит ссылочный цикл с вашим классом. Вы можете даже обнаружить и разрешить это вручную в своем коде, периодически проверяя ['gc.garbage'] (http://docs.python.org/2/library/gc.html#gc.garbage) и нарушая эталонные циклы, которые вызывают проблему. – Cartroo

0

Я не совсем уверен, что вы просите. Экземпляр менеджера контекста может быть членом класса - вы можете повторно использовать его в количестве with статей, как вам нравится, и каждый раз будут вызываться методы __enter__() и __exit__().

Итак, как только вы добавили эти методы в MyWrapper, вы можете построить его в MyClass так же, как вы выше.И тогда вы могли бы сделать что-то вроде:

def my_method(self): 
    with self.mydevice: 
     # Do stuff here 

Это будет вызывать __enter__() и __exit__() методы экземпляра, созданного в конструкторе.

Однако предложение with может охватывать только функцию - если использовать выражение with в конструкторе, то он будет вызывать __exit__() перед выходом из конструктора. Если вы хотите это сделать, единственный способ - использовать __del__(), у которого есть свои проблемы, как вы уже упоминали. Вы можете открыть и закрыть устройство только тогда, когда вам это нужно, используя with, но я не знаю, соответствует ли это вашим требованиям.

+0

Ваше решение также работает только в пределах одной функции! Что делать, если я хочу что-то открыть в методе 'my_method()', сделать что-то еще в другой функции? Ваше предложение не будет работать. – Alex

+0

Как я уже указывал в конце своего ответа, предложение 'with' будет работать только в пределах функции, в которой оно используется, - так оно и было определено. Вы можете использовать 'with' в определении класса, но оно будет применяться только во время обработки определения. Если вы хотите получить такой же эффект на протяжении всего жизненного цикла класса, вам нужно будет использовать '__del __()', даже если он страдает от проблем. Лучшим решением, вероятно, является только открытие и закрытие устройства в каждой функции. Извините, если я не сделал это достаточно ясно. – Cartroo

14

Я предлагаю использовать класс contextlib.contextmanager вместо написания класса, который реализует __enter__ и __exit__. Вот как это будет работать:

class MyWrapper(object): 
    def __init__(self, device): 
     self.device = device 

    def open(self): 
     self.device.open() 

    def close(self): 
     self.device.close() 

    # I assume your device has a blink command 
    def blink(self): 
     # do something useful with self.device 
     self.device.send_command(CMD_BLINK, 100) 

    # there is no __del__ method, as long as you conscientiously use the wrapper 

import contextlib 

@contextlib.contextmanager 
def open_device(device): 
    wrapper_object = MyWrapper(device) 
    wrapper_object.open() 
    try: 
     yield wrapper_object 
    finally: 
     wrapper_object.close() 
    return 

with open_device(device) as wrapper_object: 
    # do something useful with wrapper_object 
    wrapper_object.blink() 

линия, которая начинается с символа называется декоратора. Он изменяет объявление функции на следующей строке.

Когда встречается инструкция with, функция open_device() будет выполнять до yield. Значение в операторе yield возвращается в переменной, являющейся целью необязательного предложения as, в данном случае wrapper_object. Вы можете использовать это значение как обычный объект Python после этого. Когда управление выходит из блока на любой путь –, включая исключение для исключения –, будет выполняться оставшаяся часть функции open_device.

Я не уверен, что (а) ваш класс-оболочка добавляет функциональность к API нижнего уровня или (б), если это только то, что вы включаете, чтобы иметь контекстный менеджер. Если (b), то вы, вероятно, можете отказаться от него полностью, поскольку contextlib заботится об этом для вас. Вот что ваш код может выглядеть тогда:

import contextlib 

@contextlib.contextmanager 
def open_device(device): 
    device.open() 
    try: 
     yield device 
    finally: 
     device.close() 
    return 

with open_device(device) as device: 
    # do something useful with device 
    device.send_command(CMD_BLINK, 100) 

99% из контекста менеджера использования может быть сделано с contextlib.contextmanager. Это чрезвычайно полезный класс API (и способ, которым он реализован, также является творческим использованием низкоуровневой сантехники Python, если вы заботитесь о таких вещах).

+0

Требуется ли возврат в 'def open_device'? –