If I am going to implement a safe resource wrapper in Python, do I need to implement the Dispose Pattern like C#?
Here is a demo implementation of what I mean:
class ResourceWrapper:
def __init__(self):
self._python_resource = ... # A Python object that manages some resources.
self._external_resource = _allocate_resource() # A resource handle to an external resource.
self._is_closed = False # Whether the object has been closed.
def __del__(self):
self._close(manual_close=False) # Called by GC.
def close(self):
self._close(manual_close=True) # Called by user to free resource early.
def _close(self, manual_close):
if not self._is_closed: # Don’t want a resource to be closed more than once.
if manual_close:
# Since `_close` is called by user, we can guarantee that `self._python_resource` is still valid, so we
# can close it safely.
self._python_resource.close()
else:
# This means `_close` is called by GC, `self._python_resource` might be already GCed, but we don’t know
# for sure, so we do nothing and rely on GC to free `self._python_resource`.
pass
# GC will not take care of freeing unmanaged resource, so whether manual close or not, we have to close the
# resource to prevent leaking.
_free_resource(self._external_resource)
# Now we mark the object as closed to prevent closing multiple times.
self._is_closed = True
self._python_resource
is a resource wrapper object managed by Python GC, and self._external_resource
is a handle to an external resource that is not managed by Python GC.
I want to ensure both managed and unmanaged resource gets freed if user manual closes the wrapper, and, they also gets freed if the wrapper object gets GCed.
No, in Python you should use Context Managers:
class ResourceWrapper:
def __init__(self):
...
...
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
self._close(manual_close=False)
with ResourceWrapper() as wrapper:
# do something with wrapper
Note 1: There's this comment in _close()
method:
This means
_close
is called by GC,self._python_resource
might be already GCed, but we don’t knowfor sure, so we do nothing and rely on GC to freeself._python_resource
.
I'm not sure what you mean by that, but as long as you hold reference to an object (and as long as it isn't a weak reference) it won't be GC'ed.
Note 2: What happens if an object that is a context manager is used without with
block? Then resource will be released when object is garbage collected - but I wouldn't worry about that. Using context managers is common idiom in python (see any example with open()
ing file). If that's crucial for your application, you can acquire resources in __enter__()
, that way won't be acquired unless in with
block.
Note 3, about cyclic references: If you have two objects that hold reference to each other, you've formed cyclic reference, so that two object won't be freed by "regular" reference-counting GC. Instead, they are to be collected by generational GC, unless thay happen to have __del__
method. __del__
inhibits GC from collecting objects. See gc.garbage
:
A list of objects which the collector found to be unreachable but could not be freed (uncollectable objects). By default, this list contains only objects with
__del__() methods
. [1] Objects that have__del__()
methods and are part of a reference cycle cause the entire reference cycle to be uncollectable, including objects not necessarily in the cycle but reachable only from it.
Python 3.4 introduced PEP-442, which introduces safe object finalization. Either way, you won't have invalid references. If you have attribute (hasattr(self, "_python_resource")
) it will be valid.
Takeaway: don't use __del__
.