Here are some notes that you might want use to craft your solution:
The easiest way to retrieve a couroutine’s exception (or result!) is to await
for it. asyncio.gather()
will create tasks from coroutines and wrap all of them in one encompassing task that will fail if one of the subtasks fails:
import asyncio
import random
async def coro(n):
print("Start", n)
await asyncio.sleep(random.uniform(0.2, 0.5))
if n % 4 == 0:
raise Exception('fail ({})'.format(n))
return "OK: {}".format(n)
async def main():
tasks = [coro(i) for i in range(10)]
await asyncio.gather(*tasks)
print("done")
loop = asyncio.get_event_loop()
try:
asyncio.ensure_future(main())
loop.run_forever()
finally:
loop.close()
This however does not shutdown the loop. To stop a running loop, use loop.stop()
. Use this instead:
async def main():
tasks = [coro(i) for i in range(10)]
try:
await asyncio.gather(*tasks)
except Exception as e:
loop.stop()
raise
print("done")
Stopping the loop while some long-running coroutines are running is probably not what you want. You might want to first signal some your coroutines to shut down using an event:
import asyncio
import random
async def repeat(n):
print("start", n)
while not shutting_down.is_set():
print("repeat", n)
await asyncio.sleep(random.uniform(1, 3))
print("done", n)
async def main():
print("waiting 6 seconds..")
await asyncio.sleep(6)
print("shutting down")
shutting_down.set() # not a coroutine!
print("waiting")
await asyncio.wait(long_running)
print("done")
loop.stop()
loop = asyncio.get_event_loop()
shutting_down = asyncio.Event(loop=loop)
long_running = [loop.create_task(repeat(i + 1)) for i in range(5)]
try:
asyncio.ensure_future(main())
loop.run_forever()
finally:
loop.close()
If you don’t want to await
for your tasks, you might want to use an asyncio.Event
(or asyncio.Queue
) to signal a global error handler to stop the loop:
import asyncio
async def fail():
try:
print("doing stuff...")
await asyncio.sleep(0.2)
print("doing stuff...")
await asyncio.sleep(0.2)
print("doing stuff...")
raise Exception('fail')
except Exception as e:
error_event.payload = e
error_event.set()
raise # optional
async def error_handler():
await error_event.wait()
e = error_event.payload
print("Got:", e)
raise e
loop = asyncio.get_event_loop()
error_event = asyncio.Event()
try:
loop.create_task(fail())
loop.run_until_complete(error_handler())
finally:
loop.close()
(Used here with run_until_complete()
for simplicity, but can be used with loop.stop()
as well)
I have a code that stops running each time there is an error.
Is there a way to add a code to the script which will ignore all errors and keep running the script until completion?
Below is the code:
import sys
import tldextract
def main(argv):
in_file = argv[1]
f = open(in_file,'r')
urlList = f.readlines()
f.close()
destList = []
for i in urlList:
print i
str0 = i
for ch in ['\n','\r']:
if ch in str0:
str0 = str0.replace(ch,'')
str1 = str(tldextract.extract(str0))
str2 = i.replace('\n','') + str1.replace("ExtractResult",":")+'\n'
destList.append(str2)
f = open('destFile.txt','w')
for i in destList:
f.write(i)
f.close()
print "Completed successfully:"
if __name__== "__main__":
main(sys.argv)
Many thanks
asked Jul 22, 2015 at 12:38
7
You should always ‘try’ to open files. This way you can manage exceptions, if the file does not exist for example. Take a loot at Python Tutorial Exeption Handling
import sys
try:
f = open('myfile.txt')
s = f.readline()
i = int(s.strip())
except IOError as e:
print "I/O error({0}): {1}".format(e.errno, e.strerror)
except ValueError:
print "Could not convert data to an integer."
except:
print "Unexpected error:", sys.exc_info()[0]
raise
or
for arg in sys.argv[1:]:
try:
f = open(arg, 'r')
except IOError:
print 'cannot open', arg
else:
print arg, 'has', len(f.readlines()), 'lines'
f.close()
Do not(!) just ‘pass’ in the exception block. This will(!) make you fall on your face even harder.
answered Jul 22, 2015 at 12:43
kiigasskiigass
4271 gold badge4 silver badges16 bronze badges
1
Where ever your error(s) is happening you can wrap it in a try/except block
for i in loop:
try:
code goes here...
except:
pass
answered Jul 22, 2015 at 12:41
AustinAustin
4,3066 gold badges40 silver badges52 bronze badges
Stop and wait protocol is an error control protocol, in this protocol the sender sends data packets one at a time and waits for positive acknowledgment from the receiver’s side, if acknowledgment is received then the sender sends the next data packet else it’ll resend the previous packet until a positive acknowledgment is not received.
Note: To get more info on what is stop and wait protocol, refer Stop and Wait ARQ article.
CRC aka Cyclic redundancy check is an error detection mechanism, its procedure is as follows.
Sender side
- Choose a generator polynomial mutually agreed upon by the sender and receiver, let k be the number of bits in the key obtained from this polynomial.
- Append (k – 1) 0’s to the right of the actual binary data.
- Divide the data obtained in step 2 by the key, and store the remainder.
- Append the remainder to the actual binary data, and send the data packet.
Receiver side
- Divide the received data bits by the key.
- If the remainder is non-zero, then the data is corrupted.
In this article, we will implement stop and wait algorithm such that the sender computes error detecting code using CRC and sends the data along with error detecting code to the receiver using socket. The receiver, after ensuring an error-free frame, saves the data.
The procedure will be as follows:
At sender
- Read input from a file, n characters at a time.
- Convert it into binary
- Generate its CRC.
- Send data + CRC bits to the receiver, but before sending, randomly introduce error
At receiver:
- Receive data packet.
- Determine if it is error-free. If yes extract data bits, convert them into character form, and save them into the output file. Send Ack as OK. If not, send NAK.
At sender
- If OK is received, proceed with the next n characters. Otherwise, if NAK is received, send the data + CRC bits again to the receiver after randomly introducing the error.
Final outcome: Input and output files should match. A log file should show how many frames were in error and how many retries were done for each frame.
Error Generation
Error is introduced as follows :
- Generate a random number, say r1. Perform r1 % 2. If you get a 0 do not introduce error and send original bits. If you get a 1, introduce error.
- To decide which bit will be in error, generate another random number, say r2. Perform r2 %(size of received frame). Assume you get a value i as the outcome. Flip the i-th bit. Now send it to the receiver.
Save this file as error_gen.py.
Python3
import
random
class
err_gen() :
def
induce_err(
self
, in_str) :
chk
=
(
int
)(random.random()
*
1000
)
%
2
if
not
chk :
return
in_str
idx
=
(
int
)(random.random()
*
1000
)
%
len
(in_str)
f_bit
=
'*'
if
in_str[idx]
=
=
'0'
:
f_bit
=
'1'
else
:
f_bit
=
'0'
out_str
=
in_str[ : idx]
+
f_bit
+
in_str[idx
+
1
: ]
return
out_str
if
__name__
=
=
"__main__"
:
data
=
"1001010"
print
(
"Initial : "
, data)
print
(
"Final : "
, err_gen().induce_err(data))
Output:
Initial : 1001010 Final : 0001010
Modulo two Division
Modulo two-division is required to calculate the CRC. Explaining the working of this code is beyond the scope of this article, the curious ones can refer Modulo two division.
Save this file as calc.py.
Python3
def
xor(a, b):
result
=
[]
for
i
in
range
(
1
,
len
(b)):
if
a[i]
=
=
b[i]:
result.append(
'0'
)
else
:
result.append(
'1'
)
return
''.join(result)
def
mod2div(dividend, divisor):
pick
=
len
(divisor)
tmp
=
dividend[
0
: pick]
while
pick <
len
(dividend):
if
tmp[
0
]
=
=
'1'
:
tmp
=
xor(divisor, tmp)
+
dividend[pick]
else
:
tmp
=
xor(
'0'
*
pick, tmp)
+
dividend[pick]
pick
+
=
1
if
tmp[
0
]
=
=
'1'
:
tmp
=
xor(divisor, tmp)
else
:
tmp
=
xor(
'0'
*
pick, tmp)
return
tmp
if
__name__
=
=
"__main__"
:
dividend
=
"10010101"
divisor
=
"011010"
print
(dividend
+
" % "
+
divisor
+
" = "
+
mod2div(dividend, divisor))
Output:
10010101 % 011010 = 01001
Configuration Data
These are some constants that are shared by the receiver and the sender save this file as configuration.py.
Python3
FRAME_SIZE
=
10
BUFFER_SIZE
=
1024
*
10
END_OF_FILE
=
"##**##**##**##**##"
CRC_GENERATOR
=
"10110100110101110011010101110100000101"
REJECT
=
"NAK"
ACCEPT
=
"OK"
Client
The client class will have five methods.
- Constructor: To connect to the server using a socket at the given IP address and port number.
- asciiToBin: To convert ASCII string to binary string.
- appendZero: To append (k – 1) 0’s to the end of binary data.
- encode: To generate and append CRC at the end of actual data bits.
- sendfile :
- This method reads n characters from the input file at a time &
- Creates the data packet to be sent by calling the encode method.
- Calls induce_error method to randomly introduce an error in the data packet.
- Sends the data packet and waits for the acknowledgment.
- If the acknowledgment received is positive, then move on to the next n bits,
- else resend the current data packet.
- When the file is completely read, then send a flag to tell the receiver to stop waiting for the next frame.
- Terminate the session.
Save this file as CLIENT.py.
Python3
from
calc
import
mod2div
from
error_gen
import
err_gen
from
configuration
import
*
import
socket
class
Client:
def
__init__(
self
, ipadd, portn):
self
.socket_
=
socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self
.socket_.connect((ipadd, portn))
def
asciiToBin(
self
, data):
return
bin
(
int
.from_bytes(data.encode(),
'big'
))
def
appendZero(
self
, message):
message
=
(message.ljust(
len
(CRC_GENERATOR)
-
1
+
len
(message),
'0'
))
return
message
def
encode(
self
, data):
message
=
self
.asciiToBin(data)
dividend
=
self
.appendZero(message)
crc
=
mod2div(dividend, CRC_GENERATOR)
curr_frame
=
(message
+
crc)
return
curr_frame
def
send_file(
self
, filename
=
'file.txt'
):
f
=
open
(filename)
data
=
f.read(FRAME_SIZE)
while
len
(data) >
0
:
curr_frame
=
self
.encode(data)
curr_frame
=
err_gen().induce_err(curr_frame)
self
.socket_.send(curr_frame.encode())
if
self
.socket_.recv(BUFFER_SIZE).decode()
=
=
'OK'
:
data
=
f.read(FRAME_SIZE)
self
.socket_.send(END_OF_FILE.encode())
self
.socket_.close()
f.close()
print
(
"File sent"
)
newclient
=
Client(ipadd
=
"127.0.0.1"
, portn
=
3241
)
newclient.send_file(filename
=
"file.txt"
)
Server
The server class will have six methods.
- Constructor: To listen for client request at the given IP address and port number.
- iszero : To determine if a string represents 0.
- isCurrupted : To determine if the received data is corrupted by performing modulo 2 division by the CRC_GENERATOR.
- decode : To extract the data bits from the received data packet and convert them to their ASCII values.
- log : To log the entry of each frame in the logfile.
- receive_file :
- This method receives the data packet from the sender &
- Checks its validity by calling the isCurrupted function.
- If the data packet is valid, then it decodes it and copies it in a file at server’s end and sends a positive acknowledgement to the sender and logs the entry of the data packet in the logfile.
- else, it sends a negative acknowledgement to the sender.
- If the received data packet is the end of file, then it terminates all the connections and returns.
Save this file as SERVER.py
Python3
import
socket
from
calc
import
mod2div
from
configuration
import
*
class
Server:
def
__init__(
self
, ipaddr, portn):
self
.socket_
=
socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self
.socket_.bind((ipaddr, portn))
self
.socket_.listen(
5
)
def
iszero(
self
, data):
for
x
in
data:
if
x !
=
'0'
:
return
False
return
True
def
isCurrupted(
self
, message):
return
not
self
.iszero(mod2div(message, CRC_GENERATOR))
def
decode(
self
, message):
message
=
message[:
1
-
len
(CRC_GENERATOR)]
n
=
int
(message,
2
)
return
n.to_bytes((n.bit_length()
+
7
)
/
/
8
,
'big'
).decode()
def
log(
self
, loghandle, itr, received_frame, retrie_count):
loghandle.write(
"Frame Number : "
+
str
(itr)
+
"\n"
)
loghandle.write(
"Frame Content : \""
+
self
.decode(received_frame)
+
"\"\n"
)
loghandle.write(
"Retries : "
+
str
(retrie_count)
+
"\n\n"
)
def
receive_file(
self
, filepath, logpath):
received_socket, addr
=
self
.socket_.accept()
f
=
open
(filepath,
'w'
)
l
=
open
(logpath,
'w'
)
itr
=
1
retrie_count
=
0
while
1
:
received_frame
=
received_socket.recv(BUFFER_SIZE).decode()
if
received_frame
=
=
END_OF_FILE:
f.close()
l.close()
self
.socket_.close()
print
(
"File Received"
)
return
if
self
.isCurrupted(received_frame):
retrie_count
+
=
1
received_socket.send(REJECT.encode())
else
:
f.write(
self
.decode(received_frame))
self
.log(l, itr, received_frame, retrie_count)
retrie_count
=
0
received_socket.send(ACCEPT.encode())
newServer
=
Server(ipaddr
=
"127.0.0.1"
, portn
=
3241
)
newServer.receive_file(filepath
=
"received_data.txt"
, logpath
=
"logfile.txt"
)
To see the execution of the code, keep all the files in a single directory, and execute the server file before executing the client’s file. Also, make sure that you have the input text file and its correct path.
Execution:
Program execution process
Last Updated :
21 Dec, 2022
Like Article
Save Article
() translation by (you can also view the original English article)
В этом уроке вы узнаете, как обрабатывать ошибки в Python со всех возможных точек зрения. Обработка ошибок является важнейшим аспектом проектирования, и она проходит от самых низких уровней (иногда это аппаратное обеспечение) через весь путь к конечным пользователям. Если у вас нет продуманной стратегии в этой области, ваша система будет ненадежной, а пользовательский опыт будет плохим, и вы будете иметь много проблем с отладкой и устранением неполадок.
Ключ к успеху, зная обо всех связанных сторонах вопроса, рассматривать их целостно и сформировать решение, которое учитывает каждый аспект.
Коды статуса против Исключений
Существует две основных модели обработок ошибок: Коды статуса и Исключения. Коды статуса могут использоваться в любом языке программирования. Исключения требуют поддержки языка/среды исполнения.
Python поддерживает исключения. Python и его стандартная библиотека использует исключения свободно для того, чтобы сообщить о различных состояниях, таких как IO ошибки, ошибки деления на ноль, ошибки пределов индексации, а также некоторые некритичные ситуаций, такие как конец итерации (хотя обычно эти ошибки скрыты). Большинство библиотек придерживаются этого принципа и вызывают исключения.
Это означает, что ваш код будет обрабатывать исключения, возникающие в Python или в библиотеке, во всяком случае, так что вы можете также вызывать исключения напрямую из вашего кода, когда это необходимо и не полагаться на коды статуса.
Небольшой пример
Прежде чем погрузиться в святая святых лучших практик обработки исключений и ошибок Python, давайте рассмотрим некоторые исключения в действии:
1 |
def f(): |
2 |
|
3 |
return 4 / 0 |
4 |
|
5 |
|
6 |
|
7 |
def g(): |
8 |
|
9 |
raise Exception("Don't call us. We'll call you") |
10 |
|
11 |
|
12 |
|
13 |
def h(): |
14 |
|
15 |
try: |
16 |
|
17 |
f() |
18 |
|
19 |
except Exception as e: |
20 |
|
21 |
print(e) |
22 |
|
23 |
try: |
24 |
|
25 |
g() |
26 |
|
27 |
except Exception as e: |
28 |
|
29 |
print(e) |
При вызове h()
, мы получаем на выходе:
1 |
h()
|
2 |
|
3 |
division by zero |
4 |
|
5 |
Don't call us. We'll call you
|
Исключения Python
Python исключениями являются объекты организованные в классовой иерархии.
Вот иерархия целиком:
1 |
BaseException |
2 |
|
3 |
+-- SystemExit |
4 |
|
5 |
+-- KeyboardInterrupt |
6 |
|
7 |
+-- GeneratorExit |
8 |
|
9 |
+-- Exception |
10 |
|
11 |
+-- StopIteration |
12 |
|
13 |
+-- StandardError |
14 |
|
15 |
| +-- BufferError |
16 |
|
17 |
| +-- ArithmeticError |
18 |
|
19 |
| | +-- FloatingPointError |
20 |
|
21 |
| | +-- OverflowError |
22 |
|
23 |
| | +-- ZeroDivisionError |
24 |
|
25 |
| +-- AssertionError |
26 |
|
27 |
| +-- AttributeError |
28 |
|
29 |
| +-- EnvironmentError |
30 |
|
31 |
| | +-- IOError |
32 |
|
33 |
| | +-- OSError |
34 |
|
35 |
| | +-- WindowsError (Windows) |
36 |
|
37 |
| | +-- VMSError (VMS) |
38 |
|
39 |
| +-- EOFError |
40 |
|
41 |
| +-- ImportError |
42 |
|
43 |
| +-- LookupError |
44 |
|
45 |
| | +-- IndexError |
46 |
|
47 |
| | +-- KeyError |
48 |
|
49 |
| +-- MemoryError |
50 |
|
51 |
| +-- NameError |
52 |
|
53 |
| | +-- UnboundLocalError |
54 |
|
55 |
| +-- ReferenceError |
56 |
|
57 |
| +-- RuntimeError |
58 |
|
59 |
| | +-- NotImplementedError |
60 |
|
61 |
| +-- SyntaxError |
62 |
|
63 |
| | +-- IndentationError |
64 |
|
65 |
| | +-- TabError |
66 |
|
67 |
| +-- SystemError |
68 |
|
69 |
| +-- TypeError |
70 |
|
71 |
| +-- ValueError |
72 |
|
73 |
| +-- UnicodeError |
74 |
|
75 |
| +-- UnicodeDecodeError |
76 |
|
77 |
| +-- UnicodeEncodeError |
78 |
|
79 |
| +-- UnicodeTranslateError |
80 |
|
81 |
+-- Warning |
82 |
|
83 |
+-- DeprecationWarning |
84 |
|
85 |
+-- PendingDeprecationWarning |
86 |
|
87 |
+-- RuntimeWarning |
88 |
|
89 |
+-- SyntaxWarning |
90 |
|
91 |
+-- UserWarning |
92 |
|
93 |
+-- FutureWarning |
94 |
|
95 |
+-- ImportWarning |
96 |
|
97 |
+-- UnicodeWarning |
98 |
|
99 |
+-- BytesWarning |
100 |
|
Существует несколько специальных исключений, которые являются производными от BaseException
, такие как SystemExit
, KeyboardInterrupt
и GeneratorExit
. Еще есть класс Exception
, который является базовым классом для StopIteration
, StandardError
и Warning
. Все стандартные ошибки являются производными от StandardError
.
Когда вы получаете исключение или функция, которую вы выполнили вызывает исключение, обычный порядок кода завершается и исключение начинает распространятся вверх по стеку вызовов до тех пор, пока не встречает обработчик соответствующих исключений. Если обработчик не доступен, процесс (или, точнее, текущий поток) будет прекращен с сообщением о необработанном исключении.
Вызов исключений
Вызов исключений очень прост. Вы просто используете ключевое слово raise
чтобы вызвать объект, который является подклассом Exception
. Это может быть экземпляр Exception
, одно из стандартных исключений (напр., RuntimeError
), или подкласс Exception
, который вы получили. Вот небольшой фрагмент кода, который демонстрирует эти случаи:
1 |
# Raise an instance of the Exception class itself
|
2 |
|
3 |
raise Exception('Ummm... something is wrong') |
4 |
|
5 |
|
6 |
|
7 |
# Raise an instance of the RuntimeError class
|
8 |
|
9 |
raise RuntimeError('Ummm... something is wrong') |
10 |
|
11 |
|
12 |
|
13 |
# Raise a custom subclass of Exception that keeps the timestamp the exception was created
|
14 |
|
15 |
from datetime import datetime |
16 |
|
17 |
|
18 |
|
19 |
class SuperError(Exception): |
20 |
|
21 |
def __init__(self, message): |
22 |
|
23 |
Exception.__init__(message) |
24 |
|
25 |
self.when = datetime.now() |
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
raise SuperError('Ummm... something is wrong') |
Перехват исключений
Вы получили исключение, с условием except
, как вы видели в примере. Когда вы получили исключение, у вас есть три варианта:
- Пропустить (обработать его и продолжить работу).
- Сделать что-то вроде записи в журнал, но получить повторно то же самое исключение, чтобы продолжить его обработку на более высоком уровне.
- Вызвать другое исключение вместо текущего.
Пропустить исключение
Если вы знаете, как его обработать и как его полностью восстановить, можно пропустить исключение.
Например, если вы получаете входящий файл, который может быть в различных форматах (JSON, YAML), вы можете попробовать проанализировать его с помощью различных средств. Если анализатор JSON создаёт исключение, которое показывает, что файл имеет некорректный формат JSON, вы пропускаете его и пробуете проанализировать через парсер YAML. Если парсер YAML также не справляется с задачей, тогда вы даёте исключению перейти на следующий уровень.
1 |
import json |
2 |
|
3 |
import yaml |
4 |
|
5 |
|
6 |
|
7 |
def parse_file(filename): |
8 |
|
9 |
try: |
10 |
|
11 |
return json.load(open(filename)) |
12 |
|
13 |
except json.JSONDecodeError |
14 |
|
15 |
return yaml.load(open(filename)) |
Обратите внимание, что другие исключения (например, file not found или no read permissions) будут переходить на следующий уровень и не будут обработаны конкретным исключением. Это хорошая тактика в том случае, если вы хотите использовать YAML парсер, когда анализ с помощью JSON парсера не удался.
Если вы хотите обрабатывать все исключения, тогда используйте except Exception
. Например:
1 |
def print_exception_type(func, *args, **kwargs): |
2 |
|
3 |
try: |
4 |
|
5 |
return func(*args, **kwargs) |
6 |
|
7 |
except Exception as e: |
8 |
|
9 |
print type(e) |
Обратите внимание, что, добавляя as e
, вы привязываете объект к имении e
в вашем исключении.
Перезапуск исключения
Чтобы перезапустить исключение, просто напишите raise
без аргументов внутри обработчика. Это позволит выполнить некоторую локальную обработку, но также пропустит исключение для обработки на верхние уровни. Здесь, функция invoke_function()
выводит тип исключения в консоль и затем повторно вызывает его.
1 |
def invoke_function(func, *args, **kwargs): |
2 |
|
3 |
try: |
4 |
|
5 |
return func(*args, **kwargs) |
6 |
|
7 |
except Exception as e: |
8 |
|
9 |
print type(e) |
10 |
|
11 |
raise
|
Вызов Различных Исключений
Есть несколько случаев, когда вы хотели бы вызвать другое исключение. Иногда вы хотите сгруппировать несколько различных низкоуровневых исключений в одну категорию, которая равномерно обрабатывается на более высоком уровне кода. В других случаях вам нужно преобразовать исключение на уровне пользователя и предоставить контекст конкретного приложения.
Финальное утверждение
Иногда вы хотите убедиться, что код очистки выполняется, даже если где-то по пути возникло исключение. Например, у вас может быть подключение к базе данных, которое требуется закрыть, как только вы закончите. Это неправильный способ сделать это:
1 |
def fetch_some_data(): |
2 |
|
3 |
db = open_db_connection() |
4 |
|
5 |
query(db) |
6 |
|
7 |
close_db_Connection(db) |
Если функция query()
вызывает исключение, то вызов close_db_connection()
никогда не будет выполнен и подключение останется открытым. Утверждение finally
всегда выполняется после всех попыток обработчика. Вот как сделать это правильно:
1 |
def fetch_some_data(): |
2 |
|
3 |
db = None |
4 |
|
5 |
try: |
6 |
|
7 |
db = open_db_connection() |
8 |
|
9 |
query(db) |
10 |
|
11 |
finally: |
12 |
|
13 |
if db is not None: |
14 |
|
15 |
close_db_connection(db) |
Вызов open_db_connection()
может не вернуть подключение или вызвать исключение. В этом случае нет необходимости закрывать соединение.
При использовании finally
, вы должны быть осторожны, чтобы не вызвать другие исключения, потому, что они скроют исходное.
Диспетчеров Контекста
Контекстные менеджеры обеспечивают еще один механизм обработки ресурсов, таких как файлы или подключения к БД, которые выполняются автоматически, даже если исключения были вызваны. Вместо блоков try-finally, можно использовать определение with
. Вот пример с файлом:
1 |
def process_file(filename): |
2 |
|
3 |
with open(filename) as f: |
4 |
|
5 |
process(f.read()) |
Теперь, даже если process()
вызывает исключение, этот файл будет закрыт правильно сразу же когда область видимости блока with
завершена, независимо от того, было исключение обработано или нет.
Ведение журнала
Ведение журнала обычно требуется в нетривиальных, масштабных системах. Это особенно полезно в веб-приложениях, где вы можете исправить все исключения универсальным способом: Просто записать в журнал исключение и вернуть сообщение об ошибке.
При записи полезно учитывать тип исключения, сообщение и маршрут ошибки. Вся эта информация доступна через объект sys.exc_info
, но если вы используете logger.exception()
метод в обработчике исключений, Python извлечёт всю необходимую для вас информацию.
Это лучший пример, которую я рекомендую:
1 |
import logging |
2 |
|
3 |
logger = logging.getLogger() |
4 |
|
5 |
|
6 |
|
7 |
def f(): |
8 |
|
9 |
try: |
10 |
|
11 |
flaky_func() |
12 |
|
13 |
except Exception: |
14 |
|
15 |
logger.exception() |
16 |
|
17 |
raise
|
Если вы будете придерживаться этому шаблону,тогда (предполагаю, что вы настроили запись в журнал правильно), независимо от того, что происходит, вы будете иметь очень понятные записи в ваших журналах о что пошло не так, и будете иметь возможность исправить проблему.
Если вы повторно вызываете исключение, убедитесь что вы не записывайте в журнал повторно одну и туже ошибку на разных уровнях. Эту будет бесполезный мусор. который может запутать вас и заставить думать что произошло несколько ошибок, хотя на самом деле одна ошибка была зарегистрирована несколько раз.
Самый простой способ сделать это заключается в том, чтобы позволить всем исключениям переходить дальше (если они могут быть обработаны и пропущены ранее), и затем выполнить запись в журнал на самом верхнем уровне системы/приложения.
Sentry
Ведение журнала это возможность. Наиболее распространенные реализации которой, является использование журнала. Но, для крупномасштабных распределенных систем с сотнями, тысячами или более серверов, это не всегда лучшее решение.
Для отслеживания исключений во всей инфраструктуре, такой сервис как sentry очень полезен. Он централизует все сообщения об исключениях, и в дополнении к маршруту ошибки он добавляет состояние каждого состояния стека (значение переменных в то время, когда было вызвано исключение). Он также предоставляет приятный интерфейс с панелью мониторинга, отчетами и способами получать сообщения по нескольким проектам сразу. Он предоставляется с открытым исходным кодом, так что вы можете запустить свой собственный сервер или оформить подписку на предустановленную версию.
Работа с временной ошибкой
Некоторые ошибки являются временными, в частности при работе с распределенными системами. Система, которая начинает ругаться при первом признаке ошибки не очень полезна.
Если ваш код получает доступ к удаленной системе, которая не отвечает, традиционное решение, это таймауты, но иногда случается не каждая система разработана с таймаутами. Таймауты, не всегда удобны для калибровки при изменении условий.
Другой подход заключается в том, чтобы быстро получить ошибку и затем повторить попытку. Преимущество в том, что если цель реагирует быстро, то вам не придется тратить много времени находясь в режиме ожидания и можно реагировать незамедлительно. Но если это сделать не удалось, вы можете повторять запрос несколько раз до тех пор, пока вы не решите, что ресурс действительно недоступен и вызовете исключение. В следующем разделе я расскажу об оформителе, который может сделать это для вас.
Полезные оформители
Два оформителя которые могут помочь в обработке ошибок, это @log_error
, который записывает исключение и затем вновь вызывает его и @retry
оформитель, который будет повторять вызов функции несколько раз.
Журнал ошибок
Вот пример простой реализации. Оформитель исключает объект logger. Когда он оформляет функцию и функция вызвана, он обработает вызов в блоке try-except, и если там было исключение сделает запись в журнал и наконец повторно вызовет исключение.
1 |
def log_error(logger) |
2 |
|
3 |
def decorated(f): |
4 |
|
5 |
@functools.wraps(f) |
6 |
|
7 |
def wrapped(*args, **kwargs): |
8 |
|
9 |
try: |
10 |
|
11 |
return f(*args, **kwargs) |
12 |
|
13 |
except Exception as e: |
14 |
|
15 |
if logger: |
16 |
|
17 |
logger.exception(e) |
18 |
|
19 |
raise
|
20 |
|
21 |
return wrapped |
22 |
|
23 |
return decorated |
Вот пример, как его использовать:
1 |
import logging |
2 |
|
3 |
logger = logging.getLogger() |
4 |
|
5 |
|
6 |
|
7 |
@log_error(logger) |
8 |
|
9 |
def f(): |
10 |
|
11 |
raise Exception('I am exceptional') |
Retrier
Здесь, очень хорошая реализация @retry оформителя.
1 |
import time |
2 |
|
3 |
import math |
4 |
|
5 |
|
6 |
|
7 |
# Retry decorator with exponential backoff
|
8 |
|
9 |
def retry(tries, delay=3, backoff=2): |
10 |
|
11 |
'''Retries a function or method until it returns True.
|
12 |
|
13 |
|
14 |
|
15 |
delay sets the initial delay in seconds, and backoff sets the factor by which
|
16 |
|
17 |
the delay should lengthen after each failure. backoff must be greater than 1,
|
18 |
|
19 |
or else it isn't really a backoff. tries must be at least 0, and delay
|
20 |
|
21 |
greater than 0.'''
|
22 |
|
23 |
|
24 |
|
25 |
if backoff <= 1: |
26 |
|
27 |
raise ValueError("backoff must be greater than 1") |
28 |
|
29 |
|
30 |
|
31 |
tries = math.floor(tries) |
32 |
|
33 |
if tries < 0: |
34 |
|
35 |
raise ValueError("tries must be 0 or greater") |
36 |
|
37 |
|
38 |
|
39 |
if delay <= 0: |
40 |
|
41 |
raise ValueError("delay must be greater than 0") |
42 |
|
43 |
|
44 |
|
45 |
def deco_retry(f): |
46 |
|
47 |
def f_retry(*args, **kwargs): |
48 |
|
49 |
mtries, mdelay = tries, delay # make mutable |
50 |
|
51 |
|
52 |
|
53 |
rv = f(*args, **kwargs) # first attempt |
54 |
|
55 |
while mtries > 0: |
56 |
|
57 |
if rv is True: # Done on success |
58 |
|
59 |
return True |
60 |
|
61 |
|
62 |
|
63 |
mtries -= 1 # consume an attempt |
64 |
|
65 |
time.sleep(mdelay) # wait... |
66 |
|
67 |
mdelay *= backoff # make future wait longer |
68 |
|
69 |
|
70 |
|
71 |
rv = f(*args, **kwargs) # Try again |
72 |
|
73 |
|
74 |
|
75 |
return False # Ran out of tries :-( |
76 |
|
77 |
|
78 |
|
79 |
return f_retry # true decorator -> decorated function |
80 |
|
81 |
return deco_retry # @retry(arg[, ...]) -> true decorator |
Заключение
Обработка ошибок имеет решающее значение для пользователей и разработчиков. Python предоставляет отличную поддержку на уровне языка и стандартной библиотеки для обработки ошибок на основе исключений. Следуя рекомендациям старательно, вы можете преодолеть этот аспект, которым так часто пренебрегают.
Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером
Тестирование ошибок
—
Python: Продвинутое тестирование
В этом уроке мы начнем погружаться в тестирование и обсудим нюансы, которые возникают в тестировании приложений.
Основные тесты, которые нужно писать — это тесты на успешные сценарии работы. Но в некоторых ситуациях код должен возвращать ошибки, и их тоже бывает нужно проверять. Под ошибками понимаются ситуации, в которых код выбрасывает исключение.
Посмотрим на примере такого теста:
def test_exception():
try:
function_with_exception(0)
except Exception as e:
assert e
Этот код пытается протестировать ситуацию, при которой функция function_with_exception()
выбрасывает исключение, если ей передать 0
. Как думаете, этот тест проверит, что функция действительно порождает исключение?
На самом деле, нет. Если функция function_with_exception()
не выбросит исключение, то тест пройдет, потому что код не попадет в блок except
.
В Pytest встроен контекстный менеджер, который самостоятельно отлавливает исключение и проверяет, что оно вообще было сгенерировано:
import pytest
def test_exception():
with pytest.raises(Exception):
function_with_exception(0)
Здесь raises
перехватывает только те исключения, которые являются подтипами переданного класса. Благодаря этому мы можем управлять ожидаемым поведением и ловить только те ошибки, которые хотим поймать.
Более того, можно проверить конкретное сообщение, которое пришло вместе с исключением:
import pytest
def test_exception():
# Добавляем: as e. Здесь e – произвольное имя переменной, содержащей исключение
with pytest.raises(Exception) as e:
function_with_exception(0)
Открыть доступ
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
-
130 курсов, 2000+ часов теории -
1000 практических заданий в браузере -
360 000 студентов
Наши выпускники работают в компаниях:
Рекомендуемые программы
профессия
•
от 6 300 ₽ в месяц
Разработка веб-приложений на Django