【问题标题】:How do I make a Python function with mutually exclusive arguments?如何创建具有互斥参数的 Python 函数?
【发布时间】:2018-07-25 10:39:35
【问题描述】:

我有一个 Python 类,它需要接受两个互斥参数之一。如果参数不是排他性的(即:如果两者都给出或都不给出),则应该引发错误。

class OrgLocation:
    __init__(self, location_num=None, location_path=None):
        """location_num & location_path are mutually exclusive"""

在大多数情况下,最好的选择是创建两个单独的类。但是,我正在使用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 可用于命令行界面中的互斥参数,但我不确定如何将其应用于类构造函数

如何创建具有互斥参数的 Python 函数?

【问题讨论】:

  • 你的意思是像if location_num is not None and location_path is not None: raise SomeError('error message')这样的吗?
  • 你想看到什么行为?如果两者都给出,您是否希望显示错误?你想使用其中一个吗?
  • @Matt 如果它们不是排他的,我预计会出错。更新了我的问题以澄清。
  • @Steven Vascellaro 一定要添加逻辑来检查它们是否都为 None 以涵盖您的问题中的“都没有”的情况。

标签: python arguments mutual-exclusion


【解决方案1】:

您可能想在 init 方法中创建一个测试,但更好的问题可能是...为什么?

if location_num is not None and location_path is not None:
    raise TheseParametersAreMutuallyExclusiveError()

为什么要创建一个具有多种用途的课程?为什么不创建单独的类?

【讨论】:

  • 这不是python...而且,习惯上,人们会使用is 进行None 比较,因为None 保证是单例,但== 确实 工作。
  • 刚刚修好了,你是对的...刚刚回答了 Java 问题,所以我很糟糕 :-)
  • 我同意单独的课程是理想的。不幸的是,我正在使用具有预定义类结构的external API
  • 我已经编辑了我的问题以说明为什么需要互斥参数。
【解决方案2】:

the answer by @Ivonet 之外,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 有我实际用于您的确切示例的答案。

【讨论】:

    【解决方案3】:

    你想做的很简单:

    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?

    【讨论】:

      【解决方案4】:

      我认为装饰器是一种很好的表达方式。 我确信我的实现可以改进,但它有效,而且我认为它使用法非常易读:

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

      【讨论】:

        【解决方案5】:

        虽然有点笨拙,但您可以使用 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")
        

        【讨论】:

          【解决方案6】:

          我知道这是一个老问题,但我还没有看到有人使用我采用的简单方法。这个例子是在一个普通的旧方法中,但它在一个类的 __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
          

          【讨论】:

            【解决方案7】:

            这是我基于https://stackoverflow.com/a/55156168/286807构建的互斥守卫:

            # 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"
                #....
            

            【讨论】:

              猜你喜欢
              • 2021-11-21
              • 2016-05-04
              • 2020-07-14
              • 2015-04-20
              • 1970-01-01
              • 2012-12-04
              • 2013-12-28
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多