Класс с реестром методов на основе декораторов - PullRequest
0 голосов
/ 16 мая 2018

У меня есть класс, у которого есть несколько методов, каждый из которых имеет определенные свойства (в смысле качества). Мне бы хотелось, чтобы эти методы были доступны в списке внутри класса, чтобы они могли выполняться сразу. Обратите внимание, что свойства могут быть взаимозаменяемыми, поэтому это не может быть решено с помощью дополнительных классов, которые наследуются от исходного. В идеальном мире это выглядело бы примерно так:

class MyClass:
    def __init__():
        red_rules = set()
        blue_rules = set()
        hard_rules = set()
        soft_rules = set()

    @red
    def rule_one(self):
        return 1

    @blue
    @hard
    def rule_two(self):
        return 2

    @hard
    def rule_three(self):
        return 3

    @blue
    @soft
    def rule_four(self):
        return 4

Когда создается экземпляр класса, должно быть легко просто выполнить все красные и мягкие правила, комбинируя наборы и выполняя методы. Декораторы для этого сложны, так как обычный регистрирующий декоратор может заполнять глобальный объект, но не атрибут класса:

def red(fn):
    red_rules.add(fn)
    return fn

Как мне реализовать нечто подобное?

Ответы [ 2 ]

0 голосов
/ 16 мая 2018

Декораторы применяются , когда функция определена ;в классе, когда класс определен.На данный момент еще нет экземпляров!

У вас есть три варианта:

  1. Зарегистрируйте ваши декораторы на уровне класса.Это не так чисто, как может показаться;вы либо должны явно передать дополнительные объекты вашим декораторам (red_rules = set(), затем @red(red_rules), чтобы фабрика декораторов могла затем добавить функцию в нужное место), либо вам нужно использовать какой-то инициализатор класса, чтобы подобрать специально помеченныефункции;Вы можете сделать это с помощью базового класса, который определяет метод класса __init_subclass__ , после чего вы можете перебирать пространство имен и находить эти маркеры (атрибуты, установленные декораторами).

  2. Пусть ваш метод __init__ (или метод __new__) перебирает все методы в классе и ищет специальные атрибуты, которые декораторы поместили туда.

    Декоратору нужно будет только добавить атрибут _rule_name или аналогичный к декорированным методам, а {getattr(self, name) for for name in dir(self) if getattr(getattr(self, name), '_rule_name', None) == rule_name} подберет любой метод с правильным именем правила, определенным в rule_name.

  3. Заставьте ваших декораторов создавать новые объекты дескриптора ;дескрипторы имеют __set_name__() метод , вызываемый при создании объекта класса.Это дает вам доступ к классу, и, таким образом, вы можете добавить атрибуты к этому классу.

Обратите внимание, что __init_subclass__ и __set_name__ требуют Python 3.6 или новее;вам придется прибегнуть к метаклассу для достижения аналогичной функциональности в более ранних версиях.

Также обратите внимание, что когда вы регистрируете функции на уровне класса, вам необходимо явно свяжите их с function.__get__(self, type(cls)), чтобы превратить их в методы, или , которые вы можете явно передать в self при вызове их.Вы можете автоматизировать это, сделав выделенный класс для хранения наборов правил, и сделайте этот класс тоже дескриптором:

import types
from collections.abc import MutableSet

class RulesSet(MutableSet):
    def __init__(self, values=(), rules=None, instance=None, owner=None):
        self._rules = rules or set()  # can be a shared set!
        self._instance = instance
        self._owner = owner
        self |= values

    def __repr__(self):
        bound = ''
        if self._owner is not None:
            bound = f', instance={self._instance!r}, owner={self._owner!r}'
        rules = ', '.join([repr(v) for v in iter(self)])
        return f'{type(self).__name__}({{{rules}}}{bound})'

    def __contains__(self, ob):
        try:
            if ob.__self__ is self._instance or ob.__self__ is self._owner:
                # test for the unbound function instead when both are bound, this requires staticmethod and classmethod to be unwrapped!
                ob = ob.__func__
                return any(ob is getattr(f, '__func__', f) for f in self._rules)
        except AttributeError:
            # not a method-like object
            pass
        return ob in self._rules

    def __iter__(self):
        if self._owner is not None:
            return (f.__get__(self._instance, self._owner) for f in self._rules)
        return iter(self._rules)

    def __len__(self):
        return len(self._rules)

    def add(self, ob):
        while isinstance(ob, Rule):
            # remove any rule wrappers
            ob = ob._function
        assert isinstance(ob, (types.FunctionType, classmethod, staticmethod))
        self._rules.add(ob)

    def discard(self, ob):
        self._rules.discard(ob)

    def __get__(self, instance, owner):
        # share the set with a new, bound instance.
        return type(self)(rules=self._rules, instance=instance, owner=owner)

class Rule:
    @classmethod
    def make_decorator(cls, rulename):
        ruleset_name = f'{rulename}_rules'
        def decorator(f):
            return cls(f, ruleset_name)
        decorator.__name__ = rulename
        return decorator

    def __init__(self, function, ruleset_name):
        self._function = function
        self._ruleset_name = ruleset_name

    def __get__(self, *args):
        # this is mostly here just to make Python call __set_name__
        return self._function.__get__(*args)

    def __set_name__(self, owner, name):
        # register, then replace the name with the original function
        # to avoid being a performance bottleneck
        ruleset = getattr(owner, self._ruleset_name, None)
        if ruleset is None:
            ruleset = RulesSet()
            setattr(owner, self._ruleset_name, ruleset)
        ruleset.add(self)
        # transfer controrol to any further rule objects
        if isinstance(self._function, Rule):
            self._function.__set_name__(owner, name)
        else:
            setattr(owner, name, self._function)

red = Rule.make_decorator('red')
blue = Rule.make_decorator('blue')
hard = Rule.make_decorator('hard')
soft = Rule.make_decorator('soft')

Затем просто используйте:

class MyClass:
    @red
    def rule_one(self):
        return 1

    @blue
    @hard
    def rule_two(self):
        return 2

    @hard
    def rule_three(self):
        return 3

    @blue
    @soft
    def rule_four(self):
        return 4

и вы сможете получить доступ к self.red_rules и т. Д. Как набор со связанными методами:

>>> inst = MyClass()
>>> inst.red_rules
RulesSet({<bound method MyClass.rule_one of <__main__.MyClass object at 0x106fe7550>>}, instance=<__main__.MyClass object at 0x106fe7550>, owner=<class '__main__.MyClass'>)
>>> inst.blue_rules
RulesSet({<bound method MyClass.rule_two of <__main__.MyClass object at 0x106fe7550>>, <bound method MyClass.rule_four of <__main__.MyClass object at 0x106fe7550>>}, instance=<__main__.MyClass object at 0x106fe7550>, owner=<class '__main__.MyClass'>)
>>> inst.hard_rules
RulesSet({<bound method MyClass.rule_three of <__main__.MyClass object at 0x106fe7550>>, <bound method MyClass.rule_two of <__main__.MyClass object at 0x106fe7550>>}, instance=<__main__.MyClass object at 0x106fe7550>, owner=<class '__main__.MyClass'>)
>>> inst.soft_rules
RulesSet({<bound method MyClass.rule_four of <__main__.MyClass object at 0x106fe7550>>}, instance=<__main__.MyClass object at 0x106fe7550>, owner=<class '__main__.MyClass'>)
>>> for rule in inst.hard_rules:
...     rule()
...
2
3

Те же правила доступны для класса;обычные функции остаются несвязанными:

>>> MyClass.blue_rules
RulesSet({<function MyClass.rule_two at 0x107077a60>, <function MyClass.rule_four at 0x107077b70>}, instance=None, owner=<class '__main__.MyClass'>)
>>> next(iter(MyClass.blue_rules))
<function MyClass.rule_two at 0x107077a60>

Тестирование содержимого работает, как и ожидалось:

>>> inst.rule_two in inst.hard_rules
True
>>> inst.rule_two in inst.soft_rules
False
>>> MyClass.rule_two in MyClass.hard_rules
True
>>> MyClass.rule_two in inst.hard_rules
True

Вы можете использовать эти правила для регистрации classmethod и staticmethod объектов:

>>> class Foo:
...     @hard
...     @classmethod
...     def rule_class(cls):
...         return f'rule_class of {cls!r}'
...
>>> Foo.hard_rules
RulesSet({<bound method Foo.rule_class of <class '__main__.Foo'>>}, instance=None, owner=<class '__main__.Foo'>)
>>> next(iter(Foo.hard_rules))()
"rule_class of <class '__main__.Foo'>"
>>> Foo.rule_class in Foo.hard_rules
True
0 голосов
/ 16 мая 2018

Вы можете создать подкласс set и присвоить ему метод декоратора:

class MySet(set):
    def register(self, method):
        self.add(method)
        return method

class MyClass:
    red_rules = MySet()
    blue_rules = MySet()
    hard_rules = MySet()
    soft_rules = MySet()

    @red_rules.register
    def rule_one(self):
        return 1

    @blue_rules.register
    @hard_rules.register
    def rule_two(self):
        return 2

    @hard_rules.register
    def rule_three(self):
        return 3

    @blue_rules.register
    @soft_rules.register
    def rule_four(self):
        return 4

Или, если вы находите использование метода .register безобразным, вы всегда можете определить метод __call__ для использования самого набора в качестве декоратора:

class MySet(set):
    def __call__(self, method):
        """Use set as a decorator to add elements to it."""
        self.add(method)
        return method

class MyClass:
    red_rules = MySet()
    ...

    @red_rules
    def rule_one(self):
        return 1

    ...

Это выглядит лучше, но менее явно, поэтому другим соавторам (или будущим вам) может быть сложнее понять, что здесь происходит.


Чтобы вызвать сохраненные функции, вы можете просто перебрать нужный набор и передать его в качестве аргумента self:

my_instance = MyClass()
for rule in MyClass.red_rules:
    rule(my_instance)

Вы также можете создать вспомогательную функцию, чтобы сделать это для вас, например, вы можете создать MySet.invoke() метод:

class MySet(set):
    ...
    def invoke(self, obj):
        for rule in self:
            rule(obj)

А теперь просто позвоните:

MyClass.red_rules.invoke(my_instance)

Или вы могли бы MyClass обработать это вместо:

class MyClass:
    ...
    def invoke_rules(self, rules):
        for rule in rules:
            rule(self)

И затем вызвать это для экземпляра MyClass:

my_instance.invoke_rules(MyClass.red_rules)
...