Дескрипторы Python с аргументами?

TL; DR Python 2.7.5, при использовании дескрипторов в качестве декораторов, есть ли способ передать аргументы (в метод __init__)? ИЛИ Как я могу получить доступ к атрибутам экземпляра класса, используя декоратор метода с аргументами (как здесь)? -- Однако я считаю, что это невозможно, поэтому основное внимание ниже уделяется дескрипторам...


Длинная версия

У меня есть класс объектов с разными атрибутами типа. Основываясь на «типе» экземпляра, я хотел бы, чтобы метод был доступен или нет. Я знаю, что один из способов - создать несколько классов, но я стараюсь не использовать кучу операторов if/else при создании этих объектов. Например, у меня есть два объекта A и B, которые почти идентичны, за исключением объекта B. Я не хочу, чтобы был доступен метод get_start_date(). По сути, я хочу, чтобы и A, и B были экземплярами класса MyObjects, но имели другой атрибут "type".

type(A) == type(B)
A.genus_type != B.genus_type

Я бы использовал этот атрибут .genus_type, чтобы различать, какие методы допустимы, а какие нет...

Я думал, что могу использовать декораторы с белым списком, например:

def valid_for(whitelist):
    def wrap(f):
        def wrapper(*args, **kwargs):
            return f(*args, **kwargs)
    return wrapper
return wrap

class A(object):
    @valid_for(['typeB'])
    def do_something_cool(self):
        print 'blah'

Но проблема была в том, что у меня не было доступа к фактическому экземпляру класса в декораторе, где я мог бы проверить атрибут типа экземпляра. Основываясь на этом вопросе SO, Я подумал: «Я могу использовать дескрипторы!».

Итак, я попробовал:

class valid_for(object):
    """ descriptor to check the type of an item, to see
    if the method is valid for that type"""
    def __init__(self, func):
        self.f = func

    def __get__(self, instance, owner):
        def wrapper(*args):
            return self.f(instance, *args)
        return wrapper

Но тогда я не мог понять, как передать параметр ['typeB'] в дескриптор... по умолчанию Python передает вызываемый метод в качестве аргумента __init__. Я мог бы создать жестко закодированные дескрипторы для каждого типа и вложить их, но потом мне интересно, столкнусь ли я с эта проблема. Предполагая, что я смог преодолеть проблему вложенности, также кажется менее чистым сделать что-то вроде:

class A(object):
    @valid_for_type_b
    @valid_for_type_f
    @valid_for_type_g
    def do_something_cool(self):
        print 'blah'

Выполнение чего-то подобного только что сделало мой func равным списку ['typeB']...

class valid_for(object):
    """ descriptor to check the type of an item, to see
    if the method is valid for that type"""
    def __init__(self, func, *args):
        self.f = func

    def __get__(self, instance, owner):
        def wrapper(*args):
            return self.f(instance, *args)
        return wrapper

class A(object):
    @valid_for(['typeB'])
    def do_something_cool(self):
        print 'blah'

И моего func нет в списке *args, поэтому я не могу просто поменять местами аргументы (*args пусто).

Я искал подсказки здесь и здесь, но не нашел ничего похожего на чистый или допустимый обходной путь. Есть ли чистый способ сделать это, или мне нужно использовать несколько классов и просто смешивать различные методы? Или прямо сейчас я склоняюсь к методу экземпляра, который проверяет, но он кажется менее чистым и пригодным для повторного использования...

class A(object):
    def _valid_for(self, whitelist):
        if self.genus_type not in whitelist:
            raise Exception

    def do_something_cool(self):
        self._valid_for(['foo'])
        print 'blah'

Я использую Python 2.7.5.


ОБНОВЛЕНИЕ 1

По предложению в комментариях я попробовал:

def valid_for2(whitelist):
    def wrap(f):
        def wrapper(*args, **kwargs):
            import pdb
            pdb.set_trace()
            print args[0].genus_type
            return f(*args, **kwargs)
        return wrapper
    return wrap

Но на данный момент args[0]. просто возвращает аргументы:

(Pdb) args[0]
args = (<FormRecord object at 0x112f824d0>,)
kwargs = {}
(Pdb) args[0].genus_type
args = (<FormRecord object at 0x112f824d0>,)
kwargs = {}

Однако использование functools, как было предложено, действительно работает, поэтому я присуждаю ответ. Кажется, в functools есть какая-то черная магия, которая позволяет спорить?


ОБНОВЛЕНИЕ 2

Таким образом, исследуя предложение jonrsharpe, его метод также работает, но я должен явно использовать self вместо args[0]. Не знаю, почему поведение отличается...

def valid_for2(whitelist):
    def wrap(f):
        def wrapper(self, *args, **kwargs):
            print self.genus_type
            return f(*args, **kwargs)
        return wrapper
    return wrap

приводит к тому же результату, что и с functools. Спасибо!


person user    schedule 12.06.2015    source источник
comment
Да, я пробовал с декораторами, но с декоратором я не могу получить доступ к атрибутам этого экземпляра класса...   -  person user    schedule 12.06.2015
comment
Конечно, самая большая проблема заключается в том, что когда вы украшаете метод, у вас нет доступа к классу, в котором находится метод? Вы можете получить доступ к self (и, следовательно, проверить isinstance(self, ...) или type(self)) в wrapper (в настоящее время это args[0], но вы можете сделать его явным), но вы не можете включить класс, который в настоящее время определяется в whitelist. Как вы разрешите 'typeB' в фактический класс?   -  person jonrsharpe    schedule 12.06.2015
comment
Так что моя терминология может быть запутанной. Когда я использую 'typeB' в качестве примера выше, он определяется как атрибут экземпляра класса, например B.genus_type = 'typeB'. Я надеюсь, что все может быть одним классом, поэтому type(A) == type(B), но A.genus_type != B.genus_type.   -  person user    schedule 12.06.2015
comment
Так почему бы вам просто не проверить, например. args[0].genus_type in whitelist внутри wrapper?   -  person jonrsharpe    schedule 12.06.2015
comment
Ммм, поэтому я недостаточно осведомлен о том, почему это не работает, но я вставлю результат выше. В основном он просто возвращает список аргументов?   -  person user    schedule 12.06.2015
comment
Хорошо, в ответ @jonrsharpe, это забавно, но то, что вы предложили, работает, когда я явно использую self вместо args[0]...   -  person user    schedule 12.06.2015


Ответы (1)


Если я правильно понимаю вашу ситуацию, вы ищете замыкание -- функцию, которая может ссылаются на локальное пространство имен внешней функции.

Поскольку вы передаете ['typeB'] в valid_for, как в

@valid_for(['typeB'])

мы должны сделать valid_for функцией, которая возвращает декоратор. Декоратор, в свою очередь, принимает функцию (зарождающийся метод) в качестве входных данных и возвращает другую (wrapper) функцию.

Ниже wrapper находится замыкание, которое может получить доступ к значению typelist из своего тела во время выполнения.


import functools
def valid_for(typelist):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(self, *args):
            if self.genus_type in typelist:
                return func(self, *args)
            else:
                # handle this case
                raise NotImplementedError(
                    '{} not in {}'.format(self.genus_type, typelist))
        return wrapper
    return decorator

class A(object):
    def __init__(self):
        self.genus_type = 'typeA'
    @valid_for(['typeB'])
    def do_something_cool(self):
        print 'blah'

a = A()
try:
    a.do_something_cool()
except NotImplementedError as err:
    print(err)
    # typeA not in ['typeB']

a.genus_type = 'typeB'
a.do_something_cool()
# blah
person unutbu    schedule 12.06.2015