When posting the sample code, post the result of execution in the following execution environment. We will try to provide it in the form that is as error-free as possible, but please forgive us if it does not work or is wrong.
What do you return if something unexpected happens when you process it in a function?
I used to return None
until I knew this. For example, the following code.
#Bad example (returning None when unexpected)
from datetime import datetime
def parse_datetimes(datetime_string_list):
result = []
for datetime_string in datetime_string_list:
try:
result.append(datetime.strptime(datetime_string, "%Y-%m-%d"))
except (ValueError, TypeError):
return None
return result
>>> print(parse_datetimes(["2020-09-22"]))
[datetime.datetime(2020, 9, 22, 0, 0)]
>>> print(parse_datetimes([]))
[]
>>> print(parse_datetimes(["hoge"]))
None
This function accepts a list of strings representing the date as input, and returns a list converted to datetime type if parsing succeeds, and None
if it fails.
At first glance it looks good, but there are two problems with this.
None
(or intentionally ignores it), the program can do the next thing., it will be treated the same not only in the case of
Nonebut also in the case of the empty list
[]`, and it is easy to cause a bug.To avoid these problems, ** throw an exception if something unexpected happens inside the function **.
#Good example (throwing an exception when unexpected)
from datetime import datetime
class MyError(Exception):
def __init__(self, message):
self.message = message
def parse_datetimes(datetime_string_list):
result = []
for datetime_string in datetime_string_list:
try:
result.append(datetime.strptime(datetime_string, "%Y-%m-%d"))
except (ValueError, TypeError):
raise MyError(f"The value entered is invalid: {datetime_string}") #Throw an exception
return result
>>> print(parse_datetimes(["2020-09-22"]))
[datetime.datetime(2020, 9, 22, 0, 0)]
>>> print(parse_datetimes([]))
[]
>>> print(parse_datetimes(["hoge"]))
...
Traceback (most recent call last):
...
__main__.MyError:The value entered is invalid: hoge
If it is implemented to throw an exception as described above, processing will not continue as it is unless the exception MyError
is caught, and None
and the empty list []
are the same. It's safe because there is no fear of handling it.
[]
or an empty dictionary {}
as the default argumentIf you want to use a value if it is specified in the argument, and use the default value if it is not specified, it is convenient to set the default argument. For example, the following code.
#This is a bad example
def square(value, result_list=[]):
result_list.append(value ** 2)
return result_list
This function returns the square of value
added to result_list
.
As for the feelings of the person who implemented this,
result_list
, I want you to put the result in that list and return it.result_list
, I want you to return the result in an empty list.I think you are thinking.
# result_If you specify list, the result will be returned in that list.
>>> result_list = [1, 4]
>>> result_list = square(3, result_list)
>>> print(result_list)
[1, 4, 9]
# result_If list is not specified, the result will be returned in the empty list.
>>> result_list = square(3)
>>> print(result_list)
[9]
It looks like there's no problem at all, right? But after this, something strange happens.
>>> result_list = square(1)
>>> print(result_list)
[9, 1] #that?[1]Should be returned? ??
>>> result_list = square(2)
print(result_list)
[9, 1, 4] #that? Why are there 3 values? ??
so. If you don't know it, you will end up with such a mysterious behavior.
In fact, ** [default arguments are evaluated only once when the module is loaded, not each time the function is called](https://docs.python.org/ja/3/ faq / programming.html # why-are-default-values-shared-between-objects) **. Therefore, if you specify an empty list []
as the default argument, the empty list will be append`ed more and more, and the contents of the list will increase each time you call it as described above. ..
It's still good if you know this and use it, but it's not a behavior that everyone knows, so it's best to avoid specifying empty lists, empty dictionaries, etc. as default arguments. Instead, write:
#Good example
def square(value, result_list=None):
if result_list is None:
result_list = [] #By initializing here, it will be append to the empty list every time.
result_list.append(value ** 2)
return result_list
If implemented as above, if result_list
is not specified, the empty list will be returned with the result.
>>> result_list = square(3)
>>> print(result_list)
[9]
>>> result_list = square(1)
>>> print(result_list)
[1]
>>> result_list = square(2)
>>> print(result_list)
[4]
By the way, depending on the IDE, it may issue a warning, so be sure to respond if there is a warning. PyCharm issued the following warning.
Suddenly, do you know what the code below means?
def safe_division(numerator, denominator, /, hoge, *, ignore_overflow, ignore_zero_division):
...
You might be wondering, "Why are the arguments/
and*
written?"
This is ["Position-only arguments (on the left side of/
)" and "Keyword-only arguments (on the right side of*
) added from Python 3.8"](https://docs.python.org/ja/ It is written using 3 / glossary.html # term-parameter).
What does that mean?
/
(numerator
and denominator
in the above example) are ** position-only arguments **.
--Position-only arguments can only be specified as position arguments when calling a function (in other words, they cannot be specified as keyword arguments).
--In other words
--This call is OK: safe_division (3, 2, ...)
--This call is NG: safe_division (numerator = 3, denominator = 2, ...)
*
(ʻignore_overflow and ʻignore_zero_division
in the above example) are ** keyword-only arguments **.
--Keyword-only arguments can only be specified as keyword arguments when calling a function (in other words, they cannot be specified as positional arguments).
--In other words
--This call is OK: safe_division (..., ignore_overflow = True, ignore_zero_division = False)
--This call is NG: safe_division (..., True, False)
/
and on the left side of *
(hoge
in the above example) can be specified as either positional arguments or keyword arguments.
――In other words, you can call as before
--This call is OK: safe_division (...," a ", ...)
--This call is also OK: safe_division (..., hoge =" a ", ...)
I think the advantages of using position-only arguments and keyword-only arguments are as follows.
(denominator = 2, numerator = 3, ...)
.
――In addition, you can reduce the dependence on the argument name because you can not specify using the argument name.
--That is, even if the argument name changes from denominator
to denom
, the caller does not need to be modified. is
True! You can force it to recognize that and specify the argument --So, you can reduce the common mistake of "I intended to set ʻignore_overflow
to True
, but I specified True
for ʻignore_zero_division`!"Writing /
or *
in the argument may seem strange at first, but as mentioned above, it has advantages, so it is better to use it as needed. However, it will not work if it is less than Python 3.8, so be careful there!
Based on the contents of Chapter 3
None
.[]
or an empty dictionary {}
as the default argument of the function will lead to unexpected bugs and should be avoided.Recommended Posts