互斥关键字参数的优雅模式?

发布于 2024-10-20 10:47:22 字数 1016 浏览 4 评论 0原文

有时在我的代码中,我有一个函数可以通过两种方式之一接受参数。比如:

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)

有没有更优雅的方法来做到这一点?如果 arg 可以通过三种方式之一出现怎么办?如果类型不同,我可以这样做:

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) 那样调用它,这很奇怪。

Sometimes in my code I have a function which can take an argument in one of two ways. Something like:

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)

Is there any more elegant way to do this? What if the arg could come in one of three ways? If the types are distinct I could do:

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))

But what if they are not? This alternative seems silly:

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

cause then you have to call it like func(mytype, isName=False) which is weird.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

鹿童谣 2024-10-27 10:47:22

如何使用命令调度模式之类的东西:

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

其中 type1type2 等是实际的 Python 类型(例如 int、float 等)。

How about using something like a command dispatch pattern:

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

where type1,type2, etc are actual python types (e.g. int, float, etc).

慕烟庭风 2024-10-27 10:47:22

听起来应该去 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_): ...

Sounds like it should go to https://codereview.stackexchange.com/

Anyway, keeping the same interface, I may try

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)

or simply create 2 functions?

def funcByName(name): ...
def funcByType(type_): ...
听风念你 2024-10-27 10:47:22

让它稍微短一点的一种方法是

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

One way to make it slightly shorter is

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)

I have not yet decided if I would call this "elegant".

Note that you should raise a TypeError if the wrong number of arguments was given, not a ValueError.

情未る 2024-10-27 10:47:22

不管它的价值如何,类似的事情在标准库中也发生过。例如,参见 gzip.py 中 GzipFile 的开头(此处显示已删除文档字符串):

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'

当然,这接受 filenamefileobj 关键字,并在中定义特定行为两者都收到的情况;但总体方法似乎几乎相同。

For whatever it's worth, similar kinds of things happen in the Standard Libraries; see, for example, the beginning of GzipFile in gzip.py (shown here with docstrings removed):

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'

Of course this accepts both filename and fileobj keywords and defines a particular behavior in the case that it receives both; but the general approach seems pretty much identical.

極樂鬼 2024-10-27 10:47:22

我使用装饰器:

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

请注意,这仅考虑作为关键字参数传递的非 None 值。例如,如果除其中一个之外的所有 kwarg_names 的值为 None,则仍可以传递多个 kwarg_names

为了允许不传递任何 kwargs,只需断言计数 <= 1。

I use a decorator:

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

Used as:

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

Note that this only accounts for non- None values that are passed as keyword arguments. E.g. multiple of the kwarg_names may still be passed if all but one of them have a value of None.

To allow for passing none of the kwargs simply assert that the count is <= 1.

巴黎夜雨 2024-10-27 10:47:22

听起来您正在寻找 函数重载,但尚未实现在 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'>}]

It sounds like you're looking for function overloading, which isn't implemented in Python 2. In Python 2, your solution is nearly as good as you can expect to get.

You could probably bypass the extra argument problem by allowing your function to process multiple objects and return a generator:

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)

Test:

>>> getObjByName = lambda a: {'Name': a}
>>> getObjByType = lambda a: {'Type': a}
>>> list(func('IntType'))
[{'Name': 'IntType'}]
>>> list(func(types.IntType))
[{'Type': <type 'int'>}]
同展鸳鸯锦 2024-10-27 10:47:22

内置的 sum() 可用于布尔表达式列表。在Python中,boolint的子类,在算术运算中,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'")

The built-in sum() can be used to on a list of boolean expressions. In Python, bool is a subclass of int, and in arithmetic operations, True behaves as 1, and False behaves as 0.

This means that this rather short code will test mutual exclusivity for any number of arguments:

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'")

Variations are also possible:

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'")
ι不睡觉的鱼゛ 2024-10-27 10:47:22

我偶尔也会遇到这个问题,很难找到一个容易通用的解决方案。假设我有更复杂的参数组合,这些参数组合由一组互斥的参数描述,并且希望为每个参数支持附加参数(其中一些可能是必需的,一些可能是可选的),如以下签名所示:

def func(mutex1: str, arg1: bool): ...
def func(mutex2: str): ...
def func(mutex3: int, arg1: Optional[bool] = None): ...

我将使用面向对象来将参数包装在一组描述符中(名称取决于参数的业务含义),然后可以通过 pydantic

from typing import Optional
from pydantic import BaseModel, Extra

# Extra.forbid ensures validation error if superfluous arguments are provided
class BaseDescription(BaseModel, extra=Extra.forbid):
    pass  # Arguments common to all descriptions go here

class Description1(BaseDescription):
    mutex1: str
    arg1: bool

class Description2(BaseDescription):
    mutex2: str

class Description3(BaseDescription):
    mutex3: int
    arg1: Optional[bool]

您可以使用工厂实例化这些描述:

class DescriptionFactory:
    _class_map = {
        'mutex1': Description1,
        'mutex2': Description2,
        'mutex3': Description3
    }
    
    @classmethod
    def from_kwargs(cls, **kwargs) -> BaseDescription:
        kwargs = {k: v for k, v in kwargs.items() if v is not None}
        set_fields = kwargs.keys() & cls._class_map.keys()
        
        try:
            [set_field] = set_fields
        except ValueError:
            raise ValueError(f"exactly one of {list(cls._class_map.keys())} must be provided")
        
        return cls._class_map[set_field](**kwargs)
    
    @classmethod
    def validate_kwargs(cls, func):
        def wrapped(**kwargs):
            return func(cls.from_kwargs(**kwargs))
        return wrapped

然后您可以像这样包装实际的函数实现,并使用类型检查来查看提供了哪些参数:

@DescriptionFactory.validate_kwargs
def func(desc: BaseDescription):
    if isinstance(desc, Description1):
        ...  # use desc.mutex1 and desc.arg1
    elif isinstance(desc, Description2):
        ...  # use desc.mutex2
    ...  # etc.

并调用 func(mutex1='' 、 arg1=True)func(mutex2='')func(mutex3=123) 等等。

这并不是整体上较短的代码,但它根据您的规范以非常描述性的方式执行参数验证,在验证失败时引发有用的 pydantic 错误,并在函数实现的每个分支中产生准确的静态类型。

请注意,如果您使用的是 Python 3.10+,结构模式匹配可以简化其中的某些部分。

I occasionally run into this problem as well, and it is hard to find an easily generalisable solution. Say I have more complex combinations of arguments that are delineated by a set of mutually exclusive arguments and want to support additional arguments for each (some of which may be required and some optional), as in the following signatures:

def func(mutex1: str, arg1: bool): ...
def func(mutex2: str): ...
def func(mutex3: int, arg1: Optional[bool] = None): ...

I would use object orientation to wrap the arguments in a set of descriptors (with names depending on the business meaning of the arguments), which can then be validated by something like pydantic:

from typing import Optional
from pydantic import BaseModel, Extra

# Extra.forbid ensures validation error if superfluous arguments are provided
class BaseDescription(BaseModel, extra=Extra.forbid):
    pass  # Arguments common to all descriptions go here

class Description1(BaseDescription):
    mutex1: str
    arg1: bool

class Description2(BaseDescription):
    mutex2: str

class Description3(BaseDescription):
    mutex3: int
    arg1: Optional[bool]

You could instantiate these descriptions with a factory:

class DescriptionFactory:
    _class_map = {
        'mutex1': Description1,
        'mutex2': Description2,
        'mutex3': Description3
    }
    
    @classmethod
    def from_kwargs(cls, **kwargs) -> BaseDescription:
        kwargs = {k: v for k, v in kwargs.items() if v is not None}
        set_fields = kwargs.keys() & cls._class_map.keys()
        
        try:
            [set_field] = set_fields
        except ValueError:
            raise ValueError(f"exactly one of {list(cls._class_map.keys())} must be provided")
        
        return cls._class_map[set_field](**kwargs)
    
    @classmethod
    def validate_kwargs(cls, func):
        def wrapped(**kwargs):
            return func(cls.from_kwargs(**kwargs))
        return wrapped

Then you can wrap your actual function implementation like this and use type checking to see which arguments were provided:

@DescriptionFactory.validate_kwargs
def func(desc: BaseDescription):
    if isinstance(desc, Description1):
        ...  # use desc.mutex1 and desc.arg1
    elif isinstance(desc, Description2):
        ...  # use desc.mutex2
    ...  # etc.

and call as func(mutex1='', arg1=True), func(mutex2=''), func(mutex3=123) and so on.

This is not overall shorter code, but it performs argument validation in a very descriptive way according to your specification, raises useful pydantic errors when validation fails, and results in accurate static types in each branch of the function implementation.

Note that if you're using Python 3.10+, structural pattern matching could simplify some parts of this.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文