Ответ 1
Подход mixin в других ответах хорош и, вероятно, лучше для большинства случаев. Но, тем не менее, это портит часть удовольствия - может быть, обязывает вас иметь отдельные планеты-иерархии - подобно тому, чтобы жить с двумя абстрактными классами каждый предок "разрушаемых" и "не разрушаемых".
Первый подход: декоратор декоратора
Но у Python есть мощный механизм, называемый "протоколом дескриптора", который используется для извлечения любого атрибута из класса или экземпляра - он даже используется для обычного извлечения методов из экземпляров, поэтому можно настроить метод поиск в том, как он проверяет, должен ли он "принадлежать" этому классу и в противном случае повышать ошибку атрибута.
В протоколе дескриптора указано, что всякий раз, когда вы пытаетесь получить какой-либо атрибут из объекта экземпляра в Python, Python проверяет, существует ли атрибут в этом классе объекта, и если да, то если сам атрибут имеет метод с именем __get__
. Если он имеет, __get__
вызывается (с экземпляром и классом, где он определяется как параметры) - и все, что он возвращает, является атрибутом. Python использует это для реализации методов: функции в Python 3 имеют метод __get__
, который при вызове возвращает другой вызываемый объект, который, в свою очередь, при вызове будет вставлять параметр self
в вызов исходной функции.
Таким образом, можно создать класс, метод __get__
будет решать, следует ли возвращать функцию как связанный метод или нет, в зависимости от того, какой внешний класс был помечен как таковой - например, он мог проверять определенный флаг non_destrutible
. Это можно сделать, используя декоратор, чтобы обернуть метод с помощью этой функции дескриптора
class Muteable:
def __init__(self, flag_attr):
self.flag_attr = flag_attr
def __call__(self, func):
"""Called when the decorator is applied"""
self.func = func
return self
def __get__(self, instance, owner):
if instance and getattr(instance, self.flag_attr, False):
raise AttributeError('Objects of type {0} have no {1} method'.format(instance.__class__.__name__, self.func.__name__))
return self.func.__get__(instance, owner)
class Planet:
def __init__(self, name=""):
pass
@Muteable("undestroyable")
def destroy(self):
print("Destroyed")
class BorgWorld(Planet):
undestroyable = True
И в интерактивном приглашении:
In [110]: Planet().destroy()
Destroyed
In [111]: BorgWorld().destroy()
...
AttributeError: Objects of type BorgWorld have no destroy method
In [112]: BorgWorld().destroy
AttributeError: Objects of type BorgWorld have no destroy method
Поймите, что в отличие от просто переопределения метода этот подход вызывает ошибку при извлечении атрибута - и даже сделает работу hasattr
:
In [113]: hasattr(BorgWorld(), "destroy")
Out[113]: False
Хотя это не сработает, если вы попытаетесь извлечь метод непосредственно из класса, а не из экземпляра - в этом случае параметр instance
на __get__
установлен на None, и мы не можем скажем, из какого класса он был извлечен - только класс owner
, где он был объявлен.
In [114]: BorgWorld.destroy
Out[114]: <function __main__.Planet.destroy>
Второй подход: __delattr__
в метаклассе:
При написании вышеизложенного мне пришло в голову, что у Pythn есть специальный метод __delattr__
. Если класс Planet
реализует __delattr__
, и мы попытаемся удалить метод destroy
для определенных классов, это wuld nt work: __delattr__
gards удаление атрибутов атрибутов в экземплярах - и если вы попробуйте del
метод "destroy" в экземпляре, он все равно потерпит неудачу, так как метод находится в классе.
Однако в Python сам класс является экземпляром своего "метакласса". Обычно это type
. Правильный __delattr__
в метаклассе "Планета" может сделать возможным "disinheitance" метода "destroy", выпуская "del UndestructiblePlanet.destroy" после создания класса.
Опять же, мы используем протокол дескриптора, чтобы иметь надлежащий "удаленный метод в подклассе":
class Deleted:
def __init__(self, cls, name):
self.cls = cls.__name__
self.name = name
def __get__(self, instance, owner):
raise AttributeError("Objects of type '{0}' have no '{1}' method".format(self.cls, self.name))
class Deletable(type):
def __delattr__(cls, attr):
print("deleting from", cls)
setattr(cls, attr, Deleted(cls, attr))
class Planet(metaclass=Deletable):
def __init__(self, name=""):
pass
def destroy(self):
print("Destroyed")
class BorgWorld(Planet):
pass
del BorgWorld.destroy
И с помощью этого метода даже попытка получить или проверить метод существования на самом классе будет работать:
In [129]: BorgWorld.destroy
...
AttributeError: Objects of type 'BorgWorld' have no 'destroy' method
In [130]: hasattr(BorgWorld, "destroy")
Out[130]: False
metaclass с пользовательским методом __prepare__
.
Так как метаклассы позволяют настраивать объект, содержащий пространство имен классов, возможно иметь объект, который отвечает за оператор del
внутри тела класса, добавляя дескриптор Deleted
.
Для пользователя (программиста), использующего этот метакласс, это почти что-то, но для оператора del
разрешено в самом классе класса:
class Deleted:
def __init__(self, name):
self.name = name
def __get__(self, instance, owner):
raise AttributeError("No '{0}' method on class '{1}'".format(self.name, owner.__name__))
class Deletable(type):
def __prepare__(mcls,arg):
class D(dict):
def __delitem__(self, attr):
self[attr] = Deleted(attr)
return D()
class Planet(metaclass=Deletable):
def destroy(self):
print("destroyed")
class BorgPlanet(Planet):
del destroy
(Дескриптор "удаленный" - это правильная форма, чтобы пометить метод как "удаленный" - в этом методе, хотя он не может знать имя класса во время создания класса)
В качестве декоратора класса:
И учитывая "удаленный" дескриптор, можно просто сообщить методы, которые нужно удалить в качестве декоратора класса, - в этом случае нет необходимости в метаклассе:
class Deleted:
def __init__(self, cls, name):
self.cls = cls.__name__
self.name = name
def __get__(self, instance, owner):
raise AttributeError("Objects of type '{0}' have no '{1}' method".format(self.cls, self.name))
def mute(*methods):
def decorator(cls):
for method in methods:
setattr(cls, method, Deleted(cls, method))
return cls
return decorator
class Planet:
def destroy(self):
print("destroyed")
@mute('destroy')
class BorgPlanet(Planet):
pass
Изменение механизма __getattribute__
:
Для полноты - то, что действительно делает методы и атрибуты Python для суперкласса, - это то, что происходит внутри вызова __getattribute__
. n версия object
__getattribute__
- это где закодирован алгоритм с приоритетами для "дескриптор данных, экземпляр, класс, цепочка базовых классов..." для извлечения атрибутов.
Итак, изменение этого для класса является простой уникальной точкой для получения "законной" ошибки атрибута, без необходимости "несуществующего" дескриптора, используемого в предыдущих методах.
Проблема заключается в том, что object
__getattribute__
не использует type
один для поиска атрибута в классе - если бы это так, просто реализовать __getattribute__
в метаклассе было бы достаточно. Нужно сделать это на экземпляре, чтобы избежать экземпляра lookp метода и метакласса, чтобы избежать метаклассического поиска. Например, метакласс может вводить необходимый код:
def blocker_getattribute(target, attr, attr_base):
try:
muted = attr_base.__getattribute__(target, '__muted__')
except AttributeError:
muted = []
if attr in muted:
raise AttributeError("object {} has no attribute '{}'".format(target, attr))
return attr_base.__getattribute__(target, attr)
def instance_getattribute(self, attr):
return blocker_getattribute(self, attr, object)
class M(type):
def __init__(cls, name, bases, namespace):
cls.__getattribute__ = instance_getattribute
def __getattribute__(cls, attr):
return blocker_getattribute(cls, attr, type)
class Planet(metaclass=M):
def destroy(self):
print("destroyed")
class BorgPlanet(Planet):
__muted__=['destroy'] # or use a decorator to set this! :-)
pass