Я думаю, что идея использования метакласса - это путь. Хитрость заключается в том, чтобы динамически приводить значения, когда вы получаете их, а не сразу. Это в основном то, о чем говорит python: не зная точно, что вы получите или что там, пока вы на самом деле не получите его.
Для этого вам нужно переопределить __getattribute__
и __getattr__
в своем классе с некоторыми оговорками:
- Операторы не используют обычные методы доступа к атрибутам. Даже определение правильных
__getattribute__
и __getattr__
в вашем метаклассе не поможет. Дандеры должны быть явно переопределены для каждого класса.
- Методы, возвращаемые
__getattribute__
и __getattr__
, должны иметь свои возвращаемые значения, приведенные к целевому типу. То же самое относится и к дункерам, называемым операторами.
- Некоторые методы должны быть исключены из # 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
).