Aplicar variables de clase en una subclase

Estoy trabajando en extender el marco web de Python webapp2 para que App Engine incluya algunas características que faltan (para que crear aplicaciones sea un poco más rápido y más fácil).

Uno de los requisitos aquí es que cada subclase necesita tener algunas variables de clase estáticas específicas. ¿Es la mejor manera de lograr esto simplemente lanzar una excepción si faltan cuando las utilizo o hay una manera mejor?

Ejemplo (no código real):

Subclase:

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

page_name debe estar presente para ser procesado aquí:

 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 

Las clases base abstractas permiten declarar un resumen de propiedad, lo que obligará a todas las clases implementadoras a tener la propiedad. Solo ofrezco este ejemplo para completar, muchos pythonistas piensan que su solución propuesta es más 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 

Tratando de crear una instancia de la primera clase de implementación:

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

Tratando de crear una instancia de la segunda clase de implementación:

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

Python ya lanzará una excepción si intentas usar un atributo que no existe. Ese es un enfoque perfectamente razonable, ya que el mensaje de error dejará claro que el atributo debe estar allí. También es una práctica común proporcionar valores predeterminados razonables para estos atributos en la clase base, siempre que sea posible. Las clases base abstractas son buenas si necesita requerir propiedades o métodos, pero no funcionan con atributos de datos y no generan un error hasta que se crea una instancia de la clase.

Si desea fallar lo más rápido posible, una metaclase puede evitar que el usuario incluso defina la clase sin incluir los atributos. Lo bueno de una metaclase es que es heredable, por lo que si la define en una clase base, se usa automáticamente en cualquier clase derivada de ella.

Aquí hay tal metaclase; de hecho, aquí hay una fábrica de metaclase que le permite pasar fácilmente los nombres de atributo que desea solicitar.

 def RequiredAttributes(*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 

Ahora definir realmente una clase base con esta metaclase es un poco complicado. Debe definir los atributos para definir la clase, que es el punto completo de la metaclase, pero si los atributos se definen en la clase base, también se definen en cualquier clase derivada de ella, lo que anula el propósito. Entonces, lo que haremos es definirlos (usando un valor ficticio) y luego eliminarlos de la clase después.

 class Base(object): __metaclass__ = RequiredAttributes("a", "b" ,"c") a = b = c = 0 del Base.a, Base.b, Base.c 

Ahora, si intenta definir una subclase, pero no define los atributos:

 class Child(Base): pass 

Usted obtiene:

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

NB No tengo experiencia con Google App Engine, por lo que es posible que ya use una metaclase. En este caso, desea que su RequiredAttributesMeta derive de esa metaclase, en lugar de type .

Antes de describir mi solución, permítame presentarle cómo se crean las instancias de la clase Python:

Creación de instancias en Python

Figura 1: Creación de la instancia de Python [1]

Dada la descripción anterior, puede ver que en la clase Python las instancias son creadas en realidad por una metaclase. Como podemos ver, cuando la persona que llama está creando una instancia de nuestra clase, primero se llama al método mágico __call__ que a su vez está llamando a los __new__ y __init__ de la clase y luego __cal__ está devolviendo la instancia del objeto al llamante.

Dicho todo esto, simplemente podemos intentar verificar si la instancia creada por __init__ define realmente esos atributos “requeridos”.

Metaclase

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

Como puede ver en __call__ lo que hacemos es crear el objeto de clase y luego llamar a su método check_required_attributes() que verificará si se han definido los atributos necesarios. Allí, si los atributos requeridos no están definidos, simplemente deberíamos lanzar un error.

Superclase

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.') 

Aquí definimos la superclase real. Tres cosas:

  • Debe hacer uso de nuestra metaclase.
  • Debería definir los atributos requeridos como None ver starting_day_of_week = None
  • Debería implementar el método check_required_attributes que comprueba si los atributos requeridos son None y si deben lanzar un NotImplementedError con un mensaje de error razonable para el usuario.

Ejemplo de una subclase de trabajo y no de trabajo

 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 

Salida

 Traceback (most recent call last): File "test.py", line 50, in  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. 

Como puede ver, la primera instancia creada correctamente desde que se definió el atributo requerido, donde la segunda generó un NotImplementedError inmediatamente.

En términos generales, en Python se acepta ampliamente que la mejor manera de lidiar con este tipo de escenario, como usted sugirió correctamente, es envolver cualquier operación que necesite esta variable de clase con un bloque try-except.

Esto funciona. Evitará que incluso se definan las subclases, y mucho menos las instancias.

 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'