如何使类属性专属于超类

cai*_*ing 13 python oop inheritance class python-3.x

我有一个星球大师班:

class Planet:

    def __init__(self,name):
        self.name = name
        (...)

    def destroy(self):
        (...)
Run Code Online (Sandbox Code Playgroud)

我还有一些继承的类,Planet我想让其中一个无法被销毁(不继承该destroy函数)

例:

class Undestroyable(Planet):

    def __init__(self,name):
        super().__init__(name)
        (...)

    #Now it shouldn't have the destroy(self) function
Run Code Online (Sandbox Code Playgroud)

所以当这个运行时,

Undestroyable('This Planet').destroy()
Run Code Online (Sandbox Code Playgroud)

它应该产生如下错误:

AttributeError: Undestroyable has no attribute 'destroy'
Run Code Online (Sandbox Code Playgroud)

jsb*_*eno 16

其他答案中的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
Run Code Online (Sandbox Code Playgroud)

并在交互式提示符上:

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
Run Code Online (Sandbox Code Playgroud)

感觉不像简单地重写方法,这种方法在检索属性时会引发错误 - 甚至会使hasattr工作:

In [113]: hasattr(BorgWorld(), "destroy")
Out[113]: False
Run Code Online (Sandbox Code Playgroud)

虽然,如果试图直接从类中检索方法而不是从实例中检索方法,它将无法工作 - 在这种情况下,instance参数to __get__设置为None,我们不能说从哪个类检索它 - 只是owner宣布它的类.

In [114]: BorgWorld.destroy
Out[114]: <function __main__.Planet.destroy>
Run Code Online (Sandbox Code Playgroud)

第二种方法:__delattr__关于元类:

在编写上述内容时,我发现Pythn确实有 __delattr__特殊的方法.如果Planet类本身实现__delattr__并且我们尝试删除destroy 特定派生类的方法,那么它就不会起作用:__delattr__在实例中检查属性的属性删除 - 如果你试图在实例中del使用"destroy"方法,它无论如何都会失败,因为这个方法在课堂上.

但是,在Python中,类本身就是一个实例 - 它的"元类".那通常是type.__delattr__通过在创建类之后发出"del UndestructiblePlanet.destroy","Planet"的元类上的正确可以使"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    
Run Code Online (Sandbox Code Playgroud)

使用这种方法,即使尝试检索或检查类本身上的方法存在也会起作用:

In [129]: BorgWorld.destroy
...
AttributeError: Objects of type 'BorgWorld' have no 'destroy' method

In [130]: hasattr(BorgWorld, "destroy")
Out[130]: False
Run Code Online (Sandbox Code Playgroud)

使用自定义__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
Run Code Online (Sandbox Code Playgroud)

('删除'描述符是将方法标记为'已删除'的正确形式 - 但在此方法中,它无法在类创建时知道类名称)

作为一个类装饰器:

并且给定"已删除"描述符,可以简单地将方法作为类装饰器通知 - 在这种情况下不需要元类:

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
Run Code Online (Sandbox Code Playgroud)

修改__getattribute__机制:

为了完整起见 - 真正让Python在超类中获取方法和属性的原因是__getattribute__调用内部发生了什么.n的object版本__getattribute__是对具有用于属性检索的"数据描述符,实例,类,基类链......"的优先级的算法进行编码.

因此,更改该类是一个容易的唯一点,以获得"合法"属性错误,而不需要在以前的方法上使用"不存在"的descritor.

问题是,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
Run Code Online (Sandbox Code Playgroud)


Zer*_*eus 9

如果Undestroyable是一个独特的(或至少是不寻常的)情况,最简单的方法就是重新定义destroy():

class Undestroyable(Planet):

    # ...

    def destroy(self):
        cls_name = self.__class__.__name__
        raise AttributeError("%s has no attribute 'destroy'" % cls_name)
Run Code Online (Sandbox Code Playgroud)

从类的用户的角度来看,这将表现得好像Undestroyable.destroy()不存在......除非他们四处寻找hasattr(Undestroyable, 'destroy'),这总是可能的.

如果更频繁地发生你希望子类继承某些属性而不是其他属性,那么chepner答案中的mixin方法可能更易于维护.您可以通过创建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
Run Code Online (Sandbox Code Playgroud)

这样做的好处是,如果您尝试实例化抽象类Destructible,您将收到指向该问题的错误:

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

...同样,如果你继承Destructible但忘记定义destroy():

class InscrutablePlanet(BasePlanet, Destructible):
    pass
Run Code Online (Sandbox Code Playgroud)

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


che*_*ner 5

不是删除继承的属性,而是仅destroy通过混合类在适用的子类中继承.这保留了正确的"is-a"继承语义.

class Destructible(object):
    def destroy(self):
        pass

class BasePlanet(object):
    ...

class Planet(BasePlanet, Destructible):
    ...

class IndestructiblePlanet(BasePlanet):  # Does *not* inherit from Destructible
    ...
Run Code Online (Sandbox Code Playgroud)

您可以提供合适的定义destroy在任意的Destructible,Planet或任何来自继承类Planet.