2013-09-10 7 views
0

Я хочу дать пользователю 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') 

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

+0

Что не так с 'api_function (POST = dict (arg1 = 'foo', arg2 = 'foo2'), API = dict (arg1 = 'bar'))'? – Eevee

+0

Потому что я мог бы использовать 'api_function (POST (arg1 = 'foo', arg2 = 'foo2'), API (arg1 = 'bar'))', что является более читаемым, более коротким и более логичным решением. Но я пытаюсь сделать эту библиотеку наиболее простой для чтения для людей. – zwierzak

+0

@zwierzak Нет никакого способа сделать это, как в третьем фрагменте. Переменные не могут иметь '.' В их имени. Тем не менее, другой способ определенно выполним в зависимости от того, как вы ожидаете получить доступ к аргументам в 'api_function'. – mr2ert

ответ

0

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

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

1

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

Как я уже сказал в комментариях, первый способ невозможен. Это связано с тем, что присваивание (=) является выражением, а не выражением, поэтому оно не может вернуть значение. 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'} 
+0

Это может быть написано еще проще: 'class POST (dict): pass'. Я знаю об «проблеме» с импортом, но это будет только функция API. Большинство аргументов будут переданы как '** kwargs', потому что другие типы аргументов будут использоваться только изредка. – zwierzak

+0

Возможно, вы захотите использовать 'namedtuple' вместо класса для некоторых ограничений на имена. –

+0

@ WaleedKhan Хорошая точка. Я решил пойти с классом, потому что я не знал, какие ограничения OP там, где я оставил его как можно более открытым. – mr2ert

0

Вот мое решение.Это не самое лучшее в дизайне, так как структура аргумента окуня вложен довольно глубоко, поэтому я оценил бы обратную связь на нем:

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__.

+0

Во-первых, woah. Это безумно, очень впечатляет. Единственный код, который я бы порекомендовал, поместил '__init__' первым по соображениям удобочитаемости. Мне не очень нравится, как вызывается функция. Может быть, слишком чуждый нормальному Python? – mr2ert

+0

Я бы хотел избежать такого вызова (он будет использоваться для других целей). Но спасибо за попытку. – zwierzak

Смежные вопросы