2017-05-04 1 views
2

J'ai passé beaucoup de temps à faire des recherches sur ce sujet, mais aucune des réponses ne semble fonctionner comme je le souhaiterais.Comment créer un attribut de classe abstraite (potentiellement en lecture seule)

J'ai une classe abstraite avec un attribut de classe que je veux chaque sous-classe d'être forcé à mettre en œuvre

class AbstractFoo(): 
    forceThis = 0 

Alors que quand je fais

class RealFoo(AbstractFoo): 
    pass 

il renvoie une erreur me disant qu'il peut Ne créez pas la classe avant d'implémenter forceThis.

Comment puis-je faire cela?

(je ne veux pas l'attribut à lecture seule, mais si c'est la seule solution, je vais l'accepter.)

Pour une méthode de classe, je l'ai découvert que je peux faire

from abc import ABCMeta, abstractmethod 

class AbstractFoo(metaclass=ABCMeta): 
    @classmethod 
    @abstractmethod 
    def forceThis(): 
     """This must be implemented""" 

de sorte que

class RealFoo(AbstractFoo): 
    pass 

au moins jette l'erreur TypeError: Can't instantiate abstract class EZ with abstract methods forceThis

(Bien que cela ne force pas forceThis à être une méthode de classe.)

Comment puis-je obtenir une erreur similaire à apparaître pour l'attribut de classe?

+2

Vous pouvez utiliser 'abstractproperty', également à partir du module abc. Notez que cela nécessite que l'attribut soit une propriété, pour des raisons discutées un peu [ici] (http://stackoverflow.com/questions/23831510/python-abstract-attribute-not-property). – BrenBarn

+0

[this] (http://stackoverflow.com/a/32536493/5049813) que j'ai trouvé dans votre lien @BrenBarn est très proche de ce que je veux, mais l'erreur ne se présente que lorsque vous essayez d'accéder à la propriété, pas quand la classe est définie. –

+1

Il apparaîtra si vous essayez d'instancier la classe. Avez-vous vraiment besoin que cela se produise lorsque vous définissez la classe? Si oui, vous pourriez faire quelque chose dans le sens suggéré [ici] (http://stackoverflow.com/questions/30876344/can-i-prevent-class-definition-unless-a-method-is-implemented). – BrenBarn

Répondre

0

J'ai trouvé une solution basée sur celles publiées plus tôt. (Merci @Daniel Roseman et @martineau)

J'ai créé une métaclasse appelée ABCAMeta (le dernier 'A' signifie 'Attributs').

La classe a deux façons de travailler.

  1. Une classe qui utilise juste ABCAMeta comme métaclasse doit avoir une propriété appelée requiredAttributes qui devrait contenir une liste des noms de tous les attributs que vous voulez exiger sur les sous-classes futures de cette catégorie

  2. A La classe dont la métaclasse parent est ABCAMeta doit avoir tous les attributs requis spécifiés par ses classes parentes.

Par exemple:

class AbstractFoo(metaclass=ABCAMeta): 
    requiredAttributes = ['forceThis'] 

class RealFoo(AbstractFoo): 
    pass 

lancera une erreur:

NameError: Class RealFoo has not implemented the following attributes: 'forceThis'

Exactement ce que je voulais.

class NoRequirements(RuntimeError): 
     def __init__(self, message): 
      RuntimeError.__init__(self, message) 

class ABCAMeta(ABCMeta): 
    def __init__(mcls, name, bases, namespace): 
     ABCMeta.__init__(mcls, name, bases, namespace) 

    def __new__(mcls, name, bases, namespace): 
     def getRequirements(c): 
      """c is a class that should have a 'requiredAttributes' attribute 
      this function will get that list of required attributes or 
      raise a NoRequirements error if it doesn't find one. 
      """ 

      if hasattr(c, 'requiredAttributes'): 
       return c.requiredAttributes 
      else: 
       raise NoRequirements("Class {} has no requiredAttributes property".format(c.__name__)) 

     cls = super().__new__(mcls, name, bases, namespace) 
     #true if no parents of the class being created have ABCAMeta as their metaclass 
     basicMetaclass = True 
     #list of attributes the class being created must implement 
     #should stay empty if basicMetaclass stays True 
     reqs = [] 
     for parent in bases: 
      parentMeta = type(parent) 
      if parentMeta==ABCAMeta: 
       #the class being created has a parent whose metaclass is ABCAMeta 
       #the class being created must contain the requirements of the parent class 
       basicMetaclass=False 
       try: 
        reqs.extend(getRequirements(parent)) 
       except NoRequirements: 
        raise 
     #will force subclasses of the created class to define 
     #the atrributes listed in the requiredAttributes attribute of the created class 
     if basicMetaclass: 
      getRequirements(cls) #just want it to raise an error if it doesn't have the attributes 
     else: 
      missingreqs = [] 
      for req in reqs: 
       if not hasattr(cls, req): 
        missingreqs.append(req) 
      if len(missingreqs)!=0: 
       raise NameError("Class {} has not implemented the following attributes: {}".format(cls.__name__, str(missingreqs)[1:-1])) 
     return cls 

Toutes les suggestions d'amélioration sont les bienvenues dans les commentaires.

3

Vous pouvez le faire en définissant votre propre métaclasse. Quelque chose comme:

class ForceMeta(type): 
    required = ['foo', 'bar'] 

    def __new__(mcls, name, bases, namespace): 
     cls = super().__new__(mcls, name, bases, namespace) 
     for prop in mcls.required: 
      if not hasattr(cls, prop): 
       raise NotImplementedError('must define {}'.format(prop)) 
     return cls 

Maintenant, vous pouvez l'utiliser comme métaclasse de vos propres classes:

class RequiredClass(metaclass=ForceMeta): 
    foo = 1 

qui soulèvera l'erreur "doit définir la barre.

+0

La question était spécifiquement de savoir comment créer une ** propriété ** abstraite, alors que cela semble juste vérifier l'existence de toute sorte d'attribut de classe. – martineau

+0

@martineau Quelle est la différence entre une propriété de classe et un attribut de classe? (Je ne savais pas qu'il y avait une différence - je devrais reformuler ma question pour aider à clarifier.) –

+2

@martineau Après avoir lu un peu plus, je pense que je parle d'attributs. J'ai édité la question pour refléter cela. Merci d'avoir porté cette question à mon attention! –

0

Bien que vous puissiez faire quelque chose de très similaire avec une métaclasse, comme illustré dans le answer de Daniel Roseman, cela peut également être fait avec un décorateur de classe. Un certain nombre d'avantages qu'ils ont sont que les erreurs se produiront lorsque la classe est définie, au lieu de lorsqu'une instance d'un est créée, et la syntaxe pour les spécifier est la même dans Python 2 et 3. Certaines personnes les trouvent également plus simples et plus facile à comprendre.

def check_reqs(cls): 
    requirements = 'must_have', 

    missing = [req for req in requirements if not hasattr(cls, req)] 
    if missing: 
     raise NotImplementedError(
      'class {} did not define required attribute{} named {}'.format(
       cls.__name__, 's' if len(missing) > 1 else '', 
       ', '.join('"{}"'.format(name) for name in missing))) 
    return cls 

@check_reqs 
class Foo(object): # OK 
    must_have = 42 

@check_reqs 
class Bar(object): # raises a NotImplementedError 
    pass 
+0

Intéressant! J'aime cette approche. Je pense que je serais peut-être capable de trouver celui qui me conviendrait le mieux. Se souvenir de taper que "@check_reqs" va être mon problème principal. –

+0

@ProQ: Il n'est pas plus difficile de se souvenir de '@ check_reqs' que de spécifier une' metaclass = Something' lors de la définition d'une 'classe'. – martineau

+0

mais remarquez que lors de la création de RealFoo dans mon exemple dans la question, je n'ai pas besoin de définir la métaclasse. C'est pour des choses comme RealFoo où je ne veux pas me souvenir d'avoir à faire des choses. Je veux juste sous-classer quelque chose et puis python me rappelle que j'ai oublié de surcharger/mettre dans ces attributs, tout comme il le fait pour les méthodes. –