Your interest in the new book " Python Pro Secrets " has convinced us that the story about Python's quirks is worth continuing. Today we offer to read a small tutorial on creating custom (in the text - your own) exception classes. The author got it interesting, it's hard to disagree with him that the most important advantage of an exception is the completeness and clarity of the error message. Part of the code from the original is in the form of pictures.
Welcome to cat.
Creating your own error classes
Python provides the ability to create your own exception classes. By creating such classes, you can diversify the design of the classes in your application. A custom class of errors could log errors, inspect the object. It is we who define what the exception class does, although usually a custom class can hardly do more than display a message.
Naturally, the type of error itself is important, and we often create our own error types to indicate a specific situation that is usually not covered at the Python level. This way, users of the class will know exactly what is going on when they encounter this error.
This article is divided into two parts. First, we will define an exception class by itself. We will then demonstrate how we can integrate our own exception classes into our Python programs and show how we can thus improve the usability of the classes we design.
MyCustomError custom exception class
Throwing an exception requires the
__init__()
and methods
__str__()
.
When throwing an exception, we already create an instance of the exception and at the same time display it on the screen. Let's take a closer look at our custom exception class shown below.
The above MyCustomError class has two magic methods,
__init__
and
__str__
, which are automatically called during exception handling. The method
Init
is called when the instance is created, and the method
str
is called when the instance is displayed. Therefore, when an exception is thrown, these two methods are usually called immediately after each other. The Python exception throw statement puts a program into an error state.
In the argument list of the method
__init__
is
*args
. A component
*args
is a special pattern matching mode used in functions and methods. It allows you to pass multiple arguments, and stores the passed arguments as a tuple, but at the same time allows you not to pass arguments at all.
In our case, we can say that if
MyCustomError
any arguments were passed to the constructor , then we take the first passed argument and assign it to an attribute
message
in the object. If no arguments were passed, then the attribute
message
will be assigned a value
None
.
In the first example, the exception
MyCustomError
is thrown without any arguments, so the attribute
message
this object is assigned a value
None
. A method will be called
str
and will display the message 'MyCustomError message has been raised'.
The exception
MyCustomError
is thrown without any arguments (parentheses are empty). In other words, such an object design looks non-standard. But this is just syntactic support provided in Python when throwing an exception.
In the second example, it
MyCustomError
is passed with a string argument 'We have a problem'. It is set as an attribute of
message
the object and displayed as an error message when an exception is thrown.
The code for the MyCustomError exception class is here...
class MyCustomError(Exception):
def __init__(self, *args):
if args:
self.message = args[0]
else:
self.message = None
def __str__(self):
print('calling str')
if self.message:
return 'MyCustomError, {0} '.format(self.message)
else:
return 'MyCustomError has been raised'
# MyCustomError
raise MyCustomError('We have a problem')
CustomIntFloatDic Class
We create our own dictionary, the values ββof which can only be integers and floating point numbers.
Let's go ahead and demonstrate how to easily and usefully inject error classes into our own programs. To begin with, I will offer a slightly contrived example. In this fictional example, I'll create my own dictionary that can only accept integers or floating point numbers.
If the user tries to specify any other data type as the value in this dictionary, an exception will be thrown. This exception will provide useful information to the user on how to use this dictionary. In our case, this message directly informs the user that only integers or floating point numbers can be specified as values ββin this dictionary.
When creating your own dictionary, keep in mind that there are two places where values ββcan be added to the dictionary. Firstly, this can happen in the init method when creating an object (at this stage, the object can already be assigned keys and values), and secondly, when setting keys and values ββdirectly in the dictionary. In both of these places, you need to write code to ensure that the value can only be of type
int
or
float
.
First, I'll define the CustomIntFloatDict class that inherits from the built-in class
dict
.
dict
is passed in a list of arguments, which are enclosed in parentheses and follow the class name
CustomIntFloatDict
.
If a class is instantiated
CustomIntFloatDict
, moreover, no arguments are passed to the key and value parameters, they will be set to
None
. The expression is
if
interpreted as follows: if either the key is equal
None
, or the value is equal
None
, then a method will be called with the object
get_dict()
, which will return the attribute
empty_dict
; such an attribute on an object points to an empty list. Remember that class attributes are available on all instances of the class.
The purpose of this class is to allow the user to pass a list or tuple with the keys and values ββinside. If the user enters a list or a tuple looking for keys and values, then these two enumerated sets will be concatenated using the function
zip
the Python language. A hooked variable pointing to an object
zip
is iterable, and tuples are unpackable. By iterating over the tuples, I check if val is an instance of the class
int
or
float
. If it
val
doesn't belong to any of these classes, I throw
IntFloatValueError
my own exception and pass val as an argument to it.
IntFloatValueError Exception Class
When an exception is thrown,
IntFloatValueError
we create an instance of the class
IntFloatValueError
and simultaneously display it on the screen. This means that the magic methods
init
and will be called
str
.
The value that caused the thrown exception is set as an attribute
value
accompanying the class
IntFloatValueError
. When calling the magic str method, the user receives an error message informing that the value
init
in
CustomIntFloatDict
is invalid. The user knows what to do to fix this error.
Exception classes
IntFloatValueError
and
KeyValueConstructError
If no exception is thrown, that is, all
val
from the chained object are of types
int
or
float
, then they will be set using
__setitem__()
, and the method from the parent class will do everything for us
dict
, as shown below.
KeyValueConstructError Class
What happens if the user enters a type that is not a list or a tuple of keys and values?
Again, this example is a bit artificial, but it is handy to show you how you can use your own exception classes.
If the user does not specify the keys and values ββas a list or a tuple, an exception will be thrown
KeyValueConstructError
. The purpose of this exception is to inform the user that in order to write keys and values ββto an object
CustomIntFloatDict
, a list or a tuple must be specified in the
init
class constructor
CustomIntFloatDict
.
In the above example, as the second argument to the constructor
init
a lot was passed and an exception was thrown because of this
KeyValueConstructError
. The benefit of the displayed error message is that the displayed error message informs the user that the keys and values ββto be inserted must be reported as either a list or a tuple.
Again, when an exception is thrown, a KeyValueConstructError instance is created, and the key and values ββare passed as arguments to the KeyValueConstructError constructor. They are set as the values ββof the key and value attributes of KeyValueConstructError and are used in the __str__ method to generate a meaningful error message when the message is displayed on the screen.
Further, I even include the data types inherent in the objects added to the constructor
init
- I do this for clarity.
Setting key and value in CustomIntFloatDict
CustomIntFloatDict
inherits from
dict
. This means that it will function exactly like a dictionary, except in the places that we choose to selectively change its behavior.
__setitem__
Is a magic method called when setting a key and value in a dictionary. In our implementation,
setitem
we check that the value is of type
int
or
float
, and only after a successful check can it be set in the dictionary. If the check fails, you can use the exception class again
IntFloatValueError
. Here you can make sure that if we try to set a string
βbad_valueβ
as a value in the dictionary
test_4
, we will get an exception.
All of the code for this tutorial is shown below and posted on Github .
# , int float
class IntFloatValueError(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return '{} is invalid input, CustomIntFloatDict can only accept ' \
'integers and floats as its values'.format(self.value)
class KeyValueContructError(Exception):
def __init__(self, key, value):
self.key = key
self.value = value
def __str__(self):
return 'keys and values need to be passed as either list or tuple' + '\n' + \
' {} is of type: '.format(self.key) + str(type(self.key)) + '\n' + \
' {} is of type: '.format(self.value) + str(type(self.value))
class CustomIntFloatDict(dict):
empty_dict = {}
def __init__(self, key=None, value=None):
if key is None or value is None:
self.get_dict()
elif not isinstance(key, (tuple, list,)) or not isinstance(value, (tuple, list)):
raise KeyValueContructError(key, value)
else:
zipped = zip(key, value)
for k, val in zipped:
if not isinstance(val, (int, float)):
raise IntFloatValueError(val)
dict.__setitem__(self, k, val)
def get_dict(self):
return self.empty_dict
def __setitem__(self, key, value):
if not isinstance(value, (int, float)):
raise IntFloatValueError(value)
return dict.__setitem__(self, key, value)
#
# test_1 = CustomIntFloatDict()
# print(test_1)
# test_2 = CustomIntFloatDict({'a', 'b'}, [1, 2])
# print(test_2)
# test_3 = CustomIntFloatDict(('x', 'y', 'z'), (10, 'twenty', 30))
# print(test_3)
# test_4 = CustomIntFloatDict(('x', 'y', 'z'), (10, 20, 30))
# print(test_4)
# test_4['r'] = 1.3
# print(test_4)
# test_4['key'] = 'bad_value'
Conclusion
If you create your own exceptions, then working with the class becomes much more convenient. The exception class should have magic methods
init
and
str
that are automatically called during exception handling. It is entirely up to you what your own exception class will do. Among the methods shown are those that are responsible for inspecting an object and displaying an informative error message on the screen.
Be that as it may, exception classes make it much easier to handle any errors that arise!