This time, use PEP 380 to make yield None
equivalent to return
, and the return value should be a non-None value. Introducing the decorator to check.
PEP 380 is a new specification added in Python 3.3, which is a [yield from] for multi-stage generators. The main purpose is to add Syntax, but this time, how many were added in addition to the yield from syntax. I wrote a decorator that makes yield None
equivalent to return
by using that function.
Most people
What is the relationship between making
yield None
equivalent toreturn
and checking that the return value is a non-None value?
I think you have the question, so let me give you one simple function as an example.
python
def get_version(self, config_path=None):
if config_path is None:
config_path = self.get_default_config_path()
config = self.parse_config(config_path)
program_name = config.get("program_name")
match = RE_VERSION.match(program_name)
return match.group("version")
It's hard to understand because it's only a part, but this get_version ()
gets the path of the configuration file, parses the file, and extracts the version part as a regular expression from the item " program_name "
The function to return.
I'm used to regular expressions, so I don't hesitate to use them, but if I use regular expressions for this kind of processing,
Such code is not Pythonic!
Please note that Pythonista people may get angry.
Well, this code doesn't check the return value at all, as you can see at a glance.
That's a problem, so
None
when an error occursThe code below is modified to check the return value where necessary in the rule.
python
def get_version(self, config_path=None):
if config_path is None:
config_path = self.get_default_config_path()
if config_path is None:
return None
config = self.parse_config(config_path)
if config is None:
return None
program_name = config.get("program_name", None)
if program_name is None:
return None
match = RE_PROGRAM_VERSION.match(program_name)
if match is None:
return None
return match.group("version")
As a result, the code is full of ʻif xxx is None: return None`, and the function, which was 7 lines, is now 15 lines.
I've always wanted to do something about this, so I wrote a decorator called yield_none_becomes_return ()
using the features added in PEP 380 as mentioned earlier.
Below is the code with yield_none_becomes_return ()
applied to the first version.
python
@yield_none_becomes_return
def get_version(self, config_path=None):
if config_path is None
config_path = yield self.get_default_config_path()
config = yield self.parse_config(config_path)
program_name = yield config.get("program_name")
match = yield RE_VERSION.match(program_name)
return match.group("version")
The code, which was 15 lines in the previous version, is now 8 lines.
The only difference from the first version is the addition of decorators and yield
, nothing else has changed.
The process of yield_none_becomes_return ()
is simple. For example
python
config_path = yield self.get_default_config_path()
In the case of, if the return value of self.get_default_config_path ()
is None
, yield None
is established, so immediately return
, if it is not None
, the return value is returned to config_path
. Substitute and continue processing as it is.
If there is no argument in the decorator, it will be equivalent to return
, so None
will be returned,
python
@yield_none_becomes_return("")
It is also possible to return an arbitrary value when yield None
is satisfied by giving an argument like.
However, if you want to return an object that evaluates with callable ()
and is true,
python
@yield_none_becomes_return(value=function)
Please describe as. This is a limitation due to various reasons.
The only other thing to note is that StopIteration ()
does not propagate to the caller of the decorated function.
The source for yield_none_becomes_return ()
is below, so please let us know if you find any other problems.
ynbr.py
#!/usr/bin/env python3
# vim:fileencoding=utf-8
# Copyright (c) 2014 Masami HIRATA <[email protected]>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import sys
if sys.version_info < (3, 3): # pragma: no cover
raise ImportError("Python >= 3.3 is required")
from functools import partial, wraps
from inspect import isgeneratorfunction
DEFAULT = object()
__all__ = ['yield_none_becomes_return']
def yield_none_becomes_return(function=DEFAULT, *, value=DEFAULT):
"""This decorator changes the yield statement to None checker for
avoiding "if xxx is None: return" statements
For example:
# without this decorator:
def get_version(self, config_path=None):
if config_path is None:
config_path = self.get_default_config_path()
if config_path is None:
return ""
config = self.parse_config(config_path)
if config is None:
return ""
program_name = config.get("program_name")
if program_name is None:
return ""
match = RE_PROGRAM_VERSION.match(program_name)
if match is None:
return ""
return match.group("version")
# with this decorator:
@yield_none_becomes_return("")
def get_version(self, config_path=None):
if config_path is None:
config_path = yield self.get_default_config_path()
config = yield self.parse_config(config_path)
program_name = yield config.get("program_name")
match = yield RE_VERSION.match(program_name)
return match.group("version")
"""
if not isgeneratorfunction(function):
if function is DEFAULT:
if value is DEFAULT:
# @yield_none_becomes_return() # CORRECT
value = None
else:
if callable(function):
raise TypeError("@yield_none_becomes_return is used only " +
"for generator functions")
if value is not DEFAULT:
# @yield_none_becomes_return("B", value="C") # WRONG
raise TypeError("yield_none_becomes_return() takes " +
"1 argument but 2 were given.")
# @yield_none_becomes_return("A") # CORRECT
value = function
return partial(yield_none_becomes_return, value=value)
else:
if value is DEFAULT:
value = None
@wraps(function)
def _yield_none_becomes_return(*args, **kwargs):
generator = function(*args, **kwargs)
try:
return_value = next(generator)
while True:
if return_value is not None:
return_value = generator.send(return_value)
else:
return value
except StopIteration as exception:
return exception.value
return _yield_none_becomes_return
Recommended Posts