Подтвердить что ты не робот

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

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

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'
4b9b3361

Ответ 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

Ответ 2

Если 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'), что всегда возможно.

Если чаще всего вы хотите, чтобы подклассы наследовали некоторые свойства, а не другие, метод mixin в chepner answer, скорее всего, будет более ремонтопригодным. Вы можете улучшить его, сделав 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

Ответ 3

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

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.

Ответ 4

Метаклассы и протоколы дескрипторов - это весело, но, возможно, перебор. Иногда для сырой функциональности вы не можете бить хорошего 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

Ответ 5

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

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

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