The with
command is used to make it easier to write any block of code that involves features that need to be "finalized" (that is, restored, released, closed, etc.) after the block is closed - and it allows this to be done automatically, with the termination logic within the object used.
Then first thing: the with
end code always runs - it does not matter if an error occurred within the with
block or not.
But this could already be done with block finally
and a try...finally
clause - it does not matter the result of the block within try
, finally
always runs.
The great differential of with
is that the programmer who is using objects that need finalization does not have to worry about calling these methods explicitly.
In the case of a file, people generally know that it has to be called close
- but the most common, since close
already is used is that nobody ends up programming a snippet that manipulates file with call to close
, without using with
remembering to put this within finally
. That is, with
encourages the code to be written in a way that behaves correctly:
wrong way:
f = open("data.txt", "w")
# codigo usando o arquivo f
f.close()
The right way, without the with:
try:
f = open("data.txt", "w")
# codigo usando o arquivo f
finally:
f.close()
right way with (recommended):
with open("data.txt", "w") as f:
# codigo usando o arquivo f
(the file is automatically closed at the end of the block).
Then note that the "right" code is still a shorter line than the "wrong" one. But - as I mentioned above, close
of files is not the coolest thing of with
- why everyone knows and remembers close
(hopefully) - but is that for any object that needs a finalization - thread locks, database transactions, change the state of the terminal to read keys without requiring <enter>
, monkey patch of functions and methods in tests, with
executes completion transparently - without user needing know if you need to call the method close
, release
, undo
of each object (in fact, it forces the programmers of the classes of the objects that work with to implement a common interface, with methods __enter__
and __exit__
.
Furthermore, when you are programming your own classes that can be used with with
, you can treat within your function code __exit__
the most common or expected errors that could happen in the block: __exit__
receives information about the exception that occurred inside the block, and __exit__
can choose to handle the error and suppress the exception, or simply release the resource and pass the error forward: ie a lot of code that would always be the even if someone would have to write within the except
all clause when using their object can stay within __exit__
and people use their object without worrying about the code that would go inside except
.
Here is a simple example of a "bank account" that makes transactions with the balance but only allows transactions to take effect at the end of the with
block if there is a positive balance in all accounts:
class ContaBancariaTransacional:
def __init__(self, titular, saldo_inicial=0):
self.titular = titular
self.saldo = saldo_inicial
def deposito(self, valor):
self.temp += valor
def retirada(self, valor):
if self.saldo - self.temp - valor < 0:
raise ValueError("Não há saldo suficiente")
self.temp -= valor
return valor
def __enter__(self):
self.temp = 0
def __exit__(self, tipo_excecao, valor_excecao, traceback):
if tipo_excecao is None:
# Não ocorreu nenhum erro no bloco, e podemos
# atualizar o saldo definitivo
self.saldo += self.temp
del self.temp
def __repr__(self):
return f"conta de {self.titular}. Saldo: {self.saldo:.02f}"
def transferir(conta1, conta2, valor):
with conta1, conta2:
conta2.deposito(conta1.retirada(valor))
And in an interactive session we can see:
In [96]: conta1 = ContaBancariaTransacional("João", 100)
In [97]: conta2 = ContaBancariaTransacional("José", 0)
In [98]: conta1, conta2
Out[98]: (conta de João. Saldo: 100.00, conta de José. Saldo: 0.00)
In [99]: transferir(conta1, conta2, 100)
In [100]: conta1, conta2
Out[100]: (conta de João. Saldo: 0.00, conta de José. Saldo: 100.00)
In [101]: transferir(conta1, conta2, 50)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
(...)
ValueError: Não há saldo suficiente
In [102]: conta1, conta2
Out[102]: (conta de João. Saldo: 0.00, conta de José. Saldo: 100.00)
Disclaimer : This code is not complete to be a "two phase transaction" - and in fact, it only works because the account where the balance comes out is before in the with
command - when __exit__
of the first account is called, it raises the exception, which then prevents the balance increase in account2. Also, to put into production it would be nice to have some locks per thread in that class of Account as well, no matter how much toy it may be. But I think you can see how with
can work with classes defined by the programmer himself.
Now, without being for transfer, imagine that this object "account" is used by a system that is inside a loot machine (in case access to the object account would be remote, with some remote method call protocol, of course - but assuming a Python library to operate the machine's hardware - the code for withdrawal could simply be:
def saque(atm, conta, valor):
with conta:
valor = conta.retirada(valor)
atm.disponibilizar_notas(valor)
atm.abrir_gaveta()
atm.esperar(10)
atm.fechar_gaveta()
if not atm.gaveta_vazia():
raise RuntimeError("Usuario nao retirou o dinheiro")
Ready - If any of the methods involving mechanical parts of the machine even make an error, the actual balance of the account is not debited. (Note that if there is no desired value in the account, the withdrawal method itself already aborts the transaction.)