8. خطاها و استثناها¶
تا اینجا پیامهای خطا فقط به صورت کوتاه ذکر شدهاند، اما اگر مثالها را امتحان کرده باشید احتمالاً برخی از آنها را دیدهاید. حداقل دو نوع متمایز از خطا وجود دارد: خطاهای دستوری «syntax errors» و استثناها «exceptions».
8.1. خطاهای دستوری¶
خطاهای دستوری، که بهعنوان خطاهای تجزیهگر «parsing errors» نیز شناخته میشوند، شاید رایجترین نوع خطاهایی باشند که هنگام یادگیری پایتون با آنها مواجه میشوید:
>>> while True print('Hello world')
File "<stdin>", line 1
while True print('Hello world')
^^^^^
SyntaxError: invalid syntax
تجزیهگر «parser» خط مشکلدار را تکرار میکند و فلشهای کوچکی را نشان میدهد که به جایی اشاره دارند که خطا در آن شناسایی شده است.
توجه داشته باشید که این همیشه همان جایی نیست که باید اصلاح شود.
در این مثال، خطا در تابع print() شناسایی شده است، زیرا دو نقطه (':') درست قبل از آن وجود ندارد.
نام فایل (در مثال ما <stdin>) و شماره خط چاپ می شوند تا اگر ورودی از یک فایل آمده باشد، بدانید باید کجا را بررسی کنید.
8.2. استثناء ها¶
حتی اگر یک دستور یا عبارت از نظر دستوری «syntactically» درست باشد، ممکن است هنگام اجرای آن خطایی رخ دهد. خطاهایی که هنگام اجرا شناسایی میشوند «استثنا» «exceptions» نام دارند و الزاماً باعث توقف برنامه نمیشوند؛ بهزودی خواهید آموخت چگونه آنها را در برنامههای پایتون مدیریت کنید. با این حال، بیشتر استثناها در برنامهها مدیریت نمیشوند و به پیامهای خطا ختم میشوند، مانند چیزی که در اینجا نمایش داده شده است:
>>> 10 * (1/0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
10 * (1/0)
~^~
ZeroDivisionError: division by zero
>>> 4 + spam*3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
4 + spam*3
^^^^
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
'2' + 2
~~~~^~~
TypeError: can only concatenate str (not "int") to str
آخرین خط از پیام خطا نشان می دهد که چه اتفاقی افتاده است. استثناها دارای انواع مختلفی هستند و نوع آنها به عنوان بخشی از پیام چاپ می شود: انواع موجود در مثال عبارتند از ZeroDivisionError، NameError و TypeError. رشته ای که به عنوان نوع استثنا چاپ می شود، نام استثنای داخلی است که رخ داده است. این موضوع برای تمام استثناهای داخلی صدق می کند، اما برای استثناهای تعریف شده توسط کاربر لزوماً صحیح نیست (گرچه این یک قاعدهٔ مفید محسوب میشود). نام های استثنای استاندارد شناسه های داخلی هستند (نه کلمات کلیدی رزرو شده).
بخش باقیماندهٔ خط، جزئیاتی را بسته به نوع استثنا «exception» و علت وقوع آن ارائه میدهد.
بخش قبلی پیام خطا، زمینهای را نشان میدهد که در آن استثنا رخ داده است، بهصورت یک ردگیری پشته «stack traceback». بهطور کلی، این بخش شامل ردگیری پشتهای است که خطوطی از کد منبع را فهرست میکند؛ با این حال، خطوطی که از ورودی استاندارد «standard input» خوانده شدهاند در آن نمایش داده نمیشوند.
Built-in Exceptions لیستی از استثناهای داخلی و معانی آن ها را ارائه می دهد.
8.3. مدیریت استثناها¶
امکان نوشتن برنامههایی وجود دارد که برخی از استثناها را مدیریت میکنند. به مثال زیر نگاه کنید که از کاربر ورودی میخواهد تا زمانی که یک عدد صحیح معتبر وارد شود، اما به کاربر اجازه میدهد برنامه را قطع کند (با فشردن Control-C یا هر روشی که سیستمعامل پشتیبانی میکند)؛ توجه داشته باشید که قطع برنامه توسط کاربر با ایجاد استثنای KeyboardInterrupt مشخص میشود.
>>> while True:
... try:
... x = int(input("Please enter a number: "))
... break
... except ValueError:
... print("Oops! That was no valid number. Try again...")
...
دستور try به شکل زیر عمل میکند.
ابتدا بخش try clause (دستوری که بین کلیدواژه های
tryوexceptقرار دارد) اجرا می شود.اگر هیچ استثنایی رخ ندهد، بخش except نادیده گرفته میشود و اجرای دستور
tryبه پایان میرسد.اگر در طول اجرای بخش
tryاستثنایی رخ دهد، بقیه آن بخش نادیده گرفته میشود. سپس، اگر نوع استثنا با استثنایی که بعد از کلیدواژهexceptآمده مطابقت داشته باشد، بخش except اجرا میشود و پس از آن اجرای برنامه بعد از بلوک try/except ادامه مییابد.If an exception occurs which does not match the exception named in the except clause, it is passed on to outer
trystatements; if no handler is found, it is an unhandled exception and execution stops with an error message.
A try statement may have more than one except clause, to specify
handlers for different exceptions. At most one handler will be executed.
Handlers only handle exceptions that occur in the corresponding try clause,
not in other handlers of the same try statement. An except clause
may name multiple exceptions as a parenthesized tuple, for example:
... except (RuntimeError, TypeError, NameError):
... pass
A class in an except clause matches exceptions which are instances of the
class itself or one of its derived classes (but not the other way around --- an
except clause listing a derived class does not match instances of its base classes).
For example, the following code will print B, C, D in that order:
class B(Exception):
pass
class C(B):
pass
class D(C):
pass
for cls in [B, C, D]:
try:
raise cls()
except D:
print("D")
except C:
print("C")
except B:
print("B")
Note that if the except clauses were reversed (with except B first), it
would have printed B, B, B --- the first matching except clause is triggered.
When an exception occurs, it may have associated values, also known as the exception's arguments. The presence and types of the arguments depend on the exception type.
The except clause may specify a variable after the exception name. The
variable is bound to the exception instance which typically has an args
attribute that stores the arguments. For convenience, builtin exception
types define __str__() to print all the arguments without explicitly
accessing .args.
>>> try:
... raise Exception('spam', 'eggs')
... except Exception as inst:
... print(type(inst)) # the exception type
... print(inst.args) # arguments stored in .args
... print(inst) # __str__ allows args to be printed directly,
... # but may be overridden in exception subclasses
... x, y = inst.args # unpack args
... print('x =', x)
... print('y =', y)
...
<class 'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs
The exception's __str__() output is printed as the last part ('detail')
of the message for unhandled exceptions.
BaseException is the common base class of all exceptions. One of its
subclasses, Exception, is the base class of all the non-fatal exceptions.
Exceptions which are not subclasses of Exception are not typically
handled, because they are used to indicate that the program should terminate.
They include SystemExit which is raised by sys.exit() and
KeyboardInterrupt which is raised when a user wishes to interrupt
the program.
Exception can be used as a wildcard that catches (almost) everything.
However, it is good practice to be as specific as possible with the types
of exceptions that we intend to handle, and to allow any unexpected
exceptions to propagate on.
The most common pattern for handling Exception is to print or log
the exception and then re-raise it (allowing a caller to handle the
exception as well):
import sys
try:
f = open('myfile.txt')
s = f.readline()
i = int(s.strip())
except OSError as err:
print("OS error:", err)
except ValueError:
print("Could not convert data to an integer.")
except Exception as err:
print(f"Unexpected {err=}, {type(err)=}")
raise
The try ... except statement has an optional else
clause, which, when present, must follow all except clauses. It is useful
for code that must be executed if the try clause does not raise an exception.
For example:
for arg in sys.argv[1:]:
try:
f = open(arg, 'r')
except OSError:
print('cannot open', arg)
else:
print(arg, 'has', len(f.readlines()), 'lines')
f.close()
The use of the else clause is better than adding additional code to
the try clause because it avoids accidentally catching an exception
that wasn't raised by the code being protected by the try ...
except statement.
Exception handlers do not handle only exceptions that occur immediately in the try clause, but also those that occur inside functions that are called (even indirectly) in the try clause. For example:
>>> def this_fails():
... x = 1/0
...
>>> try:
... this_fails()
... except ZeroDivisionError as err:
... print('Handling run-time error:', err)
...
Handling run-time error: division by zero
8.4. Raising Exceptions¶
The raise statement allows the programmer to force a specified
exception to occur. For example:
>>> raise NameError('HiThere')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
raise NameError('HiThere')
NameError: HiThere
The sole argument to raise indicates the exception to be raised.
This must be either an exception instance or an exception class (a class that
derives from BaseException, such as Exception or one of its
subclasses). If an exception class is passed, it will be implicitly
instantiated by calling its constructor with no arguments:
raise ValueError # shorthand for 'raise ValueError()'
If you need to determine whether an exception was raised but don't intend to
handle it, a simpler form of the raise statement allows you to
re-raise the exception:
>>> try:
... raise NameError('HiThere')
... except NameError:
... print('An exception flew by!')
... raise
...
An exception flew by!
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
raise NameError('HiThere')
NameError: HiThere
8.5. Exception Chaining¶
If an unhandled exception occurs inside an except section, it will
have the exception being handled attached to it and included in the error
message:
>>> try:
... open("database.sqlite")
... except OSError:
... raise RuntimeError("unable to handle error")
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
open("database.sqlite")
~~~~^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'database.sqlite'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
raise RuntimeError("unable to handle error")
RuntimeError: unable to handle error
To indicate that an exception is a direct consequence of another, the
raise statement allows an optional from clause:
# exc must be exception instance or None.
raise RuntimeError from exc
This can be useful when you are transforming exceptions. For example:
>>> def func():
... raise ConnectionError
...
>>> try:
... func()
... except ConnectionError as exc:
... raise RuntimeError('Failed to open database') from exc
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
func()
~~~~^^
File "<stdin>", line 2, in func
ConnectionError
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
raise RuntimeError('Failed to open database') from exc
RuntimeError: Failed to open database
It also allows disabling automatic exception chaining using the from None
idiom:
>>> try:
... open('database.sqlite')
... except OSError:
... raise RuntimeError from None
...
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
raise RuntimeError from None
RuntimeError
For more information about chaining mechanics, see Built-in Exceptions.
8.6. User-defined Exceptions¶
Programs may name their own exceptions by creating a new exception class (see
کلاس ها for more about Python classes). Exceptions should typically
be derived from the Exception class, either directly or indirectly.
Exception classes can be defined which do anything any other class can do, but are usually kept simple, often only offering a number of attributes that allow information about the error to be extracted by handlers for the exception.
Most exceptions are defined with names that end in "Error", similar to the naming of the standard exceptions.
Many standard modules define their own exceptions to report errors that may occur in functions they define.
8.7. Defining Clean-up Actions¶
The try statement has another optional clause which is intended to
define clean-up actions that must be executed under all circumstances. For
example:
>>> try:
... raise KeyboardInterrupt
... finally:
... print('Goodbye, world!')
...
Goodbye, world!
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
raise KeyboardInterrupt
KeyboardInterrupt
If a finally clause is present, the finally
clause will execute as the last task before the try
statement completes. The finally clause runs whether or
not the try statement produces an exception. The following
points discuss more complex cases when an exception occurs:
If an exception occurs during execution of the
tryclause, the exception may be handled by anexceptclause. If the exception is not handled by anexceptclause, the exception is re-raised after thefinallyclause has been executed.An exception could occur during execution of an
exceptorelseclause. Again, the exception is re-raised after thefinallyclause has been executed.If the
finallyclause executes abreak,continueorreturnstatement, exceptions are not re-raised.If the
trystatement reaches abreak,continueorreturnstatement, thefinallyclause will execute just prior to thebreak,continueorreturnstatement's execution.If a
finallyclause includes areturnstatement, the returned value will be the one from thefinallyclause'sreturnstatement, not the value from thetryclause'sreturnstatement.
For example:
>>> def bool_return():
... try:
... return True
... finally:
... return False
...
>>> bool_return()
False
A more complicated example:
>>> def divide(x, y):
... try:
... result = x / y
... except ZeroDivisionError:
... print("division by zero!")
... else:
... print("result is", result)
... finally:
... print("executing finally clause")
...
>>> divide(2, 1)
result is 2.0
executing finally clause
>>> divide(2, 0)
division by zero!
executing finally clause
>>> divide("2", "1")
executing finally clause
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
divide("2", "1")
~~~~~~^^^^^^^^^^
File "<stdin>", line 3, in divide
result = x / y
~~^~~
TypeError: unsupported operand type(s) for /: 'str' and 'str'
As you can see, the finally clause is executed in any event. The
TypeError raised by dividing two strings is not handled by the
except clause and therefore re-raised after the finally
clause has been executed.
In real world applications, the finally clause is useful for
releasing external resources (such as files or network connections), regardless
of whether the use of the resource was successful.
8.8. Predefined Clean-up Actions¶
Some objects define standard clean-up actions to be undertaken when the object is no longer needed, regardless of whether or not the operation using the object succeeded or failed. Look at the following example, which tries to open a file and print its contents to the screen.
for line in open("myfile.txt"):
print(line, end="")
The problem with this code is that it leaves the file open for an indeterminate
amount of time after this part of the code has finished executing.
This is not an issue in simple scripts, but can be a problem for larger
applications. The with statement allows objects like files to be
used in a way that ensures they are always cleaned up promptly and correctly.
with open("myfile.txt") as f:
for line in f:
print(line, end="")
After the statement is executed, the file f is always closed, even if a problem was encountered while processing the lines. Objects which, like files, provide predefined clean-up actions will indicate this in their documentation.
8.10. Enriching Exceptions with Notes¶
When an exception is created in order to be raised, it is usually initialized
with information that describes the error that has occurred. There are cases
where it is useful to add information after the exception was caught. For this
purpose, exceptions have a method add_note(note) that accepts a string and
adds it to the exception's notes list. The standard traceback rendering
includes all notes, in the order they were added, after the exception.
>>> try:
... raise TypeError('bad type')
... except Exception as e:
... e.add_note('Add some information')
... e.add_note('Add some more information')
... raise
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
raise TypeError('bad type')
TypeError: bad type
Add some information
Add some more information
>>>
For example, when collecting exceptions into an exception group, we may want to add context information for the individual errors. In the following each exception in the group has a note indicating when this error has occurred.
>>> def f():
... raise OSError('operation failed')
...
>>> excs = []
>>> for i in range(3):
... try:
... f()
... except Exception as e:
... e.add_note(f'Happened in Iteration {i+1}')
... excs.append(e)
...
>>> raise ExceptionGroup('We have some problems', excs)
+ Exception Group Traceback (most recent call last):
| File "<stdin>", line 1, in <module>
| raise ExceptionGroup('We have some problems', excs)
| ExceptionGroup: We have some problems (3 sub-exceptions)
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| f()
| ~^^
| File "<stdin>", line 2, in f
| raise OSError('operation failed')
| OSError: operation failed
| Happened in Iteration 1
+---------------- 2 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| f()
| ~^^
| File "<stdin>", line 2, in f
| raise OSError('operation failed')
| OSError: operation failed
| Happened in Iteration 2
+---------------- 3 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| f()
| ~^^
| File "<stdin>", line 2, in f
| raise OSError('operation failed')
| OSError: operation failed
| Happened in Iteration 3
+------------------------------------
>>>