Skip to content

Commit

Permalink
Use __del__ in Python to clean up Rust objects
Browse files Browse the repository at this point in the history
`__del__` more closely maps to what we want than context managers. For
instance, with the current code's use of context management, the only way to
make a collection of `ZipCodeDatabase`s would be to explicitly nest
`with ... as` statements. As well, a context manager would complicate
persisting a Rust object that is expensive to initialize but cheap to use,
whereas `__del__` supports this while still freeing the resources when the
reference count drops to zero.

There are two drawbacks to using `__del__` over context management:
1. Memory cleanup in the presence of reference cycles in Python is less
   predictable.
  * This is already an issue in Python that developers will need to be aware
    of. The fact that some of the objects involved will be cleaned up by
    calling a Rust function doesn't exacerbate that problem.
2. The Rust cleanup function might not run immediately upon the variable going
   out of scope
  * Since the cleanup function is to avoid a memory leak, I don't see how this
    is any different than any other allocated object in Python. Additionally,
    we know in the Rust world to not rely on `drop` being called to guarantee
    properties like safety, so I think `__del__` maps better to the concept of
    the `drop` function it will be calling.

Aside from `__del__` reducing the restrictions on using Rust objects within
Python, it's also conceptually simpler. And to an audience that's already
learning Rust, it should be easier to teach in conjunction with teaching
`Drop`.

Fixes #60
  • Loading branch information
scooter-dangle committed Feb 15, 2018
1 parent 00e0bb6 commit c94fe45
Show file tree
Hide file tree
Showing 2 changed files with 11 additions and 14 deletions.
15 changes: 6 additions & 9 deletions examples/objects/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,7 @@ class ZipCodeDatabase:
def __init__(self):
self.obj = lib.zip_code_database_new()

def __enter__(self):
return self

def __exit__(self, exc_type, exc_value, traceback):
def __del__(self):
lib.zip_code_database_free(self.obj)

def populate(self):
Expand All @@ -35,8 +32,8 @@ def populate(self):
def population_of(self, zip):
return lib.zip_code_database_population_of(self.obj, zip.encode('utf-8'))

with ZipCodeDatabase() as database:
database.populate()
pop1 = database.population_of("90210")
pop2 = database.population_of("20500")
print(pop1 - pop2)
database = ZipCodeDatabase()
database.populate()
pop1 = database.population_of("90210")
pop2 = database.population_of("20500")
print(pop1 - pop2)
10 changes: 5 additions & 5 deletions site/objects/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,11 @@ We create an empty structure to represent our type. This will only be
used in conjunction with the `POINTER` method, which creates a new
type as a pointer to an existing one.

To ensure that memory is properly cleaned up, we use a *context
manager*. This is tied to our class through the `__enter__` and
`__exit__` methods. We use the `with` statement to start a new
context. When the context is over, the `__exit__` method will be
automatically called, preventing the memory leak.
To ensure that memory is properly cleaned up, we define a `__del__`
method on our class for the Python garbage collector to call when it
detects that there are no longer any references to our instance of the
`ZipCodeDatabase` class, which prevents a memory leak.


## Haskell

Expand Down

0 comments on commit c94fe45

Please sign in to comment.