Source From Here
Preface
Discovering the quirks of Python's context manager
Python’s context managers are great for resource management and stopping the propagation of leaked abstractions. You’ve probably used it while opening a file or a database connection. Usually it starts with a with statement like this:
In the above case, file.txt gets automatically closed when the execution flow goes out of the scope. This is equivalent to writing:
Writing Custom Context Managers
To write a custom context manager, you need to create a class that includes the __enter__ and __exit__ methods. Let’s recreate a custom context manager that will execute the same workflow as above:
You can use the above class just like a regular context manager:
From Generators to Context Managers
Creating context managers by writing a class with __enter__ and __exit__ methods, is not difficult. However, you can achieve better brevity by defining them using contextlib.contextmanager decorator. This decorator converts a generator function into a context manager. The blueprint for creating context manager decorators goes something like this:
When you use the context manager with the with statement:
It roughly translates to:
The setup code goes before the try..finally block. Notice the point where the generator yields. This is where the code block nested in the with statement gets executed. After the completion of the code block, the generator is then resumed. If an unhandled exception occurs in the block, it’s re-raised inside the generator at the point where the yield occurred and then the finally block is executed. If no unhandled exception occurs, the code gracefully proceeds to the finally block where you run your cleanup code.
Let’s implement the same CustomFileOpen context manager with contextmanager decorator:
Writing Context Managers as Decorators
You can use context managers as decorators also. To do so, while defining the class, you have to inherit from contextlib.ContextDecorator class. Let’s make a RunTime decorator that will be applied on a file-opening function. The decorator will:
You can use the decorator like this:
Using the function like this should return:
You can also create the same decorator via contextlib.contextmanager decorator.
Nesting Contexts
You can nest multiple context managers to manage resources simultaneously. Consider the following dummy manager:
Output:
Notice the order they’re closed. Context managers are treated as a stack, and should be exited in reverse order in which they’re entered. If an exception occurs, this order matters, as any context manager could suppress the exception, at which point the remaining managers will not even get notified of this. The __exit__ method is also permitted to raise a different exception, and other context managers then should be able to handle that new exception.
Combining Multiple Context Managers
You can combine multiple context managers too. Let’s consider these two managers:
Now combine these two using the decorator syntax. The following function takes the above define managers a and b and returns a combined context manager ab.
This can be used as:
Output:
If you have variable numbers of context managers and you want to combine them gracefully, contextlib.ExitStack is here to help. Let’s rewrite context manager ab using ExitStack. This function takes the individual context managers and their arguments as tuples and returns the combined manager:
Then you can use it this way:
Output:
ExitStack can be also used in cases where you want to manage multiple resources gracefully. For example, suppose, you need to create a list from the contents of multiple files in a directory. Let’s see, how you can do so while avoiding accidental memory leakage with robust resource management:
Using Context Managers to Create SQLAlchemy Session
If you are familiar with SQLALchemy, Python’s SQL toolkit and Object Relational Mapper, then you probably know the usage of Session to run a query. A Session basically turns any query into a transaction and makes it atomic. Context managers can help you write a transaction session in a very elegant way. A basic querying workflow in SQLAlchemy may look like this:
The excerpt above creates an in memory SQLite connection and a session_scope function with context manager. The session_scope function takes care of committing and rolling back in case of exception automatically. The session_scope function can be used to run queries in the following way:
Discovering the quirks of Python's context manager
Python’s context managers are great for resource management and stopping the propagation of leaked abstractions. You’ve probably used it while opening a file or a database connection. Usually it starts with a with statement like this:
- with open("file.txt", "wt") as f:
- f.write("contents go here")
- try:
- f = open("file.txt", "wt")
- text = f.write("contents go here")
- finally:
- f.close()
To write a custom context manager, you need to create a class that includes the __enter__ and __exit__ methods. Let’s recreate a custom context manager that will execute the same workflow as above:
- class CustomFileOpen:
- """Custom context manager for opening files."""
- def __init__(self, filename, mode):
- self.filename = filename
- self.mode = mode
- def __enter__(self):
- self.f = open(self.filename, self.mode)
- return self.f
- def __exit__(self, *args):
- self.f.close()
- with CustomFileOpen("file.txt", "wt") as f:
- f.write("contents go here")
Creating context managers by writing a class with __enter__ and __exit__ methods, is not difficult. However, you can achieve better brevity by defining them using contextlib.contextmanager decorator. This decorator converts a generator function into a context manager. The blueprint for creating context manager decorators goes something like this:
- @contextmanager
- def some_generator(<arguments>):
- <setup>
- try:
- yield <value>
- finally:
- <cleanup>
- with some_generator(<arguments>) as <variable>:
- <body>
- <setup>
- try:
- <variable> = <value>
- <body>
- finally:
- <cleanup>
Let’s implement the same CustomFileOpen context manager with contextmanager decorator:
- from contextlib import contextmanager
- @contextmanager
- def CustomFileOpen(filename, method):
- """Custom context manager for opening a file."""
- f = open(filename, method)
- try:
- yield f
- finally:
- f.close()
You can use context managers as decorators also. To do so, while defining the class, you have to inherit from contextlib.ContextDecorator class. Let’s make a RunTime decorator that will be applied on a file-opening function. The decorator will:
- from contextlib import ContextDecorator
- from time import time
- class RunTime(ContextDecorator):
- """Timing decorator."""
- def __init__(self, description):
- self.description = description
- def __enter__(self):
- print(self.description)
- self.start_time = time()
- def __exit__(self, *args):
- self.end_time = time()
- run_time = self.end_time - self.start_time
- print(f"The function took {run_time} seconds to run.")
- @RunTime("This function opens a file")
- def custom_file_write(filename, mode, content):
- with open(filename, mode) as f:
- f.write(content)
You can also create the same decorator via contextlib.contextmanager decorator.
- from contextlib import contextmanager
- @contextmanager
- def runtime(description):
- print(description)
- start_time = time()
- try:
- yield
- finally:
- end_time = time()
- run_time = end_time - start_time
- print(f"The function took {run_time} seconds to run.")
You can nest multiple context managers to manage resources simultaneously. Consider the following dummy manager:
- from contextlib import contextmanager
- @contextmanager
- def get_state(name):
- print("entering:", name)
- yield name
- print("exiting :", name)
- # multiple get_state can be nested like this
- with get_state("A") as A, get_state("B") as B, get_state("C") as C:
- print("inside with statement:", A, B, C)
Notice the order they’re closed. Context managers are treated as a stack, and should be exited in reverse order in which they’re entered. If an exception occurs, this order matters, as any context manager could suppress the exception, at which point the remaining managers will not even get notified of this. The __exit__ method is also permitted to raise a different exception, and other context managers then should be able to handle that new exception.
Combining Multiple Context Managers
You can combine multiple context managers too. Let’s consider these two managers:
- from contextlib import contextmanager
- @contextmanager
- def a(name):
- print("entering a:", name)
- yield name
- print("exiting a:", name)
- @contextmanager
- def b(name):
- print("entering b:", name)
- yield name
- print("exiting b:", name)
- @contextmanager
- def ab(a, b):
- with a("A") as A, b("B") as B:
- yield (A, B)
- with ab(a, b) as AB:
- print("Inside the composite context manager:", AB)
If you have variable numbers of context managers and you want to combine them gracefully, contextlib.ExitStack is here to help. Let’s rewrite context manager ab using ExitStack. This function takes the individual context managers and their arguments as tuples and returns the combined manager:
- from contextlib import contextmanager, ExitStack
- @contextmanager
- def ab(cms, args):
- with ExitStack() as stack:
- yield [stack.enter_context(cm(arg)) for cm, arg in zip(cms, args)]
- with ab((a, b), ("A", "B")) as AB:
- print("Inside the composite context manager:", AB)
ExitStack can be also used in cases where you want to manage multiple resources gracefully. For example, suppose, you need to create a list from the contents of multiple files in a directory. Let’s see, how you can do so while avoiding accidental memory leakage with robust resource management:
- from contextlib import ExitStack
- from pathlib import Path
- # ExitStack ensures all files are properly closed after o/p
- with ExitStack() as stack:
- streams = (
- stack.enter_context(open(fname, "r")) for fname in Path("src").rglob("*.py")
- )
- contents = [f.read() for f in streams]
If you are familiar with SQLALchemy, Python’s SQL toolkit and Object Relational Mapper, then you probably know the usage of Session to run a query. A Session basically turns any query into a transaction and makes it atomic. Context managers can help you write a transaction session in a very elegant way. A basic querying workflow in SQLAlchemy may look like this:
- from sqlalchemy import create_engine
- from sqlalchemy.orm import sessionmaker
- from contextlib import contextmanager
- # an Engine, which the Session will use for connection resources
- some_engine = create_engine("sqlite://")
- # create a configured "Session" class
- Session = sessionmaker(bind=some_engine)
- @contextmanager
- def session_scope():
- """Provide a transactional scope around a series of operations."""
- session = Session()
- try:
- yield session
- session.commit()
- except:
- session.rollback()
- raise
- finally:
- session.close()
- with session_scope() as session:
- myobject = MyObject("foo", "bar")
- session.add(myobject)
沒有留言:
張貼留言