Магическое назначение для настраиваемых параметров

Я хочу дать пользователю API для моей библиотеки более простой способ отличить разные типы параметров, которые я передаю функции. Все группы аргументов определены раньше (на данный момент у меня есть 3 группы), но атрибуты из них должны быть построены при запуске. Я могу сделать это в стиле Django ORM, где двойной подчеркивание разделяет 2 части параметра. Но это очень непроницаемо. Пример:

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

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

Лучший способ сделать эту SQLAlchemy, но только для сравнения атрибутов, и все аргументы определены ранее. Пример:

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 работать, как в третьем фрагменте?
  • Если нет, есть ли действительно очень близкое решение, чтобы выглядеть в третьем фрагменте? Лучший способ должен использовать оператор присваивания API.arg1='foo', худший API(arg1='foo')

Требования - должны работать хотя бы на Python 2.7. Хорошо работать на Python 3.2.

EDIT1 Мой первый тест, в котором используется оператор равенства (но он НИКОГДА не должен использоваться таким образом):

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

Он работает правильно, но использование == предполагает, что я хочу что-то сравнить, и я хочу присвоить значение.

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

Благодарим вас за интерес и ответы.


ПРИМЕЧАНИЕ. Я не знаю, насколько мне нравится эта схема, которую вы хотите. Но я знаю, что одна досадная вещь будет всем импортом для вызова api_function. EG from api import POST, API, api_function

Как я уже сказал в комментариях, первый способ невозможен. Это связано с тем, что присваивание (=) является выражением, а не выражением, поэтому оно не может вернуть значение. Источник

Но, как вы просили,

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'}
</stdin></module></stdin>


Вот мое решение. Это не самое лучшее в дизайне, так как структура аргумента 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__.

licensed under cc by-sa 3.0 with attribution.