Вот список требований, которым должно удовлетворять решение:
- Создание устаревшего класса должно вызвать предупреждение
- Подклассы устаревшего класса должны вызывать предупреждение
- Поддержка
isinstance
и issubclass
чеки
Решение
Этого можно достичь с помощью пользовательского метакласса:
class DeprecatedClassMeta(type):
def __new__(cls, name, bases, classdict, *args, **kwargs):
alias = classdict.get('_DeprecatedClassMeta__alias')
if alias is not None:
def new(cls, *args, **kwargs):
alias = getattr(cls, '_DeprecatedClassMeta__alias')
if alias is not None:
warn("{} has been renamed to {}, the alias will be "
"removed in the future".format(cls.__name__,
alias.__name__), DeprecationWarning, stacklevel=2)
return alias(*args, **kwargs)
classdict['__new__'] = new
classdict['_DeprecatedClassMeta__alias'] = alias
fixed_bases = []
for b in bases:
alias = getattr(b, '_DeprecatedClassMeta__alias', None)
if alias is not None:
warn("{} has been renamed to {}, the alias will be "
"removed in the future".format(b.__name__,
alias.__name__), DeprecationWarning, stacklevel=2)
# Avoid duplicate base classes.
b = alias or b
if b not in fixed_bases:
fixed_bases.append(b)
fixed_bases = tuple(fixed_bases)
return super().__new__(cls, name, fixed_bases, classdict,
*args, **kwargs)
def __instancecheck__(cls, instance):
return any(cls.__subclasscheck__(c)
for c in {type(instance), instance.__class__})
def __subclasscheck__(cls, subclass):
if subclass is cls:
return True
else:
return issubclass(subclass, getattr(cls,
'_DeprecatedClassMeta__alias'))
Объяснение
DeprecatedClassMeta.__new__
метод вызывается не только для класса, метаклассом которого он является, но и для каждого подкласса этого класса. Это дает возможность гарантировать, что ни один экземпляр DeprecatedClass
никогда не будет создан или переклассифицирован.
Инстанциация проста. Метакласс переопределяет метод __new__
DeprecatedClass
, чтобы всегда возвращать экземпляр NewClass
.
Субклассирование не намного сложнее. DeprecatedClassMeta.__new__
получает список базовых классов и должен заменить экземпляры DeprecatedClass
на NewClass
.
Наконец, проверки isinstance
и issubclass
осуществляются через __instancecheck__
и __subclasscheck__
, определенные в PEP 3119 .
Test
class NewClass:
foo = 1
class NewClassSubclass(NewClass):
pass
class DeprecatedClass(metaclass=DeprecatedClassMeta):
_DeprecatedClassMeta__alias = NewClass
class DeprecatedClassSubclass(DeprecatedClass):
foo = 2
class DeprecatedClassSubSubclass(DeprecatedClassSubclass):
foo = 3
assert issubclass(DeprecatedClass, DeprecatedClass)
assert issubclass(DeprecatedClassSubclass, DeprecatedClass)
assert issubclass(DeprecatedClassSubSubclass, DeprecatedClass)
assert issubclass(NewClass, DeprecatedClass)
assert issubclass(NewClassSubclass, DeprecatedClass)
assert issubclass(DeprecatedClassSubclass, NewClass)
assert issubclass(DeprecatedClassSubSubclass, NewClass)
assert isinstance(DeprecatedClass(), DeprecatedClass)
assert isinstance(DeprecatedClassSubclass(), DeprecatedClass)
assert isinstance(DeprecatedClassSubSubclass(), DeprecatedClass)
assert isinstance(NewClass(), DeprecatedClass)
assert isinstance(NewClassSubclass(), DeprecatedClass)
assert isinstance(DeprecatedClassSubclass(), NewClass)
assert isinstance(DeprecatedClassSubSubclass(), NewClass)
assert NewClass().foo == 1
assert DeprecatedClass().foo == 1
assert DeprecatedClassSubclass().foo == 2
assert DeprecatedClassSubSubclass().foo == 3