【问题标题】:Magic assign for custom parameters自定义参数的魔术分配
【发布时间】:2013-09-10 21:28:13
【问题描述】:

我想为我的库提供用户 API,以更简单的方式区分我传递给函数的不同类型的参数。所有参数组都在前面定义(现在我有 3 个组),但它们的属性需要在运行时构建。我可以在 Django ORM 样式中执行此操作,其中双下划线分隔参数的 2 部分。但它非常不可读。示例:

def api_function(**kwargs):
    """ Separate passed arguments """

api_function(post__arg1='foo', api__arg1='bar', post_arg2='foo2')

执行此 SQLAlchemy 的更好方法,但仅用于比较属性,并且所有 args 都已在前面定义。示例:

class API(object):
    arg1 = Arg()
    arg2 = Arg()
class Post(object): #...
def api_function(*args):
    """ Separate passed arguments """

api_function(POST.arg1=='foo', API.arg1=='bar', POST.arg2=='foo2')

我想要实现的是这样的行为:

class API(object): # Magic
class POST(object): # Magic
def api_function(*args):
    """ Separate passed arguments """

api_function(POST.arg1='foo', API.arg1='bar', POST.arg2='foo2')

我尝试了什么:

  • 用定义的__setattr__声明元模型,但它在评估SyntaxError: keyword can't be an expression时上升
  • 声明__set__,但它是为已知属性设计的

我的问题是:

  • 在 Python 中是否可以像在第三个 sn-p 中一样工作?
  • 如果没有,在第三个 sn-p 中是否有任何非常接近的解决方案?最好的方式应该使用赋值运算符API.arg1='foo',最差的方式应该是API(arg1='foo')

要求——至少应该在 Python 2.7 上工作。很适合在 Python 3.2 上工作。

EDIT1 我的第一个测试是使用相等运算符(但 NEVER 应该以这种方式使用):

class APIMeta(type):
    def __getattr__(cls, item):
        return ApiData(item, None)

class API(object):
    __metaclass__ = APIMeta

    def __init__(self, key, value):
        self.key = key
        self.value = value

    def __str__(self):
        return "{0}={1}".format(self.key, self.value)

    def __eq__(self, other):
        self.value = other
        return self

def print_api(*api_data):
    for a in api_data:
        print(str(a))

print_api(API.page=='3', API=='bar')

它工作正常,但使用== 表示我想比较一些东西并且我想分配值。

【问题讨论】:

  • api_function(POST=dict(arg1='foo', arg2='foo2'), API=dict(arg1='bar')) 有什么问题?
  • 因为我也可以使用api_function(POST(arg1='foo', arg2='foo2'), API(arg1='bar')),它更具可读性、更短和更合乎逻辑的解决方案。但我正在努力让这个图书馆更易于人们阅读。
  • @zwierzak 没有办法像第三个 sn-p 那样做到这一点。变量的名称中不能包含 .。但是,另一种方法绝对可行,具体取决于您期望如何访问api_function 中的参数。
  • 我不同意它更具可读性。如果我读到它,我将不确定发生了什么,我当然不会期望它只是传递一些命名字典的一种懒惰方式。
  • @zwierzak 在带有= 的python 分配中无法返回值。

标签: python python-2.7


【解决方案1】:

注意:我不知道我有多喜欢你想要的这个架构。但我知道一件烦人的事情是调用api_function 的所有导入。例如。 from api import POST, API, api_function

正如我在 cmets 中所说,第一种方式是不可能的。这是因为赋值 (=) 是语句而不是表达式,所以它不能返回值。 Source

但你要求的另一种方式肯定是:

class POST(object):
    def __init__(self, **kwargs):
        self.args = kwargs
    # You'll also probably want to make this function a little safer.
    def __getattr__(self, name):
        return self.args[name]

def api_function(*args):
    # Update this to how complicated the handling needs to be
    # but you get the general idea...
    post_data = None
    for a in args:
        if isinstance(a, POST):
            post_data = a.args
    if post_data is None:
        raise Exception('This function needs a POST object passed.')
    print post_data

使用它:

>>> api_function('foo')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in api_function
Exception: This function needs a POST object passed.

>>> api_function(POST(arg1='foo'))
{'arg1': 'foo'}

>>> api_function(POST(arg1='foo',
...                   arg2='bar'
...                  )
...             )
{'arg1': 'foo', 'arg2': 'bar'}

【讨论】:

  • 写起来更简单:class POST(dict): pass。我知道导入的“问题”,但它只是 API 的一个功能。大多数参数将作为**kwargs 传递,因为其他类型的参数只会偶尔使用。
  • 您可能希望使用 namedtuple 而不是类来限制名称。
  • @WaleedKhan 好点。我决定去上课,因为我不知道 OP 的限制在哪里,所以我尽可能保持开放。
【解决方案2】:

这是我的解决方案。这不是最好的设计,因为参数 grouper 的结构嵌​​套得很深,所以我很感激你的反馈:

class ArgumentGrouper(object):
    """Transforms a function so that you can apply arguments in named groups.

    This system isn't tested as thoroughly as something with so many moving
    parts should be. Use at own risk.

    Usage:
    @ArgumentGrouper("foo", "bar")
    def method(regular_arg, foo__arg1, bar__arg2):
        print(regular_arg + foo__arg1 + bar__arg2)

    method.foo(", ").bar("world!")("Hello")()  # Prints "Hello, world!"
    """

    def __call__(self, func):
        """Decorate the function."""
        return self.Wrapper(func, self.argument_values)

    def __init__(self, *argument_groups):
        """Constructor.

        argument_groups -- The names of argument groups in the function.
        """
        self.argument_values = {i: {} for i in argument_groups}

    class Wrapper(object):
        """This is the result of decorating the function. You can call group
        names as function to supply their keyword arguments.
        """

        def __call__(self, *args):
            """Execute the decorated function by passing any given arguments
            and predefined group arguments.
            """
            kwargs = {}
            for group, values in self.argument_values.items():
                for name, value in values.items():
                    # Add a new argument in the form foo__arg1 to kwargs, as
                    # per the supplied arguments.
                    new_name = "{}__{}".format(
                        group,
                        name
                    )
                    kwargs[new_name] = value

            # Invoke the function with the determined arguments.
            return self.func(*args, **kwargs)

        def __init__(self, func, argument_values):
            """Constructor.

            func -- The decorated function.
            argument_values -- A dict with the current values for group
                arguments. Must be a reference to the actual dict, since each
                WrappedMethod uses it.
            """
            self.func = func
            self.argument_values = argument_values

        def __getattr__(self, name):
            """When trying to call `func.foo(arg1="bar")`, provide `foo`. TODO:
            This would be better handled at initialization time.
            """
            if name in self.argument_values:
                return self.WrappedMethod(name, self, self.argument_values)
            else:
                return self.__dict__[name]

        class WrappedMethod(object):
            """For `func.foo(arg1="bar")`, this is `foo`. Pretends to be a
            function that takes the keyword arguments to be supplied to the
            decorated function.
            """
            def __call__(self, **kwargs):
                """`foo` has been called, record the arguments passed."""
                for k, v in kwargs.items():
                    self.argument_values[self.name][k] = v
                return self.wrapper

            def __init__(self, name, wrapper, argument_values):
                """Constructor.

                name -- The name of the argument group. (This is the string
                    "foo".)
                wrapper -- The decorator. We need this so that we can return it
                    to chain calls.
                argument_values -- A dict with the current values for group
                    arguments. Must be a reference to the actual dict, since
                    each WrappedMethod uses it.
                """
                self.name = name
                self.wrapper = wrapper
                self.argument_values = argument_values

# Usage:
@ArgumentGrouper("post", "api")
def api_function(regular_arg, post__arg1, post__arg2, api__arg3):
    print("Got regular args {}".format(regular_arg))
    print("Got API args {}, {}, {}".format(post__arg1, post__arg2, api__arg3))

api_function.post(
    arg1="foo", arg2="bar"
).api(
    arg3="baz"
)
api_function("foo")

那么,用法:

@ArgumentGrouper("post", "api")
def api_function(regular_arg, post__arg1, post__arg2, api__arg3):
    print("Got regular args {}".format(regular_arg))
    print("Got API args {}, {}, {}".format(post__arg1, post__arg2, api__arg3))

api_function.post(
    arg1="foo", arg2="bar"
).api(
    arg3="baz"
)
api_function("foo")

输出:

Got regular args foo
Got API args foo, bar, baz

通过自省来刮取参数组名称应该很简单。

您会注意到参数命名约定被硬编码到 WrappedMethod 中,因此您必须确保您可以接受。

你也可以在一个语句中调用它:

api_function.post(
    arg1="foo", arg2="bar"
).api(
    arg3="baz"
)("foo")

或者你可以添加一个专用的run 方法来调用它,它会代替Wrapper.__call__

【讨论】:

  • 首先,哇。这太疯狂了,令人印象深刻。出于可读性原因,我建议的唯一明智的代码是将__init__s 放在首位。我真的不喜欢函数的调用方式。也许与普通的 Python 有点太陌生了?
  • 我想避免这样的调用(它将用于其他目的)。但是感谢您的尝试。
【解决方案3】:

Python 不允许在任何其他代码中使用赋值运算符,所以:

(a=1)
func((a=1))

将上升SyntaxError。这意味着不可能以这种方式使用它。此外:

func(API.arg1=3)

将被视为赋值的左侧是参数API.arg1,它在 Python 中不是变量的有效名称。唯一的解决方案是以 SQLAlchemy 风格制作:

func({
    API.arg1: 'foo',
    API.arg2: 'bar',
    DATA.arg1: 'foo1',
})

func(**{
    API.arg1: 'foo',
    API.arg2: 'bar',
    DATA.arg1: 'foo1',
})

或者只是:

func( API(arg1='foo', arg2='bar'), POST(arg1='foo1'), POST(arg2='bar1'))

感谢您的关注和回答。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-11-06
    • 2019-09-26
    • 2013-01-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多