【问题标题】:How to make a class attribute exclusive to the super class如何使超类专有的类属性
【发布时间】:2017-08-18 17:08:59
【问题描述】:

我有一个关于行星的大师班:

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'

【问题讨论】:

    标签: python python-3.x class oop inheritance


    【解决方案1】:

    如果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'),这总是有可能的。

    如果您希望子类继承某些属性而不是其他属性的情况更频繁,chepner's answer 中的 mixin 方法可能更易于维护。您可以通过将Destructible 设置为abstract base class 来进一步改进它:

    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
    

    【讨论】:

      【解决方案2】:

      与其删除继承的属性,不如仅通过混合类在适用的子类中继承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
          ...
      

      您可以在DestructiblePlanet 或从Planet 继承的任何类中为destroy 提供合适的定义。

      【讨论】:

        【解决方案3】:

        你不能只继承一个类的一部分。要么全有,要么全无。

        你可以做的是把destroy函数放在类的第二层,这样你就有了没有destry函数的Planet类,然后你在添加destroy函数的地方创建了一个DestroyablePlanet-Class,所有可摧毁的行星都使用它。

        或者您可以在 Planet-Class 的构造中放置一个标志,以确定销毁函数是否能够成功,然后在销毁函数中进行检查。

        【讨论】:

          【解决方案4】:

          其他答案中的 mixin 方法很好,并且在大多数情况下可能更好。但是,尽管如此,它还是破坏了一部分乐趣——也许你不得不拥有单独的行星层次结构——比如不得不与两个抽象类一起生活,每个类都是“可破坏”和“不可破坏”的祖先。

          第一种方法:描述符装饰器

          但是 Python 有一个强大的机制,称为“描述符协议”,它用于从类或实例中检索任何属性——它甚至通常用于从实例中检索方法——因此,可以自定义方法以一种检查它是否“应该属于”该类的方式进行检索,否则会引发属性错误。

          描述符协议规定,每当您尝试从 Python 中的实例对象获取任何属性时,Python 将检查该属性是否存在于该对象的类中,如果存在,则该属性本身是否具有名为 __get__ 的方法。如果有,则调用__get__(将其定义为参数的实例和类)-它返回的都是属性。 Python 使用它来实现方法:Python 3 中的函数有一个 __get__ 方法,当调用该方法时,将返回另一个可调用对象,反过来,在调用时将在对原始函数的调用中插入 self 参数。

          因此,可以创建一个类,其__get__ 方法将根据标记为的外部类决定是否将函数作为绑定方法返回 - 例如,它可以检查特定标志 @ 987654326@。这可以通过使用装饰器来包装具有此描述符功能的方法来完成

          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
          

          虽然,如果尝试直接从类中而不是从实例中检索方法,它将不起作用 - 在这种情况下,__get__instance 参数设置为 None,我们不能说它是从哪个类中检索到的 - 只是声明它的 owner 类。

          In [114]: BorgWorld.destroy
          Out[114]: <function __main__.Planet.destroy>
          

          第二种方法:元类上的__delattr__

          在编写上述内容时,我突然想到 Pythn 确实有 __delattr__ 特殊方法。如果 Planet 类本身实现了 __delattr__ 并且我们尝试删除特定派生类上的 destroy 方法,它不会起作用:__delattr__ gards 删除实例中的属性 - 如果你愿意尝试del实例中的“destroy”方法,无论如何它都会失败,因为该方法在类中。

          然而,在 Python 中,类本身就是一个实例——它的“元类”。这通常是 type 。在“Planet”的元类上适当的__delattr__ 可以通过在类创建后发出“del UndestructiblePlanet.destroy”来实现“destroy”方法的“disinheitance”。

          再次,我们使用描述符协议在子类上有一个适当的“删除方法”:

          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
          

          具有自定义 __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__ 调用中发生的事情。在object 版本的__getattribute__ 中,对属性检索的“数据描述符、实例、类、基类链......”具有优先级的算法进行编码。

          因此,为类更改它是一个容易获得“合法”属性错误的独特点,而无需在以前的方法中使用“不存在”描述符。

          问题是object__getattribute__ 没有使用type 来搜索类中的属性——如果这样做了,只需在元类上实现__getattribute__ 就足够了.必须在实例上执行此操作以避免方法的实例查找,并在元类上执行此操作以避免元类查找。当然,元类可以注入所需的代码:

          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
          

          【讨论】:

          • 我想知道是否有人会为这种(可笑地过度设计,但非常有趣)描述符/元类方法而烦恼。致敬,先生!如果您添加 @mute('destroy') 类装饰器以避免 del 语句,则 +50 赏金;-)
          • @ZeroPiraeus: 嗯...类装饰器——甚至不需要元类,因为“删除”的描述符可以直接由它设置!
          • 太棒了 :-) 带有自定义 __prepare__ 的元类不起作用(一旦缩进错误和不正确的 BorgPlanet 超类被修复,del destroy 引发 NameError),但是类装饰器的效果非常好 :-) 一旦 48 小时限制结束,我将申请赏金。
          • 抱歉 - 由于 Deleted.__init__ 中的签名错误,__prepare__ 已损坏 - 这是 Python 转换异常的情况之一(在这种情况下,来自调用的 TypeError由于__delitem__ 失败而导致的 NameError)
          【解决方案5】:

          元类和描述符协议很有趣,但可能有点矫枉过正。有时,对于原始功能,您无法击败优秀的 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
          

          【讨论】:

          • 为什么Undestroyable中需要__init__方法?我尝试在没有__init__ 定义的情况下实现Undestroyable,结果仍然相同。你有什么特别的原因吗?
          • 它说here 表示虽然从没有__slots__ 的类继承,但该类的__dict__ 始终是可访问的,因此定义__slots__ 没有用吗?您能否解释一下在这种情况下为什么/如何不同?
          • @ShikharChauhan 这只是为了表明__init__ 方法可能存在(并在必要时用于初始化其他东西)。你是对的,对于最小的例子,它不是必需的。
          • @ShikharChauhan 关于__slots__ 的说法是正确的,因为对象实例仍然有__dict__。但是方法属性在类__dict__,而不是实例__dict__
          猜你喜欢
          • 1970-01-01
          • 2021-03-13
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-12-04
          • 1970-01-01
          • 2019-01-30
          相关资源
          最近更新 更多