Элегантный шаблон для взаимоисключающих аргументов ключевых слов? - PullRequest
9 голосов
/ 03 марта 2011

Иногда в моем коде есть функция, которая может принимать аргумент одним из двух способов. Что-то вроде:

def func(objname=None, objtype=None):
    if objname is not None and objtype is not None:
        raise ValueError("only 1 of the ways at a time")
    if objname is not None:
        obj = getObjByName(objname)
    elif objtype is not None:
        obj = getObjByType(objtype)
    else:
        raise ValueError("not given any of the ways")

    doStuffWithObj(obj)

Есть ли более элегантный способ сделать это? Что если арг может прийти одним из трех способов? Если типы различны, я мог бы сделать:

def func(objnameOrType):
    if type(objnameOrType) is str:
        getObjByName(objnameOrType)
    elif type(objnameOrType) is type:
        getObjByType(objnameOrType)
    else:
        raise ValueError("unk arg type: %s" % type(objnameOrType))

Но что, если их нет? Эта альтернатива кажется глупой:

def func(objnameOrType, isName=True):
    if isName:
        getObjByName(objnameOrType)
    else:
        getObjByType(objnameOrType)

потому что тогда вы должны назвать это как func(mytype, isName=False), что странно.

Ответы [ 7 ]

6 голосов
/ 03 марта 2011

Как насчет использования чего-то вроде шаблона диспетчеризации команд:

def funct(objnameOrType):
   dispatcher = {str: getObjByName,
                 type1: getObjByType1,
                 type2: getObjByType2}
   t = type(objnameOrType)
   obj = dispatcher[t](objnameOrType)
   doStuffWithObj(obj)

, где type1, type2 и т. Д. - фактические типы Python (например, int, float и т. Д.).

3 голосов
/ 03 марта 2011

Что бы это ни стоило, подобные вещи происходят в стандартных библиотеках;см., например, начало GzipFile в gzip.py (показанном здесь без удаленных строк документации):

class GzipFile:
    myfileobj = None
    max_read_chunk = 10 * 1024 * 1024   # 10Mb
    def __init__(self, filename=None, mode=None,
                 compresslevel=9, fileobj=None):
        if mode and 'b' not in mode:
            mode += 'b'
        if fileobj is None:
            fileobj = self.myfileobj = __builtin__.open(filename, mode or 'rb')
        if filename is None:
            if hasattr(fileobj, 'name'): filename = fileobj.name
            else: filename = ''
        if mode is None:
            if hasattr(fileobj, 'mode'): mode = fileobj.mode
            else: mode = 'rb'

Конечно, это принимает и ключевые слова filename и fileobj и определяет конкретное поведение вдело, что он получает оба;но общий подход кажется почти идентичным.

3 голосов
/ 03 марта 2011

Звучит так, как будто должно идти https://codereview.stackexchange.com/

В любом случае, сохраняя тот же интерфейс, я могу попробовать

arg_parsers = {
  'objname': getObjByName,
  'objtype': getObjByType,
  ...
}
def func(**kwargs):
  assert len(kwargs) == 1 # replace this with your favorite exception
  (argtypename, argval) = next(kwargs.items())
  obj = arg_parsers[argtypename](argval) 
  doStuffWithObj(obj)

или просто создать 2 функции?

def funcByName(name): ...
def funcByType(type_): ...
3 голосов
/ 03 марта 2011

Один способ сделать его немного короче -

def func(objname=None, objtype=None):
    if [objname, objtype].count(None) != 1:
        raise TypeError("Exactly 1 of the ways must be used.")
    if objname is not None:
        obj = getObjByName(objname)
    else: 
        obj = getObjByType(objtype)

Я еще не решил, назову ли я это "элегантным".

Обратите внимание, что вы должны поднять TypeError, если было указано неправильное количество аргументов, а не ValueError.

1 голос
/ 01 мая 2018

Я использую декоратор:

from functools import wraps

def one_of(kwarg_names):
    # assert that one and only one of the given kwarg names are passed to the decorated function
    def inner(f):
        @wraps(f)
        def wrapped(*args, **kwargs):
            count = 0
            for kw in kwargs:
                if kw in kwarg_names and kwargs[kw] is not None:
                    count += 1

            assert count == 1, f'exactly one of {kwarg_names} required, got {kwargs}'

            return f(*args, **kwargs)
        return wrapped
    return inner

Используется как:

@one_of(['kwarg1', 'kwarg2'])
def my_func(kwarg1='default', kwarg2='default'):
    pass

Обратите внимание, что это учитывает только не-1007 * значения, которые передаются в качестве аргументов ключевых слов. Например. кратное kwarg_names может быть передано, если все, кроме одного, имеют значение None.

Чтобы пропустить ни одного из kwargs, просто утверждайте, что счет <= 1. </p>

0 голосов
/ 03 марта 2019

Встроенный sum() может использоваться в списке логических выражений.В Python bool является подклассом int, а в арифметических операциях True ведет себя как 1, а False ведет себя как 0.

Это означает, что этот довольно короткий код будет проверять взаимноеисключительность для любого количества аргументов:

def do_something(a=None, b=None, c=None):
    if sum([a is not None, b is not None, c is not None]) != 1:
        raise TypeError("specify exactly one of 'a', 'b', or 'c'")

Возможны также варианты:

def do_something(a=None, b=None, c=None):
    if sum([a is not None, b is not None, c is not None]) > 1:
        raise TypeError("specify at most one of 'a', 'b', or 'c'")
0 голосов
/ 03 марта 2011

Похоже, вы ищете перегрузка функции , которая не реализована в Python 2. В Python 2 ваше решение почти так же хорошо, как вы ожидаете.

Вероятно, вы могли бы обойти проблему дополнительный аргумент , позволив вашей функции обрабатывать несколько объектов и возвращать генератор:

import types

all_types = set([getattr(types, t) for t in dir(types) if t.endswith('Type')])

def func(*args):
    for arg in args:
        if arg in all_types:
            yield getObjByType(arg)
        else:
            yield getObjByName(arg)

Тест:

>>> getObjByName = lambda a: {'Name': a}
>>> getObjByType = lambda a: {'Type': a}
>>> list(func('IntType'))
[{'Name': 'IntType'}]
>>> list(func(types.IntType))
[{'Type': <type 'int'>}]
...