如何使用互斥参数创建 Python 函数?
How do I make a Python function with mutually exclusive arguments?
我有一个 Python class 需要接受两个互斥参数之一。如果参数不是排他性的,(即:如果两者都给出或都不给出),则应引发错误。
class OrgLocation:
__init__(self, location_num=None, location_path=None):
"""location_num & location_path are mutually exclusive"""
在大多数情况下,最好的选择是制作两个单独的 classes。但是,我正在使用 an external API,其中 要求 这两个属性相互排斥。
要求:
<OrgLocation LocationPathName="ROOT/BU/DIV/SL/DEPT/JOB" LocationNum="1234"/>
回复:
<Error Message="Use either LocationNum or LocationPathName but not both." ErrorCode="1186">
类似的问题似乎表明 argparse
可用于命令行界面中的互斥参数,但我不确定如何将其应用于 class 构造函数
如何创建具有互斥参数的 Python 函数?
您可能想在 init 方法中创建一个测试,但更好的问题可能是...为什么?
if location_num is not None and location_path is not None:
raise TheseParametersAreMutuallyExclusiveError()
为什么要制作一个具有多种用途的 class?为什么不创建单独的 classes?
除了,Python中的一种常见方式是接受单个参数,然后回避它:
class Location:
__init__(self, location):
"""location_num & location_path are mutually exclusive"""
try:
x = self.locationArray[location] #location is a num?
except TypeError:
x = self.locationDict[location] #location is a string?
可能还有另一个例外。如果你想使用 argparse
,这可能只对两个参数有点矫枉过正,但可以很好地扩展:
import argparse
class Bla:
parser = argparse.ArgumentParser(prog='Class Bla init')
path_group = parser.add_mutually_exclusive_group(required=True)
path_group.add_argument('--num',nargs=1,type=int)
path_group.add_argument('--path',nargs=1,type=str)
def __init__(self,**kwargs):
args=self.parser.parse_args(sum(
zip(map(
lambda x: '--'+x,kwargs.keys()),
map(str,kwargs.values())),()))
#Bla(x='abc')
#Bla(num='abc')
Bla(path='abc')
Bla(path='abc',num=3)
从上到下的结果:
usage: Class Bla init [-h] (--num NUM | --path PATH)
bla.py: error: one of the arguments --num --path is required
usage: Class Bla init [-h] (--num NUM | --path PATH)
bla.py: error: argument --num: invalid int value: 'abc'
<__main__.Bla object at 0x7fd070652160>
usage: Class Bla init [-h] (--num NUM | --path PATH)
bla.py: error: argument --num: not allowed with argument --path
这也很酷,因为 Bla(help='anything')
实际上会打印用法(并退出)。这是为了回答有关 argparse
的具体问题,但需要明确的是,@Ivonet 提供了我将实际用于您的确切示例的答案。
你想做的很简单:
class Location:
__init__(self, location_num=None, location_path=None):
"""location_num & location_path are mutually exclusive"""
if location_num is not None and location_path is not None:
raise ValueError("should have location_num or location_path, but not both")
elif location_num:
#create location from int
elif location_str:
#create location from str
但它被认为是不正确的 python。您应该创建备用构造函数作为类方法,而不是:
class Location:
def __init__(self, parsed_location):
#create location
@classmethod
def from_int(cls, location_int):
return cls(parse_int(location_int))
@classmethod
def from_str(cls, location_str):
return cls(parse_str(location_str))
有关更深入的示例,请参阅 What is a clean, pythonic way to have multiple constructors in Python?。
我认为装饰器是执行此操作的一种很好且富有表现力的方式。
我确信我的实现可以改进,但它有效,而且我认为它使用法非常可读:
class MutuallyExclusiveArgsError(Exception):
def __init__(self, groups):
err = f"These groups or arguments are mutually exclusive: {','.join(str(tuple(g)) for g in groups)}"
super().__init__(err)
def exclusive_args(*args):
import attr
import functools
from typing import Callable,Set,Union,Iterable
@attr.s
class _inner:
_arg_groups_conv = lambda val: {arg: group for group in {frozenset([s]) if isinstance(s, str) else s for s in val} for arg in group}
func : Callable = attr.ib()
arg_groups : Set[Union[str,Iterable]] = attr.ib(converter=_arg_groups_conv, kw_only=True)
def __attrs_post_init_(self):
functools.update_wrapper(self, self.func)
def __call__(self, *args, **kwargs):
groups = {self.arg_groups[kw] for kw in kwargs}
if len(groups) > 1:
raise MutuallyExclusiveArgsError(groups)
self.func(*args, **kwargs)
return functools.partial(_inner, arg_groups=args)
用法如下所示:
@exclusive_args("one", "two")
def ex(*, one=None, two=None):
print(one or two)
ex(one=1, two=2)
---------------------------------------------------------------------------
MutuallyExclusiveArgsError Traceback (most recent call last)
<ipython-input-38-0f1d142483d2> in <module>
----> 1 ex(one=1, two=2)
<ipython-input-36-c2ff5f47260f> in __call__(self, *args, **kwargs)
21 groups = {self.arg_groups[kw] for kw in kwargs}
22 if len(groups) > 1:
---> 23 raise MutuallyExclusiveArgsError(groups)
24 self.func(*args, **kwargs)
25 return functools.partial(_inner, arg_groups=args)
MutuallyExclusiveArgsError: These groups or arguments are mutually exclusive: ('two',),('one',)
ex(one=1)
1
ex(two=2)
2
或者像这样:
@exclusive_args("one", ("two","three"))
def ex(*, one=None, two=None, three=None):
print(one, two, three)
ex(one=1)
1 None None
ex(two=1)
None 1 None
ex(three=1)
None None 1
ex(two=1, three=2)
None 1 2
ex(one=1, two=2)
---------------------------------------------------------------------------
MutuallyExclusiveArgsError Traceback (most recent call last)
<ipython-input-46-0f1d142483d2> in <module>
----> 1 ex(one=1, two=2)
<ipython-input-36-c2ff5f47260f> in __call__(self, *args, **kwargs)
21 groups = {self.arg_groups[kw] for kw in kwargs}
22 if len(groups) > 1:
---> 23 raise MutuallyExclusiveArgsError(groups)
24 self.func(*args, **kwargs)
25 return functools.partial(_inner, arg_groups=args)
MutuallyExclusiveArgsError: These groups or arguments are mutually exclusive: ('one',),('two', 'three')
ex(one=1,three=3)
---------------------------------------------------------------------------
MutuallyExclusiveArgsError Traceback (most recent call last)
<ipython-input-47-0dcb487cba71> in <module>
----> 1 ex(one=1,three=3)
<ipython-input-36-c2ff5f47260f> in __call__(self, *args, **kwargs)
21 groups = {self.arg_groups[kw] for kw in kwargs}
22 if len(groups) > 1:
---> 23 raise MutuallyExclusiveArgsError(groups)
24 self.func(*args, **kwargs)
25 return functools.partial(_inner, arg_groups=args)
MutuallyExclusiveArgsError: These groups or arguments are mutually exclusive: ('one',),('two', 'three')
虽然有点 hacky,但您可以按如下方式使用 XOR 运算符:
class OrgLocation:
def __init__(self, location_num=None, location_path=None):
"""location_num & location_path are mutually exclusive"""
assert (location_num is None) ^ bool(location_path is None), "location_num and location_path are mutually exclussive"
print("OK")
我知道这是一个老问题,但我还没有看到有人使用我采用的简单方法。这个例子是在一个普通的旧方法中,但它在 class:
的 __init__
方法中同样有效
def circle_area(radius=None, diameter=None, circumference=None):
# check for mutually-exclusive parameters
number_of_options_specified = len([opt for opt in [circumference, diameter, radius] if opt is not None])
if number_of_options_specified != 1:
raise ValueError(f"Exactly one of radius ({radius}) / diameter ({diameter}) / circumference ({circumference}) must be specified")
# calculate
pi = 3.14
if radius is not None:
area = pi * radius**2
if diameter is not None:
area = pi * (diameter/2.0)**2
if circumference is not None:
area = (circumference**2)/(4.0*pi)
return area
这里是我基于构建的互斥守卫:
# Mutually Exclusive function predicate
# Returns True if no. of args that are True or not None is > 1
def ismuex(*a):
return not bool(sum(map(lambda v: bool(v if isinstance(v, bool) else not v is None), a)) > 1)
用法:
def my_func(arg_1, arg_2, arg3):
assert ismuex(arg_1, arg_2, arg3), \
"arguments arg_1, arg_2 and arg_3 are mutually exclusive"
#....
我有一个 Python class 需要接受两个互斥参数之一。如果参数不是排他性的,(即:如果两者都给出或都不给出),则应引发错误。
class OrgLocation:
__init__(self, location_num=None, location_path=None):
"""location_num & location_path are mutually exclusive"""
在大多数情况下,最好的选择是制作两个单独的 classes。但是,我正在使用 an external API,其中 要求 这两个属性相互排斥。
要求:
<OrgLocation LocationPathName="ROOT/BU/DIV/SL/DEPT/JOB" LocationNum="1234"/>
回复:
<Error Message="Use either LocationNum or LocationPathName but not both." ErrorCode="1186">
类似的问题似乎表明 argparse
可用于命令行界面中的互斥参数,但我不确定如何将其应用于 class 构造函数
如何创建具有互斥参数的 Python 函数?
您可能想在 init 方法中创建一个测试,但更好的问题可能是...为什么?
if location_num is not None and location_path is not None:
raise TheseParametersAreMutuallyExclusiveError()
为什么要制作一个具有多种用途的 class?为什么不创建单独的 classes?
除了
class Location:
__init__(self, location):
"""location_num & location_path are mutually exclusive"""
try:
x = self.locationArray[location] #location is a num?
except TypeError:
x = self.locationDict[location] #location is a string?
可能还有另一个例外。如果你想使用 argparse
,这可能只对两个参数有点矫枉过正,但可以很好地扩展:
import argparse
class Bla:
parser = argparse.ArgumentParser(prog='Class Bla init')
path_group = parser.add_mutually_exclusive_group(required=True)
path_group.add_argument('--num',nargs=1,type=int)
path_group.add_argument('--path',nargs=1,type=str)
def __init__(self,**kwargs):
args=self.parser.parse_args(sum(
zip(map(
lambda x: '--'+x,kwargs.keys()),
map(str,kwargs.values())),()))
#Bla(x='abc')
#Bla(num='abc')
Bla(path='abc')
Bla(path='abc',num=3)
从上到下的结果:
usage: Class Bla init [-h] (--num NUM | --path PATH)
bla.py: error: one of the arguments --num --path is required
usage: Class Bla init [-h] (--num NUM | --path PATH)
bla.py: error: argument --num: invalid int value: 'abc'
<__main__.Bla object at 0x7fd070652160>
usage: Class Bla init [-h] (--num NUM | --path PATH)
bla.py: error: argument --num: not allowed with argument --path
这也很酷,因为 Bla(help='anything')
实际上会打印用法(并退出)。这是为了回答有关 argparse
的具体问题,但需要明确的是,@Ivonet 提供了我将实际用于您的确切示例的答案。
你想做的很简单:
class Location:
__init__(self, location_num=None, location_path=None):
"""location_num & location_path are mutually exclusive"""
if location_num is not None and location_path is not None:
raise ValueError("should have location_num or location_path, but not both")
elif location_num:
#create location from int
elif location_str:
#create location from str
但它被认为是不正确的 python。您应该创建备用构造函数作为类方法,而不是:
class Location:
def __init__(self, parsed_location):
#create location
@classmethod
def from_int(cls, location_int):
return cls(parse_int(location_int))
@classmethod
def from_str(cls, location_str):
return cls(parse_str(location_str))
有关更深入的示例,请参阅 What is a clean, pythonic way to have multiple constructors in Python?。
我认为装饰器是执行此操作的一种很好且富有表现力的方式。 我确信我的实现可以改进,但它有效,而且我认为它使用法非常可读:
class MutuallyExclusiveArgsError(Exception):
def __init__(self, groups):
err = f"These groups or arguments are mutually exclusive: {','.join(str(tuple(g)) for g in groups)}"
super().__init__(err)
def exclusive_args(*args):
import attr
import functools
from typing import Callable,Set,Union,Iterable
@attr.s
class _inner:
_arg_groups_conv = lambda val: {arg: group for group in {frozenset([s]) if isinstance(s, str) else s for s in val} for arg in group}
func : Callable = attr.ib()
arg_groups : Set[Union[str,Iterable]] = attr.ib(converter=_arg_groups_conv, kw_only=True)
def __attrs_post_init_(self):
functools.update_wrapper(self, self.func)
def __call__(self, *args, **kwargs):
groups = {self.arg_groups[kw] for kw in kwargs}
if len(groups) > 1:
raise MutuallyExclusiveArgsError(groups)
self.func(*args, **kwargs)
return functools.partial(_inner, arg_groups=args)
用法如下所示:
@exclusive_args("one", "two")
def ex(*, one=None, two=None):
print(one or two)
ex(one=1, two=2)
---------------------------------------------------------------------------
MutuallyExclusiveArgsError Traceback (most recent call last)
<ipython-input-38-0f1d142483d2> in <module>
----> 1 ex(one=1, two=2)
<ipython-input-36-c2ff5f47260f> in __call__(self, *args, **kwargs)
21 groups = {self.arg_groups[kw] for kw in kwargs}
22 if len(groups) > 1:
---> 23 raise MutuallyExclusiveArgsError(groups)
24 self.func(*args, **kwargs)
25 return functools.partial(_inner, arg_groups=args)
MutuallyExclusiveArgsError: These groups or arguments are mutually exclusive: ('two',),('one',)
ex(one=1)
1
ex(two=2)
2
或者像这样:
@exclusive_args("one", ("two","three"))
def ex(*, one=None, two=None, three=None):
print(one, two, three)
ex(one=1)
1 None None
ex(two=1)
None 1 None
ex(three=1)
None None 1
ex(two=1, three=2)
None 1 2
ex(one=1, two=2)
---------------------------------------------------------------------------
MutuallyExclusiveArgsError Traceback (most recent call last)
<ipython-input-46-0f1d142483d2> in <module>
----> 1 ex(one=1, two=2)
<ipython-input-36-c2ff5f47260f> in __call__(self, *args, **kwargs)
21 groups = {self.arg_groups[kw] for kw in kwargs}
22 if len(groups) > 1:
---> 23 raise MutuallyExclusiveArgsError(groups)
24 self.func(*args, **kwargs)
25 return functools.partial(_inner, arg_groups=args)
MutuallyExclusiveArgsError: These groups or arguments are mutually exclusive: ('one',),('two', 'three')
ex(one=1,three=3)
---------------------------------------------------------------------------
MutuallyExclusiveArgsError Traceback (most recent call last)
<ipython-input-47-0dcb487cba71> in <module>
----> 1 ex(one=1,three=3)
<ipython-input-36-c2ff5f47260f> in __call__(self, *args, **kwargs)
21 groups = {self.arg_groups[kw] for kw in kwargs}
22 if len(groups) > 1:
---> 23 raise MutuallyExclusiveArgsError(groups)
24 self.func(*args, **kwargs)
25 return functools.partial(_inner, arg_groups=args)
MutuallyExclusiveArgsError: These groups or arguments are mutually exclusive: ('one',),('two', 'three')
虽然有点 hacky,但您可以按如下方式使用 XOR 运算符:
class OrgLocation:
def __init__(self, location_num=None, location_path=None):
"""location_num & location_path are mutually exclusive"""
assert (location_num is None) ^ bool(location_path is None), "location_num and location_path are mutually exclussive"
print("OK")
我知道这是一个老问题,但我还没有看到有人使用我采用的简单方法。这个例子是在一个普通的旧方法中,但它在 class:
的__init__
方法中同样有效
def circle_area(radius=None, diameter=None, circumference=None):
# check for mutually-exclusive parameters
number_of_options_specified = len([opt for opt in [circumference, diameter, radius] if opt is not None])
if number_of_options_specified != 1:
raise ValueError(f"Exactly one of radius ({radius}) / diameter ({diameter}) / circumference ({circumference}) must be specified")
# calculate
pi = 3.14
if radius is not None:
area = pi * radius**2
if diameter is not None:
area = pi * (diameter/2.0)**2
if circumference is not None:
area = (circumference**2)/(4.0*pi)
return area
这里是我基于构建的互斥守卫:
# Mutually Exclusive function predicate
# Returns True if no. of args that are True or not None is > 1
def ismuex(*a):
return not bool(sum(map(lambda v: bool(v if isinstance(v, bool) else not v is None), a)) > 1)
用法:
def my_func(arg_1, arg_2, arg3):
assert ismuex(arg_1, arg_2, arg3), \
"arguments arg_1, arg_2 and arg_3 are mutually exclusive"
#....