【问题标题】:Enforcing Class Variables in a Subclass在子类中强制执行类变量
【发布时间】:2014-03-29 13:48:10
【问题描述】:

我正在为 App Engine 扩展 Python webapp2 Web 框架,以引入一些缺失的功能(以便更快、更轻松地创建应用程序)。

这里的一个要求是每个子类需要有一些特定的静态类变量。实现这一点的最佳方法是在我使用它们时简单地抛出异常,还是有更好的方法?

示例(非真实代码):

子类:

class Bar(Foo):
  page_name = 'New Page'

page_name 需要存在才能在此处处理:

page_names = process_pages(list_of_pages)

def process_pages(list_of_pages)
  page_names = []

  for page in list_of_pages:
    page_names.append(page.page_name)

  return page_names

【问题讨论】:

  • 这更像是一个 Python 问题,而不是 App Engine 问题。
  • 您能否提供SSCCE/MVCE 来支持您的问题?
  • 顺便说一句,def process_pages(list_of_pages): return [ page.page_name for page in list_of_pages ]

标签: python subclass class-variables


【解决方案1】:

如果您尝试使用不存在的属性,Python 将已经抛出异常。这是一种完全合理的方法,因为错误消息会清楚地表明该属性需要存在。在可能的情况下,在基类中为这些属性提供合理的默认值也是常见的做法。如果您需要属性或方法,抽象基类是很好的选择,但它们不适用于数据属性,并且在类实例化之前不会引发错误。

如果您想尽快失败,元类甚至可以阻止用户在不包含属性的情况下定义类。元类的好处是它是可继承的,所以如果你在基类上定义它,它会自动用于任何从它派生的类。

这是一个元类;事实上,这里有一个元类factory,它可以让你轻松地传入你想要的属性名称。

def build_required_attributes_metaclass(*required_attrs):

    class RequiredAttributesMeta(type):
        def __init__(cls, name, bases, attrs):
            missing_attrs = ["'%s'" % attr for attr in required_attrs 
                             if not hasattr(cls, attr)]
            if missing_attrs:
                raise AttributeError("class '%s' requires attribute%s %s" %
                                     (name, "s" * (len(missing_attrs) > 1), 
                                      ", ".join(missing_attrs)))
    return RequiredAttributesMeta

现在使用这个元类实际定义一个基类有点棘手。您必须定义属性来定义类,这是元类的全部点,但如果属性是在基类上定义的,它们也会在从它派生的任何类上定义,从而违背了目的。所以我们要做的是定义它们(使用虚拟值),然后从类中删除它们。

class Base(object):
    __metaclass__ = build_required_attributes_metaclass("a", "b" ,"c")
    a = b = c = 0

del Base.a, Base.b, Base.c

现在如果你尝试定义一个子类,但不定义属性:

class Child(Base):
    pass

你得到:

AttributeError: class 'Child' requires attributes 'a', 'b', 'c'

注意我对 Google App Engine 没有任何经验,因此它可能已经使用了元类。在这种情况下,您希望您的 RequiredAttributesMeta 派生自该元类,而不是 type

【讨论】:

    【解决方案2】:

    Abstract Base Classes 允许声明一个属性抽象,这将强制所有实现类具有该属性。我只是为了完整起见提供这个例子,许多pythonistas认为你提出的解决方案更pythonic。

    import abc
    
    class Base(object):
        __metaclass__ = abc.ABCMeta
    
        @abc.abstractproperty
        def value(self):
            return 'Should never get here'
    
    
    class Implementation1(Base):
    
        @property
        def value(self):
            return 'concrete property'
    
    
    class Implementation2(Base):
        pass # doesn't have the required property
    

    尝试实例化第一个实现类:

    print Implementation1()
    Out[6]: <__main__.Implementation1 at 0x105c41d90>
    

    尝试实例化第二个实现类:

    print Implementation2()
    ---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    <ipython-input-4-bbaeae6b17a6> in <module>()
    ----> 1 Implementation2()
    
    TypeError: Can't instantiate abstract class Implementation2 with abstract methods value
    

    【讨论】:

    • 不过,这似乎是一种完全的矫枉过正,特别是因为你仍然需要一个 try-except 块来围绕正在使用它的任何东西。至少在我看来,最好使用 Python 的常见做法来“请求宽恕而不是请求许可”。 ABC 还有其他美妙的用法,我只是觉得这个用例有点过头了。
    • 虽然我同意这可能是矫枉过正,但它是一种替代方法,并且还提到其他人可能认为我提到的解决方案更 Pythonic。谢谢!
    • 'abc.abstractproperty' is deprecated since Python 3.3. Use 'property' with 'abc.abstractmethod' instead 所以现在可以使用@property @abstractmethod 装饰器了。
    【解决方案3】:

    在描述我的解决方案之前,先给大家介绍一下Python类实例是如何创建的:

    图1:Python实例创建[1]

    根据上面的描述,你可以看到在 Python 中类的实例实际上是由一个 Metaclass 创建的。正如我们所见,当调用者创建我们类的实例时,首先调用__call__ 魔术方法,该方法依次调用类的__new____init__,然后__cal__ 返回对象实例返回给调用者。

    话虽如此,我们可以简单地尝试检查__init__ 创建的实例是否确实定义了那些“必需”属性。

    元类

    class ForceRequiredAttributeDefinitionMeta(type):
        def __call__(cls, *args, **kwargs):
            class_object = type.__call__(cls, *args, **kwargs)
            class_object.check_required_attributes()
            return class_object
    

    正如您在__call__ 中看到的,我们所做的是创建类对象,然后调用其check_required_attributes() 方法,该方法将检查是否已定义所需的属性。如果没有定义所需的属性,我们应该简单地抛出一个错误。

    超类

    Python 2

    class ForceRequiredAttributeDefinition(object):
        __metaclass__ = ForceRequiredAttributeDefinitionMeta
        starting_day_of_week = None
    
        def check_required_attributes(self):
            if self.starting_day_of_week is None:
                raise NotImplementedError('Subclass must define self.starting_day_of_week attribute. \n This attribute should define the first day of the week.')
    

    Python 3

    class ForceRequiredAttributeDefinition(metaclass=ForceRequiredAttributeDefinitionMeta):
        starting_day_of_week = None
    
        def check_required_attributes(self):
            if self.starting_day_of_week is None:
                raise NotImplementedError('Subclass must define self.starting_day_of_week attribute. \n This attribute should define the first day of the week.')
    

    这里我们定义了实际的超类。三件事:

    • 应该使用我们的元类。
    • 应将所需属性定义为None,参见starting_day_of_week = None
    • 应实现check_required_attributes 方法,检查所需属性是否为None,以及是否要向用户抛出带有合理错误消息的NotImplementedError

    工作和非工作子类的示例

    class ConcereteValidExample(ForceRequiredAttributeDefinition):
        def __init__(self):
            self.starting_day_of_week = "Monday"
    
    
    class ConcereteInvalidExample(ForceRequiredAttributeDefinition):
        def __init__(self):
            # This will throw an error because self.starting_day_of_week is not defined.
            pass
    

    输出

    Traceback (most recent call last):
      File "test.py", line 50, in <module>
        ConcereteInvalidExample()  # This will throw an NotImplementedError straightaway
      File "test.py", line 18, in __call__
        obj.check_required_attributes()
      File "test.py", line 36, in check_required_attributes
        raise NotImplementedError('Subclass must define self.starting_day_of_week attribute. \n This attribute should define the first day of the week.')
    NotImplementedError: Subclass must define self.starting_day_of_week attribute.
     This attribute should define the first day of the week.
    

    如您所见,成功创建的第一个实例定义了所需的属性,第二个实例直接引发了NotImplementedError

    【讨论】:

      【解决方案4】:

      这行得通。甚至会阻止子类被定义,更不用说实例化了。

      class Foo:
      
          page_name = None
          author = None
      
          def __init_subclass__(cls, **kwargs):
              for required in ('page_name', 'author',):
                  if not getattr(cls, required):
                      raise TypeError(f"Can't instantiate abstract class {cls.__name__} without {required} attribute defined")
              return super().__init_subclass__(**kwargs)
      
      
      class Bar(Foo):
          page_name = 'New Page'
          author = 'eric'
      

      【讨论】:

        【解决方案5】:

        一般而言,在 Python 中,人们普遍认为,处理这种情况的最佳方法,正如您正确建议的那样,是用 try-except 块包装需要此类变量的任何操作。

        【讨论】:

          【解决方案6】:

          我爱this answer。 最好的方法是一次性的。对其他读者来说,远没有元类那么可怕。

          但是,如果您希望将其作为通用工具插入到很多地方,元类就很棒。我借鉴了其他一些答案,但还添加了bases 检查,以便您可以在mixin 中使用它,并且mixin 本身不会触发它。可以添加类似的检查以跳过 ABC。

          def RequiredAttributes(*required_attrs):
              class RequiredAttributesMeta(type):
                  def __init__(cls, name, bases, attrs):
                      if not bases:
                          return  # No bases implies mixin. Mixins aren't the final class, so they're exempt.
                      if missing_attrs := [attr for attr in required_attrs if not hasattr(cls, attr)]:
                          raise AttributeError(f"{name!r} requires attributes: {missing_attrs}")
              return RequiredAttributesMeta
          

          然后像这样使用:

          class LicenseAccessMixin(metaclass=RequiredAttributes('access_control')):
              ...  # define methods that safely refer to `self.access_control`.
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2018-04-04
            • 2018-09-26
            • 1970-01-01
            • 1970-01-01
            • 2012-11-26
            • 2015-10-24
            • 1970-01-01
            • 2015-09-21
            相关资源
            最近更新 更多