Как создать тип, закрытый для унаследованных операций? - PullRequest
0 голосов
/ 15 сентября 2018

В математическом смысле набор (или тип) является закрытым под операцией, если операция всегда возвращает член самого набора.

Этот вопрос касается создания классаон закрыт для всех операций, унаследованных от его суперклассов.

Рассмотрим следующий класс.

class MyInt(int):
    pass

Поскольку __add__ не был переопределен, он не закрывается при добавлении.

x = MyInt(6)
print(type(x + x))  # <class 'int'>

Один очень утомительный способ сделать тип закрытым - вручную откатить результат каждой операции, которая возвращает int в MyInt.

Здесь я автоматизировал этот процесс, используяметакласс, но это кажется слишком сложным решением.

import functools

class ClosedMeta(type):
    _register = {}

    def __new__(cls, name, bases, namespace):
        # A unique id for the class
        uid = max(cls._register) + 1 if cls._register else 0

        def tail_cast(f):
            @functools.wraps(f)
            def wrapper(*args, **kwargs):
                out = f(*args, **kwargs)
                if type(out) in bases:
                    # Since the class does not exist yet, we will recover it later
                    return cls._register[uid](out)
                return out
            return wrapper

        for base in reversed(bases):
            for name, attr in base.__dict__.items():
                if callable(attr) and name not in namespace:
                    namespace[name] = tail_cast(attr)

        subcls = super().__new__(cls, name, bases, namespace)
        cls._register[uid] = subcls
        return subcls

class ClosedInt(int, metaclass=ClosedMeta):
    pass

Это не работает в некоторых случаях, таких как property и методы, восстановленные с помощью __getattribute__.Также происходит сбой, когда база состоит не только из базовых типов.

Например, это не удается:

class MyInt(int):
    pass

class ClosedInt(MyInt, metaclass=ClosedMeta):
    pass

ClosedInt(1) + ClosedInt(1) # returns the int 2

Я пытался это исправить, но, похоже, все глубже и глубжев кроличьей норе.

Это похоже на проблему, которая может иметь какое-то простое питоническое решение.Каковы другие, более аккуратные способы достижения такого закрытого типа?

Ответы [ 5 ]

0 голосов
/ 15 сентября 2018

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

class containerize:
    def __call__(self, obj):
        if isinstance(obj, type):
            return self.decorate_class(obj)
        return self.decorate_callable(obj)

    def decorate_class(self, cls):
        for name in dir(cls):
            attr = getattr(cls, name)
            if callable(attr) and name not in ('__class__', '__init__', '__new__', '__str__', '__repr__', '__getattribute__'):
                setattr(cls, name, self.decorate_callable(attr))
        return cls

    def decorate_callable(self, func):
        def wrapper(obj, *args, **kwargs):
            return obj.__class__(func(obj, *args, **kwargs))
        return wrapper

так что:

class MyInt(int):
    pass

@containerize()
class ClosedIntContainer(MyInt):
    pass

i = ClosedIntContainer(3) + ClosedIntContainer(2)
print(i, type(i).__name__)

будет выводить:

5 ClosedIntContainer

и что в качестве бонуса декоратор может быть выборочно использован и для отдельных методов:

class MyInt(int):
    @containerize()
    def __add__(self, other):
        return super().__add__(other)

i = MyInt(3) + MyInt(2)
print(i, type(i).__name__)

Это выводит:

5 MyInt
0 голосов
/ 15 сентября 2018

Каждый пишет короткие коды и мета-классы, а я едва пишу декоратор. (блин, лол) Но я все равно поделюсь этим.

from functools import wraps


class CLOSED:
    _built_ins = [
        '__add__', '__sub__', '__mul__', '__floordiv__',
        '__div__', '__truediv__', '__mod__', '__divmod__',
        '__pow__', '__lshift__', '__rshift__','__and__',
        '__or__', '__xor__',
    ]

    @staticmethod
    def register_closed(method):  # Or you can use type annotations
        method.registered = True  # Or you can add the method names as string to closed decorator
        return method  # In this version you decorate the methods with this

    @staticmethod
    def closed_method(method, cls):
        @wraps(method)
        def wrapper(*a, **kw):
            return cls(method(*a, **kw))

        return wrapper

    @classmethod
    def closed_class(klass, cls):
        for magic in klass._built_ins:
            _method = getattr(cls, magic, False)
            if _method:
                setattr(cls, magic, klass.closed_method(_method, cls))

        for method in dir(cls):
            c1 = method not in klass._built_ins
            c2 = method not in dir(object)
            c3 = getattr(getattr(cls, method), 'registered', False)
            if all((c1, c2, c3)):
                _method = getattr(cls, method)
                setattr(cls, method, klass.closed_method(_method, cls))
        return cls

Теперь, после долгой настройки, вы просто украшаете класс, как обычно; Я слишком сонный, чтобы заставить его работать с унаследованными классами, поэтому сейчас вам нужно украсить класс, который наследует от закрытого класса.

@CLOSED.closed_class
class foo(int):
    @CLOSED.register_closed  # or if you can simply add this to CLOSED.closed_class
    def bar(self, other):    # if you are certain that every method can be casted to its own class
        """Basically just the __add__ method"""
        return self + other


print(type(foo(1) + foo(1))); print(foo(1) + foo(1))  # <class '__main__.foo'> 2
print(type(foo(1).bar(2))); print(foo(1).bar(2))      # <class '__main__.foo'> 3


@CLOSED.closed_class
class baz(foo):
    pass

print(type(baz(1) + baz(3))); print(baz(1) + baz(3))  # <class '__main__.baz'> 4
print(type(baz(1).bar(4))); print(baz(1).bar(4))      # <class '__main__.baz'> 5

Не стесняйтесь понижать голос, поскольку я все еще не уверен, правильно ли я понял вопрос.

0 голосов
/ 15 сентября 2018

Я думаю, что идея использования метакласса - это путь. Хитрость заключается в том, чтобы динамически приводить значения, когда вы получаете их, а не сразу. Это в основном то, о чем говорит python: не зная точно, что вы получите или что там, пока вы на самом деле не получите его.

Для этого вам нужно переопределить __getattribute__ и __getattr__ в своем классе с некоторыми оговорками:

  1. Операторы не используют обычные методы доступа к атрибутам. Даже определение правильных __getattribute__ и __getattr__ в вашем метаклассе не поможет. Дандеры должны быть явно переопределены для каждого класса.
  2. Методы, возвращаемые __getattribute__ и __getattr__, должны иметь свои возвращаемые значения, приведенные к целевому типу. То же самое относится и к дункерам, называемым операторами.
  3. Некоторые методы должны быть исключены из # 2 для обеспечения правильной работы оборудования.

Одна и та же базовая оболочка приведения может использоваться для всех возвращаемых значений атрибута и метода. Ему нужно только выполнить рекурсию ровно один раз, когда он вызывается по результату __getattribute__ или __getattr__.

Решение, показанное ниже, делает именно это. Он явно оборачивает все ошибки, которые не указаны в качестве исключений. Все остальные атрибуты либо приводятся немедленно, либо переносятся, если они являются функциями. Он позволяет настраивать любой метод, проверяя все в __mro__, включая сам класс. Решение будет корректно работать с классом и статическими методами, потому что оно хранит процедуру приведения и не использует type(self) (как это делали некоторые из моих предыдущих попыток). Он будет правильно исключать любые атрибуты, перечисленные в exceptions, а не только более сложные методы.

import functools


def isdunder(x):
    return isinstance(x, str) and x.startswith('__') and x.endswith('__')


class DunderSet:
    def __contains__(self, x):
        return isdunder(x)


def wrap_method(method, xtype, cast):

    @functools.wraps(method)
    def retval(*args, **kwargs):
        result = method(*args, **kwargs)
        return cast(result) if type(result) == xtype else result

    return retval


def wrap_getter(method, xtype, cast, exceptions):
    @functools.wraps(method)
    def retval(self, name, *args, **kwargs):
        result = method(self, name, *args, **kwargs)
        return result if name in exceptions else check_type(result, xtype, cast)

    return retval


def check_type(value, xtype, cast):
    if type(value) == xtype:
        return cast(value)
    if callable(value):
        return wrap_method(value, xtype, cast)
    return value


class ClosedMeta(type):
    def __new__(meta, name, bases, dct, **kwargs):
        if 'exceptions' in kwargs:
            exceptions = set([
                '__new__', '__init__', '__del__',
                '__init_subclass__', '__instancecheck__', '__subclasscheck__',
                *map(str, kwargs.pop('exceptions'))
            ])
        else:
            exceptions = DunderSet()
        target = kwargs.pop('target', bases[0] if bases else object)

        cls = super().__new__(meta, name, bases, dct, **kwargs)

        for base in cls.__mro__:
            for name, item in base.__dict__.items():
                if isdunder(name) and (base is cls or name not in dct) and callable(item):
                    if name in ('__getattribute__', '__getattr__'):
                        setattr(cls, name, wrap_getter(item, target, cls, exceptions))
                    elif name not in exceptions:
                        setattr(cls, name, wrap_method(item, target, cls))
        return cls

    def __init__(cls, *args, **kwargs):
        return super().__init__(*args)


class MyInt(int):
    def __contains__(self, x):
        return x == self
    def my_op(self, other):
        return int(self * self // other)


class ClosedInt(MyInt, metaclass=ClosedMeta, target=int,
                exceptions=['__index__', '__int__', '__trunc__', '__hash__']):
    pass

class MyClass(ClosedInt, metaclass=type):
    def __add__(self, other):
        return 1

print(type(MyInt(1) + MyInt(2)))
print(0 in MyInt(0), 1 in MyInt(0))
print(type(MyInt(4).my_op(16)))

print(type(ClosedInt(1) + ClosedInt(2)))
print(0 in ClosedInt(0), 1 in ClosedInt(0))
print(type(ClosedInt(4).my_op(16)))

print(type(MyClass(1) + ClosedInt(2)))

Результат

<class 'int'>
True False
<class 'int'> 

<class '__main__.ClosedInt'>
True False
<class '__main__.ClosedInt'>

<class 'int'>

Последний пример - дань уважения @ wim's answer . Это показывает, что вы должны хотеть сделать это, чтобы это работало.

Ссылка IDEOne, потому что у меня сейчас нет доступа к компьютеру: https://ideone.com/iTBFW3

Приложение 1. Улучшенные исключения по умолчанию

Я думаю, что лучший набор исключений по умолчанию, чем все более сложные методы, можно выполнить, внимательно изучив раздел документации Имена специальных методов документации. Методы можно разделить на два широких класса: методы с очень конкретными возвращаемыми типами, которые заставляют механизм python работать, и методы, выходные данные которых должны быть проверены и упакованы, когда они возвращают экземпляр вашего интересующего типа. Существует третья категория - методы, которые всегда следует исключать, даже если вы забыли явно упомянуть их.

Вот список методов, которые всегда исключаются:

  • __new__
  • __init__
  • __del__
  • __init_subclass__
  • __instancecheck__
  • __subclasscheck__

Вот список всего, что должно быть исключено по умолчанию:

  • __repr__
  • __str__
  • __bytes__
  • __format__
  • __lt__
  • __le__
  • __eq__
  • __ne__
  • __gt__
  • __ge__
  • __hash__
  • __bool__
  • __setattr__
  • __delattr__
  • __dir__
  • __set__
  • __delete__
  • __set_name__
  • __slots__ (не метод, но все же)
  • __len__
  • __length_hint__
  • __setitem__
  • __delitem__
  • __iter__
  • __reversed__
  • __contains__
  • __complex__
  • __int__
  • __float__
  • __index__
  • __enter__
  • __exit__
  • __await__
  • __aiter__
  • __anext__
  • __aenter__
  • __aexit__

Если мы спрятали этот список в переменную с именем default_exceptions, класс DunderSet может быть полностью удален, а условное условие, извлекающее exceptions, может быть заменено на:

exceptions = set([
    '__new__', '__init__', '__del__',
    '__init_subclass__', '__instancecheck__', '__subclasscheck__',
    *map(str, kwargs.pop('exceptions', default_exceptions))
])

Приложение 2: Улучшенное нацеливание

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

Первый шаг в этом - сделать target контейнером классов вместо единственной ссылки на класс. Вместо

target = kwargs.pop('target', bases[0] if bases else object)

сделать

target = kwargs.pop('target', bases[:1] if bases else [object])
try:
    target = set(target)
except TypeError:
    target = {target}

Теперь замените каждое вхождение blah == target (или blah == xtype в оболочках) на blah in target (или blah in xtype).

0 голосов
/ 15 сентября 2018

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

Вот основные моменты, которые требовались отбыть исправленным.

  • Мы должны проверить методы всех классов в mro , а не только баз ;

  • __getattribute__ и __getattr__ должны рассматриваться как особые случаи;

  • Атрибуты с __get__ должны обрабатываться отдельно;

  • Мы должны написать список исключений, поскольку такие методы, как __int__ или __eq__, очевидно, должны возвращать ожидаемые типы.

Код

import functools

def get_mro(bases):
    # We omit 'object' as it is the base type
    return type('', bases, {}).__mro__[1:-1]

class ClosedMeta(type):
    _register = {}

    # Some methods return type must not change
    _exceptions = ('__int__', '__eq__', ...)

    def __new__(cls, name, bases, namespace):
        # A unique id for the class
        uid = max(cls._register) + 1 if cls._register else 0
        mro = get_mro(bases)

        def tail_cast(f):
            """Cast the return value of f"""
            @functools.wraps(f)
            def wrapper(*args, **kwargs):
                out = f(*args, **kwargs)
                if type(out) in mro:
                    # Since the class does not exist yet, we will recover it later
                    return cls._register[uid](out)
                return out
            return wrapper

        def deep_tail_cast(f):
            """Cast the return value of f or the return value of f(...)"""
            @functools.wraps(f)
            def wrapper(*args, **kwargs):
                out = f(*args, **kwargs)
                if callable(out):
                    return tail_cast(out)
                elif type(out) in mro:
                    return cls._register[uid](out)
                else:
                    return out
            return wrapper

        class PropertyCast:
            """Cast the return value of a property"""
            def __init__(self, prop):
                self.prop = prop

            def __get__(self, instance, owner):
                return cls._register[uid](self.prop.__get__(instance, owner))

            def __set__(self, instance, value):
                return self.prop.__set__(instance, value)

            def __delete__(self, instance):
                return self.prop.__delete__(instance)

        for base in reversed(mro):
            for name, attr in base.__dict__.items():
                if name in ('__getattr__', '__getattribute__'):
                    namespace[name] = deep_tail_cast(attr)
                elif callable(attr) and name not in namespace and name not in cls._exceptions:
                    namespace[name] = tail_cast(attr)
                elif hasattr(attr, '__get__'):
                    namespace[name] = PropertyCast(attr)

        subcls = super().__new__(cls, name, bases, namespace)
        cls._register[uid] = subcls
        return subcls

Пример

class MyInt(int):
    def __getattr__(self, _):
        return 1

    @property
    def foo(self):
        return 2

class ClosedInt(MyInt, metaclass=ClosedMeta):
    pass

x = ClosedInt(2)
print(type(x * x), x * x)
print(type(x.foo), x.foo)
print(type(x.bar), x.bar)

Вывод

<class '__main__.ClosedIntContainer'> 4
<class '__main__.ClosedIntContainer'> 2
<class '__main__.ClosedIntContainer'> 1

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

0 голосов
/ 15 сентября 2018

Этого нельзя сделать, модель данных это запрещает. И я могу доказать это вам:

>>> class MyClass(ClosedInt, metaclass=type):
...     def __add__(self, other):
...         return 'potato'
...     
>>> MyClass(1) + ClosedInt(2)
'potato'

Добавление обрабатывается сначала левым объектом, и если левый тип обрабатывает его (т.е. не возвращает NotImplemented singleton), то Ничего о other не рассматривается в этой операции. Если правый тип является подклассом левого типа, вы можете контролировать результат с помощью отраженного метода __radd__ - но, конечно, это невозможно в общем случае.

...