今回は PEP 380 を使って yield None
を return
相当にして、戻り値が非None値であることをチェックするデコレーターを紹介します。
PEP 380 というのは Python 3.3 で追加された新しい仕様で、ジェネレーターを多段にするための yield from 構文の追加が主な目的なのですが、今回は yield from 構文以外に追加されたいくつかの機能を使うことで yield None
を return
相当にするデコレーターを書いてみました。
ほとんどの方は
yield None
をreturn
相当にすることと、戻り値が非None値であることのチェックにどういう関係があるのか?
という疑問をお持ちだと思いますので、例として簡単な関数を一つ挙げてみます。
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")
一部だけですので分かりにくいと思いますが、この get_version()
は設定ファイルのパスを取得してからそのファイルをパースして "program_name"
という項目からバージョン部分を正規表現で取り出して返す関数です。
私は正規表現に慣れているので特に遠慮せず使っていますが、この程度の処理に正規表現を使うと
そんなコードは Pythonic ではない!
とPythonistaの方々に怒られてしまうかもしれませんのでご注意ください。
さて、このコードはぱっと見で分かるように戻り値のチェックを全くしていません。
それはさすがに問題ですので、
- エラー発生時には
None
を返す
というルールで必要な箇所で戻り値をチェックするように修正したのが下記のコードです。
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")
結果として、コードが if xxx is None: return None
だらけになって、7行だった関数が15行になりました。
このようになってしまうことをどうにかしたいと前々から思っていたので、先に述べたように PEP 380 で追加された機能を使って書いたのが、yield_none_becomes_return()
というデコレーターです。
最初のバージョンに、yield_none_becomes_return()
を適用したコードが下記になります。
@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")
直前のバージョンで15行だったコードが8行になりました。
最初のバージョンとの違いはデコレーターと yield
の追加だけで、他は一切変わっていません。
yield_none_becomes_return()
の処理は単純です。例えば、
config_path = yield self.get_default_config_path()
の場合、self.get_default_config_path()
の戻り値が None
だった場合は yield None
が成立するので即 return
、None
でなかった場合は、config_path
に戻り値を代入してそのまま処理を継続させます。
デコレーターに引数がなければ return
相当になりますので None
を返しますが、
@yield_none_becomes_return("")
のように引数を与えることで、yield None
が成立した場合に任意の値を返すようにすることも可能です。
ただし、callable()
で評価して真になるオブジェクトを返したい場合は、
@yield_none_becomes_return(value=function)
のように記述してください。これは諸々の事情による制限です。
他に注意が必要なのは、デコレートした関数の呼び出し元に StopIteration()
が伝播しないことぐらいでしょうか。
yield_none_becomes_return()
のソースは下記になりますので、他に何か問題を見つけた場合はぜひお知らせください。
# !/usr/bin/env python3
# vim:fileencoding=utf-8
# Copyright (c) 2014 Masami HIRATA <msmhrt@gmail.com>
# 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