Как сделать атрибут класса исключительным для суперкласса

У меня есть мастер-класс для планеты:

class Planet: def __init__(self,name): self.name = name (...) def destroy(self): (...) 

У меня также есть несколько классов, которые наследуют от Planet и я хочу, чтобы один из них не мог быть уничтожен (чтобы не наследовать функцию destroy )

Пример:

 class Undestroyable(Planet): def __init__(self,name): super().__init__(name) (...) #Now it shouldn't have the destroy(self) function 

Поэтому, когда это выполняется,

 Undestroyable('This Planet').destroy() 

это должно привести к ошибке, например:

 AttributeError: Undestroyable has no attribute 'destroy' 

5 Solutions collect form web for “Как сделать атрибут класса исключительным для суперкласса”

Подход 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 атрибут удаления атрибутов в экземплярах – и если вы попытаетесь обработать метод «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 

И с помощью этого метода даже попытка получить или проверить метод existense на самом классе будет работать:

 In [129]: BorgWorld.destroy ... AttributeError: Objects of type 'BorgWorld' have no 'destroy' method In [130]: hasattr(BorgWorld, "destroy") Out[130]: False 

метакласса с помощью специального метода __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__ object не использует 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 

Если Undestroyable является уникальным (или, по крайней мере, необычным) случаем, вероятно, проще всего переименовать destroy() :

 class Undestroyable(Planet): # ... def destroy(self): cls_name = self.__class__.__name__ raise AttributeError("%s has no attribute 'destroy'" % cls_name) 

С точки зрения пользователя класса, это будет вести себя так, как будто Undestroyable.destroy() не существует …, если они не hasattr(Undestroyable, 'destroy') с hasattr(Undestroyable, 'destroy') , что всегда возможно.

Если чаще всего вы хотите, чтобы подклассы наследовали некоторые свойства, а не другие, подход mixin в ответе Chepner скорее всего будет более удобным . Вы можете улучшить его, сделав Destructible абстрактным базовым классом :

 from abc import abstractmethod, ABCMeta class Destructible(metaclass=ABCMeta): @abstractmethod def destroy(self): pass class BasePlanet: # ... pass class Planet(BasePlanet, Destructible): def destroy(self): # ... pass class IndestructiblePlanet(BasePlanet): # ... pass 

Это имеет то преимущество, что если вы попытаетесь создать экземпляр абстрактного класса Destructible , вы получите сообщение об ошибке, указывающее на проблему:

 >>> Destructible() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: Can't instantiate abstract class Destructible with abstract methods destroy 

… аналогично, если вы наследуете от Destructible но забудьте определить destroy() :

 class InscrutablePlanet(BasePlanet, Destructible): pass 

 >>> InscrutablePlanet() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: Can't instantiate abstract class InscrutablePlanet with abstract methods destroy 

Вместо удаления атрибута, который наследуется, наследуйте destroy только в подклассах, где это применимо, через класс mix-in. Это сохраняет правильную семантику наследования «есть-а».

 class Destructible(object): def destroy(self): pass class BasePlanet(object): ... class Planet(BasePlanet, Destructible): ... class IndestructiblePlanet(BasePlanet): # Does *not* inherit from Destructible ... 

Вы можете предоставить соответствующие определения для destroy в любом из Destructible , Planet или любого класса, который наследует от Planet .

Метаклассы и протоколы дескрипторов – это весело, но, возможно, перебор. Иногда, для сырой функциональности, вы не можете победить хороший ole ' __slots__ .

 class Planet(object): def __init__(self, name): self.name = name def destroy(self): print("Boom! %s is toast!\n" % self.name) class Undestroyable(Planet): __slots__ = ['destroy'] def __init__(self,name): super().__init__(name) print() x = Planet('Pluto') # Small, easy to destroy y = Undestroyable('Jupiter') # Too big to fail x.destroy() y.destroy() Boom! Pluto is toast! Traceback (most recent call last): File "planets.py", line 95, in <module> y.destroy() AttributeError: destroy 

Вы не можете наследовать только часть класса. Все это или ничего.

Что вы можете сделать, так это поставить функцию destroy на втором уровне класса, например, у вас есть класс Planet без функции destry, а затем вы создадите класс DestroyablePlanet, в который вы добавляете функцию destroy, которая все использование разрушаемых планет.

Или вы можете поместить флаг в конструкцию Planet-Class, который определяет, сможет ли функция destroy выполнить успешное выполнение или нет, а затем проверяется в функции destroy.

  • ctypes - Новичок
  • Разница между супер () и вызывающим суперклассом напрямую
  • Попытка поиска первого восходящего прогона в списке Python
  • Python 3.3 Изображение TKinter не существует
  • Python: цикл над последовательными символами?
  • Есть еще web.py для python3?
  • запуск imagemagick convert (консольное приложение) из python
  • Почему Python не распознает исходный файл, закодированный utf-8?
  •  
    Interesting Posts for Van-Lav
    Python - лучший язык программирования в мире.