Check the return value using PEP 380

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 to return 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

The 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

Check the return value using PEP 380
About the return value of pthread_mutex_init ()
About the return value of the histogram.
Check python code styles using pep8
Watch out for the return value of __len__
Generate a hash value using the HMAC method.
Check the status of your data using pandas_profiling
Return one-hot encoded features to the original category value
The story that the return value of tape.gradient () was None
Try using the Twitter API
Clone using the dd command
Try using the Twitter API
Try using the PeeringDB 2.0 API
Check the code with flake8
Outline the face using Dlib (1)
[Python] Check the installed libraries
Get and set the value of the dropdown menu using Python and Selenium
Check the drawing result using Plotly by embedding CodePen in Qiita
Finding the optimum value of a function using a genetic algorithm (Part 1)