Skip to content

13. As classes genéricas [BaseEntity] e [MyException]

Vamos agora definir duas classes que iremos utilizar regularmente daqui em diante.

Image

13.1. A classe MyException

A classe [MyException] (MyException.py) fornece uma classe de exceção personalizada:


# une classe d'exception propriétaire dérivant de [BaseException]
class MyException(BaseException):
    # constructeur
    def __init__(self: object, code: int, message: str):
        # parent
        BaseException.__init__(self, message)
        # code erreur
        self.code = code
 
    # toString
    def __str__(self):
        return f"MyException[{self.code}, {super().__str__()}]"
 
    # getter
    @property
    def code(self) -> int:
        return self.__code
 
    # setter
    @code.setter
    def code(self, code: int):
        # le code d'erreur doit être un entier positif
        if isinstance(code, int) and code > 0:
            self.__code = code
        else:
            # exception
            raise BaseException(f"code erreur {code} incorrect")

Notas

  • linha 2: a classe [MyException] deriva da classe predefinida [BaseException];
  • linha 4: o construtor aceita dois parâmetros:
    • [code]: um código de erro inteiro;
    • [message]: uma mensagem de erro;
  • linha 6: a mensagem de erro é passada para a classe pai;
  • linhas 14–27: o atributo [code] é acedido através de um getter/setter;
  • linhas 23–24: a validade do atributo [code] é verificada: deve ser um inteiro maior que 0;

13.2. A classe [BaseEntity]

A classe [BaseEntity] será a classe pai da maioria das classes que criamos para encapsular informações sobre um objeto. Daqui em diante, utilizaremos principalmente dois tipos de classes:

  • classes cujo único objetivo é encapsular informações sobre um único objeto num único local. Estas não terão comportamentos (métodos) além de getters/setters e uma função de exibição (__str__). Se houver N objetos para gerir, estas classes serão instanciadas N vezes. [BaseEntity] será a classe pai deste tipo de classe;
  • classes cuja função principal é encapsular métodos e muito pouca informação. Estas classes serão instanciadas apenas uma vez (singleton). A sua função é implementar os algoritmos de uma aplicação;

A classe [BaseEntity] é a seguinte:

#  imports
import json
import re

from MyException import MyException


class BaseEntity(object):
    #  properties excluded from class state
    excluded_keys = []

    #  class properties
    @staticmethod
    def get_allowed_keys() -> list:
        #  id: object identifier
        return ["id"]

    #  toString
    def __str__(self) -> str:
        return self.asjson()

    #  getter
    @property
    def id(self) -> int:
        return self.__id

    #   setter
    @id.setter
    def id(self, id):
        #  id must be an integer >=0
        try:
            id = int(id)
            erreur = id < 0
        except:
            erreur = True
        #  mistake?
        if erreur:
            raise MyException(1, f"L'identifiant d'une entité {self.__class__} doit être un entier >=0")
        else:
            self.__id = id

    def fromdict(self, state: dict, silent=False):
        

    def set_value(self, key: str, value, new_attributes) -> dict:
        

    def asdict(self, included_keys: list = None, excluded_keys: list = []) -> dict:
        

    def asjson(self, excluded_keys: list = []) -> str:
        

    def fromjson(self, json_state: str):
        

Comentários

  • O objetivo da classe [BaseEntity] é facilitar as conversões entre Objeto/Dicionário e Objeto/JSON. Ela disponibiliza os seguintes métodos:
    • [asdict]: devolve um dicionário com as propriedades do objeto;
    • [fromdict]: cria um objeto a partir de um dicionário;
    • [asjson]: devolve a cadeia JSON do objeto, semelhante à função [__str__];
    • [fromjson]: constrói um objeto a partir da sua string JSON;
  • A classe [BaseEntity] destina-se a ser derivada e não a ser utilizada tal como está;
  • linhas 22–25: a classe [BaseEntity] tem apenas uma propriedade, o inteiro [id]. Esta propriedade é o identificador do objeto. Na prática, é frequentemente útil poder distinguir entre instâncias da mesma classe. Faremos isso utilizando esta propriedade, que é única para cada instância. Além disso, os objetos provêm frequentemente de bases de dados onde são identificados por uma chave primária, tipicamente um inteiro. Nesses casos, [id] servirá como chave primária;
  • linhas 27–40: o setter para a propriedade [id]. Verificamos se é um inteiro >= 0. Se não for esse o caso, é lançada uma exceção do tipo [MyException] (linha 39);
  • linha 10: [excluded_keys] é um atributo de classe, não um atributo de instância. Por isso, escrevemos [BaseEntity.excluded_keys]. Este atributo de classe é uma lista que contém as propriedades da classe que não participam nas conversões Object/Dictionary e Object/JSON;
  • linhas 12–16: [get_allowed_keys] devolve a lista das propriedades da classe. Numa conversão de Dicionário → Objeto ou JSON → Objeto, apenas as chaves presentes nesta lista serão aceites. Cada classe derivada da classe [BaseEntity] terá de redefinir esta lista;

É importante compreender aqui que as propriedades e funções da classe [BaseEntity] são acessíveis às classes derivadas de [BaseEntity]. Este é o ponto-chave a compreender.

Vamos agora examinar o código da classe [BaseEntity] em detalhe. É bastante avançado. Os leitores principiantes podem simplesmente ler a descrição da função de cada função sem se aprofundarem no código em si.

13.2.1. O método [BaseEntity.fromdict]

13.2.1.1. Definição

O método [fromdict] permite-lhe inicializar um objeto [BaseEntity] ou um objeto derivado a partir de um dicionário:

def fromdict(self, state: dict, silent=False):
        #  object is updated
        #  authorized keys
        allowed_keys = self.__class__.get_allowed_keys()
        #  state key traversal
        for key, value in state.items():
            #  is the key authorized?
            if key not in allowed_keys:
                if not silent:
                    raise MyException(2, f"la clé {key} n'est pas autorisée")
            else:
                #  we try to assign the value to the key
                #  we let any exception go up
                setattr(self, key, value)
        #  we return the object
        return self

Comentários

  • linha 1: a função recebe o dicionário [state] como parâmetro, a partir do qual o objeto atual será inicializado;
  • linha 4: chamamos a função estática [get_allowed_keys] da classe que chamou a função [fromdict]. Se estivermos a lidar com uma classe derivada de [BaseEntity] e essa classe derivada tiver redefinido a função estática [get_allowed_keys], então é a função [get_allowed_keys] que é chamada. Cada classe derivada redefine esta função estática para declarar as suas propriedades;
  • linha 6: percorremos as chaves e os valores do dicionário [state];
  • linha 8: se a chave [key] não for uma das propriedades da classe, então:
    • ela é ignorada;
    • é lançada uma exceção (linha 10). O programador especifica a sua preferência passando o parâmetro [silent] apropriado (linha 1). O valor padrão de [silent] faz com que seja lançada uma exceção se for feita uma tentativa de inicializar o objeto com uma propriedade que este não possui;
  • linha 14: se a chave estiver entre as propriedades do objeto, então é atribuída ao objeto [self] utilizando a função predefinida [setattr];
  • linha 16: a função devolve o objeto inicializado;

13.2.1.2. Exemplos

Image

13.2.1.2.1. A classe [Utils]

A classe [Utils] (Utils.py) é a seguinte:

class Utils:
    #  static method
    @staticmethod
    def is_string_ok(string: str) -> bool:
        #  string is a string
        erreur = not isinstance(string, str)
        if not erreur:
            #  is the chain empty?
            erreur = string.strip() == ''
        #  result
        return not erreur

Define, nas linhas 3–11, um método estático que retorna um valor booleano verdadeiro se o seu parâmetro [str] for uma string não vazia;

13.2.1.2.2. A classe [Person]

A classe [Person] (Person.py) deriva da classe [BaseEntity]:

#  imports
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


#  person class
class Personne(BaseEntity):
    #  properties excluded from class state
    excluded_keys = []

    #  class properties
    #  id: person's identifier
    #  first name: person's first name
    #  name: person's name
    #  age: age of the person
    @staticmethod
    def get_allowed_keys() -> list:
        #  id: object identifier
        return BaseEntity.get_allowed_keys() + ["nom", "prénom", "âge"]

    #  getters
    @property
    def prénom(self) -> str:
        return self.__prénom

    @property
    def nom(self) -> str:
        return self.__nom

    @property
    def âge(self) -> int:
        return self.__âge

    #  setters
    @prénom.setter
    def prénom(self, prénom: str):
        #  first name must be non-empty
        if Utils.is_string_ok(prénom):
            self.__prénom = prénom.strip()
        else:
            raise MyException(11, "Le prénom doit être une chaîne de caractères non vide")

    @nom.setter
    def nom(self, nom: str):
        #  first name must be non-empty
        if Utils.is_string_ok(nom):
            self.__nom = nom.strip()
        else:
            raise MyException(12, "Le nom doit être une chaîne de caractères non vide")

    @âge.setter
    def âge(self, âge: int):
        #  age must be an integer >=0
        erreur = False
        if isinstance(âge, int):
            if âge >= 0:
                self.__âge = âge
            else:
                erreur = True
        else:
            erreur = True
        #  mistake?
        if erreur:
            raise MyException(13, "L'âge doit être un entier >=0")
  • linha 8: a classe [Person] deriva da classe [BaseEntity];
  • linhas 8–65: mantivemos a maior parte da classe [Person] encontrada anteriormente. As diferenças são as seguintes:
    • a classe já não tem um construtor;
    • a classe utiliza a exceção [MyException], por exemplo, linha 65;
    • possui um método estático, [get_allowed_keys], linhas 17–20, que define a lista das suas propriedades. As propriedades específicas da classe [Person] são adicionadas às da classe pai [BaseEntity];
    • possui uma lista estática [excluded_keys], à qual voltaremos mais tarde;
13.2.1.2.3. A classe [Teacher]

A classe [Teacher] (Teacher.py) deriva da classe [Person]:

#  imports
from MyException import MyException
from Personne import Personne
from Utils import Utils


#  class Teacher
class Enseignant(Personne):
    #  properties excluded from class state
    excluded_keys = []

    #  class properties
    #  id: person's identifier
    #  first name: person's first name
    #  name: person's name
    #  age: age of the person
    #  discipline: discipline taught
    @staticmethod
    def get_allowed_keys() -> list:
        #  id: object identifier
        return Personne.get_allowed_keys() + ["discipline"]

    #  properties
    @property
    def discipline(self) -> str:
        return self.__discipline

    @discipline.setter
    def discipline(self, discipline: str):
        #  the discipline must be a non-empty string
        if Utils.is_string_ok(discipline):
            self.__discipline = discipline
        else:
            raise MyException(21, "La discipline doit être une chaîne de caractères non vide")

    #  show method
    def show(self):
        print(f"Enseignant[{self.id}, {self.prénom}, {self.nom}, {self.âge}]")
  • linha 8: a classe [Professor] estende (ou deriva de) a classe [Pessoa];
  • linhas 18–21: definem a lista de propriedades da classe;
  • linhas 37–38: o método [show] exibe a identidade do professor;
13.2.1.2.4. A configuração [config]

Os scripts de exemplo utilizam a seguinte configuração [config]:

def configure():
    import os

    #  configuration file folder
    script_dir = os.path.dirname(os.path.abspath(__file__))

    #  absolute paths of folders to put in the syspath
    absolute_dependencies = [
        #  the BaseEntity class
        f"{script_dir}/entities",
    ]

    #  update syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  we return the configuration
    return {}
  • linhas 8–10: os diretórios que contêm as dependências do projeto;
  • linhas 14–15: o Python Path é construído;
  • linha 18: retorna um dicionário vazio (não há outras configurações além do syspath);
13.2.1.2.5. O script [fromdict_01]

O script [fromdict_01] é o seguinte:

#  configure the application
import config

config = config.configure()

#  syspath is configured - imports can be made
from Enseignant import Enseignant

#  a teacher
enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "paul", "âge": 56})
enseignant1.show()
  • Linha 10: Criamos um objeto [Teacher] a partir de um dicionário. Para isso, usamos o construtor padrão da classe para criar um objeto [Teacher] ao qual aplicamos o método [fromdict]. É importante compreender que, neste caso, o método [fromdict] que está a ser executado é o da classe pai [BaseEntity]. Na verdade:
    • o método [fromdict] é primeiro procurado na classe [Teacher]. Como não existe;
    • é então procurado na classe pai [Person]. Não existe;
    • é então procurado na classe pai [BaseEntity]. Existe;
  • linha 11: o objeto [Teacher] é exibido;

Os resultados são os seguintes:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/fromdict_01.py
Enseignant[1, paul, lourou, 56]
 
Process finished with exit code 0
13.2.1.2.6. O script [fromdict_02]

O script [fromdict_02] é o seguinte:

#  configure the application
import config

config = config.configure()

#  syspath is configured - imports can be made
from Enseignant import Enseignant

#  a teacher
enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "", "âge": 56})
enseignant1.show()
  • Linha 10: Criamos um professor com um nome próprio vazio. Isto deve gerar uma exceção, porque a classe [Person] não aceita nomes próprios vazios. Este exemplo ilustra a diferença entre um dicionário e um objeto. Este último pode validar as suas propriedades, enquanto o dicionário não pode;

Os resultados são os seguintes:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/fromdict_02.py
Traceback (most recent call last):
  File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/fromdict_02.py", line 10, in <module>
    enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "", "âge": 56})
  File "C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\classes\02/entities\BaseEntity.py", line 55, in fromdict
    setattr(self, key, value)
  File "C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\classes\02/entities\Personne.py", line 42, in prénom
    raise MyException(11, "Le prénom doit être une chaîne de caractères non vide")
MyException.MyException: MyException[11, Le prénom doit être une chaîne de caractères non vide]
 
Process finished with exit code 1
13.2.1.2.7. O script [fromdict_03]

O script [fromdict_03] é o seguinte:

#  configure the application
import config

config = config.configure()

#  syspath is configured - imports can be made
from Enseignant import Enseignant

#  a teacher
enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "albert", "âge": 56, "sexe": "M"})
enseignant1.show()
  • Linha 10: Criamos um Professor a partir de um dicionário que contém uma chave (sexo) que não pertence à classe [Professor]. Deveria, então, ser levantada uma exceção;

Os resultados são os seguintes:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/fromdict_03.py
Traceback (most recent call last):
  File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/fromdict_03.py", line 10, in <module>
    enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "albert", "âge": 56, "sexe": "M"})
  File "C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\classes\02/entities\BaseEntity.py", line 51, in fromdict
    raise MyException(2, f"la clé [{key}] n'est pas autorisée")
MyException.MyException: MyException[2, la clé [sexe] n'est pas autorisée]
 
Process finished with exit code 1
13.2.1.2.8. O script [fromdict_04]

O script [fromdict_04] é uma cópia do [fromdict_03] com uma pequena diferença:

#  configure the application
import config

config = config.configure()

#  syspath is configured - imports can be made
from Enseignant import Enseignant

#  a teacher
enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "albert", "âge": 56, "sexe": "M"}, silent=True)
enseignant1.show()
  • linha 10: utilizámos o parâmetro [silent=True] para indicar que, se uma chave do dicionário não for uma propriedade da classe [Teacher], deve simplesmente ser ignorada. Neste caso, não será levantada nenhuma exceção;

Os resultados são os seguintes:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/fromdict_04.py
Enseignant[1, albert, lourou, 56]
 
Process finished with exit code 0

13.2.2. O método [BaseEntity.asdict]

13.2.2.1. Definição

O método [BaseEntity.asdict] devolve um dicionário cujas chaves são as propriedades do objeto:


    def asdict(self, included_keys: list = None, excluded_keys: list =[]) -> dict:
        # attributs de l'objet
        attributes = self.__dict__
        # les nouveaux attributs
        new_attributes = {}
        # on parcourt les attributs
        for key, value in attributes.items():
            # si la clé est explicitement demandée
            if included_keys and key in included_keys:
                self.set_value(key, value, new_attributes)
            # sinon, si la clé n'est pas exclue
            elif not included_keys and key not in self.__class__.excluded_keys and key not in excluded_keys:
                self.set_value(key, value, new_attributes)
        # on rend le dictionnaire des attributs
        return new_attributes

Comentários

  • linha 1: a função [asdict] devolve o dicionário das propriedades do objeto;
  • linha 1: [included_keys]: a lista de chaves a incluir no dicionário;
  • linha 1: [excluded_keys]: a lista de chaves a excluir do dicionário;
  • linha 3: a propriedade [self.__dict__] devolve o dicionário de propriedades do objeto. Os nomes das propriedades são as chaves e os seus valores são os valores do dicionário. Um objeto pode conter referências a outros objetos. Nesse caso, os nomes das propriedades são precedidos pelo nome da classe a que pertencem. Isto é algo que não queremos. Queremos as propriedades sem o seu prefixo;
  • linha 3: é importante compreender aqui que, se a função [asdict] for executada dentro de uma classe derivada de [BaseEntity], a propriedade [self.__dict__] devolve o dicionário de propriedades do objeto derivado;
  • linha 5: o dicionário que vamos construir;
  • linha 7: iteramos sobre os valores de [self.__dict__] na forma (chave, valor);
  • linha 9: se a chave atual pertencer à lista de chaves a incluir, então é adicionada ao dicionário [new_attributes] utilizando a função [set_value], que descreveremos em breve;
  • linha 12: se o parâmetro [included_keys] não estiver presente, então é utilizado o parâmetro [excluded_keys]. Se a propriedade não estiver entre as propriedades a excluir, então é adicionada ao dicionário [new_attributes];
  • linha 12: existem várias formas de excluir uma propriedade do dicionário:
    • foi definida ao nível do atributo de classe [excluded_keys];
    • foi definida na lista [excluded_keys] passada à função [asdict];
    • o parâmetro [included_keys] está presente e não inclui a propriedade;
  • linha 15: devolvemos o dicionário [new_attributes]

A função [set_value] nas linhas 10 e 13 é a seguinte:

    @staticmethod
    def set_value(key: str, value, new_attributes: dict):
        #  keys can be of the form __Class__key
        match = re.match("^.*?__(.*?)$", key)
        if match:
            #  note the new key
            newkey = match.groups()[0]
        else:
            #  the key remains unchanged
            newkey = key
        #  insert the new key into the [new_attributes] dictionary
        #  type, transforming if necessary the associated value into one of the
        #  dict, list, simple type
        new_attributes[newkey] = BaseEntity.check_value(value)

Comentários

  • linha 4: verifique se a chave está no formato __Class_key. Este é o formato que assume se pertencer a um objeto incluído no objeto principal. Neste caso, queremos manter apenas a string [key];
  • linha 7: mantemos apenas a cadeia de caracteres que se segue aos dois últimos caracteres sublinhados da cadeia;
  • linhas 8–10: se a chave não estiver no formato __Class_key, mantemo-la tal como está;
  • linhas 11–14: o valor associado à chave [newkey] é calculado pelo método estático [BaseEntity.check_value];

O método estático [BaseEntity.check_value] é o seguinte:

    @staticmethod
    def check_value(value):
        #  the value can be of type BaseEntity, list, dict or a simple type
        #  is value an instance of BaseEntity?
        if isinstance(value, BaseEntity):
            value2 = value.asdict()
        #  is value of type list
        elif isinstance(value, list):
            value2 = BaseEntity.list2list(value)
        #  is value a dict type?
        elif isinstance(value, dict):
            value2 = BaseEntity.dict2dict(value)
        #  value is a simple type
        else:
            value2 = value
        #  we return the result
        return value2
  • linha 1: o método [check_value] é estático (um método de classe, não um método de instância). Recebe como parâmetro o valor a ser associado a uma chave do dicionário:
    • linha 17: se este valor for de um tipo simples, permanece inalterado;
    • linhas 5-6: se este valor for do tipo BaseEntity, o valor é substituído pelo seu dicionário. Isto resulta numa chamada recursiva;
    • linhas 8–9: se este valor for uma lista, é substituído pelo valor [BaseEntity.list2list];
    • linhas 11–12: se este valor for um dicionário, é substituído pelo valor [BaseEntity.dict2dict];

O método estático [BaseEntity.list2list] é o seguinte:

1
2
3
4
5
6
7
8
    @staticmethod
    def list2list(liste: list) -> list:
        #  inspect list items
        newlist = []
        for value in liste:
            newlist.append(BaseEntity.check_value(value))
        #  return the new list
        return newlist
  • linha 2: o método recebe uma lista e devolve uma lista;
  • linhas 5-6: cada valor na lista passada como parâmetro é substituído pelo valor devolvido pelo método estático [BaseEntity.check_value]. Trata-se, portanto, de uma chamada recursiva. O método estático [BaseEntity.check_value] é chamado até que o seu parâmetro [value] seja um tipo simples (não um tipo BaseEntity, lista ou dicionário);

O método estático [BaseEntity.dict2dict] é o seguinte:

1
2
3
4
5
6
7
8
    @staticmethod
    def dict2dict(dictionary: dict) -> dict:
        #  inspect dictionary items
        newdict = {}
        for key, value in dictionary.items():
            newdict[key] = BaseEntity.check_value(value)
        #  the new dictionary is returned
        return newdict
  • linha 2: o método recebe um dicionário e devolve um dicionário;
  • linhas 5-6: cada valor no dicionário passado como parâmetro é substituído pelo valor devolvido pelo método estático [BaseEntity.check_value]. Trata-se, portanto, de uma chamada recursiva. O método estático [BaseEntity.check_value] é chamado até que o seu parâmetro [value] seja um tipo simples (não uma BaseEntity, lista ou dicionário);

13.2.2.2. Exemplos

O script [asdict_01] demonstra várias utilizações do método [asdict]:

#  configure the application
import config
config = config.configure()

#  syspath is configured - imports can be made
from Enseignant import Enseignant
from BaseEntity import BaseEntity

#  a teacher
enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "paul", "âge": 56})
dict1 = enseignant1.asdict()
print(type(dict1))
print(enseignant1.__dict__)
print(dict1)
print(enseignant1.asdict(excluded_keys=["_Personne__âge"]))
Enseignant.excluded_keys = ["_Personne__prénom"]
print(enseignant1)
#  another teacher
enseignant2 = Enseignant().fromdict({"id": 2, "nom": "abélard", "prénom": "béatrice", "âge": 57})
print(enseignant2.asdict())
print(enseignant2.asdict(included_keys=["_Personne__nom"]))
#  a list of entities within an entity
Enseignant.excluded_keys = []
entity1 = BaseEntity()
enseignants = [enseignant1, enseignant2]
setattr(entity1, "enseignants", enseignants)
print(entity1.asdict())
#  a dictionary of entities within an entity
matières = {"maths": enseignant1, "français": enseignant2}
setattr(entity1, "matières", matières)
print(entity1.asdict())

Os resultados da execução são os seguintes:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/asdict_01.py
<class 'dict'>
{'_BaseEntity__id': 1, '_Personne__nom': 'lourou', '_Personne__prénom': 'paul', '_Personne__âge': 56}
{'id': 1, 'nom': 'lourou', 'prénom': 'paul', 'âge': 56}
{'id': 1, 'nom': 'lourou', 'prénom': 'paul'}
{"id": 1, "nom": "lourou", "âge": 56}
{'id': 2, 'nom': 'abélard', 'âge': 57}
{'nom': 'abélard'}
{'enseignants': [{'id': 1, 'nom': 'lourou', 'prénom': 'paul', 'âge': 56}, {'id': 2, 'nom': 'abélard', 'prénom': 'béatrice', 'âge': 57}]}
{'enseignants': [{'id': 1, 'nom': 'lourou', 'prénom': 'paul', 'âge': 56}, {'id': 2, 'nom': 'abélard', 'prénom': 'béatrice', 'âge': 57}], 'matières': {'maths': {'id': 1, 'nom': 'lourou', 'prénom': 'paul', 'âge': 56}, 'français': {'id': 2, 'nom': 'abélard', 'prénom': 'béatrice', 'âge': 57}}}
 
Process finished with exit code 0
  • A linha 4 demonstra a vantagem do método [asdict] em relação ao uso da propriedade [__dict__]. As propriedades são despojadas do seu prefixo de classe. Isto torna-as mais fáceis de visualizar;
  • Existem várias formas de utilizar o método [asdict]:
    • se quiser todas as propriedades: utilize o método [asdict] sem parâmetros;
    • se quiser apenas determinadas propriedades:
      • se houver mais propriedades para incluir do que para excluir: utilize o único parâmetro [excluded_keys];
      • Se houver menos propriedades para incluir do que para excluir: usaremos apenas o parâmetro [included_keys];

13.2.3. O método [BaseEntity.asjson]

Este método devolve a cadeia JSON de um objeto [BaseEntity] ou das suas classes derivadas. Apresenta a cadeia JSON do dicionário devolvido pelo método [asdict]. O seu código é o seguinte:

1
2
3
def asjson(self, included_keys: list = None, excluded_keys: list = []) -> str:
        #  the json string
        return json.dumps(self.asdict(included_keys=included_keys, excluded_keys=excluded_keys), ensure_ascii=False)
  • linha 1: os parâmetros do método [asjson] são os do método [asdict];

Aqui está um exemplo (asjson_01) que utiliza este método:

#  configure the application
import config
config = config.configure()

#  syspath is configured - imports can be made
from Enseignant import Enseignant
from BaseEntity import BaseEntity

#  a teacher
enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "paul", "âge": 56})
print(type(enseignant1.asjson()))
print(enseignant1.asjson(excluded_keys=["_Personne__âge"]))
Enseignant.excluded_keys = ["_Personne__prénom"]
print(enseignant1.asjson())
#  another teacher
enseignant2 = Enseignant().fromdict({"id": 2, "nom": "abélard", "prénom": "béatrice", "âge": 57})
print(enseignant2.asjson())
print(enseignant2.asjson(included_keys=["_Personne__nom"]))
#  a list of entities within an entity
Enseignant.excluded_keys = []
entity1 = BaseEntity()
enseignants = [enseignant1, enseignant2]
setattr(entity1, "enseignants", enseignants)
print(entity1.asjson())
#  a dictionary of entities within an entity
matières = {"maths": enseignant1, "français": enseignant2}
setattr(entity1, "matières", matières)
print(entity1.asjson())

Os resultados são os seguintes:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/asjson_01.py
<class 'str'>
{"id": 1, "nom": "lourou", "prénom": "paul"}
{"id": 1, "nom": "lourou", "âge": 56}
{"id": 2, "nom": "abélard", "âge": 57}
{"nom": "abélard"}
{"enseignants": [{"id": 1, "nom": "lourou", "prénom": "paul", "âge": 56}, {"id": 2, "nom": "abélard", "prénom": "béatrice", "âge": 57}]}
{"enseignants": [{"id": 1, "nom": "lourou", "prénom": "paul", "âge": 56}, {"id": 2, "nom": "abélard", "prénom": "béatrice", "âge": 57}], "matières": {"maths": {"id": 1, "nom": "lourou", "prénom": "paul", "âge": 56}, "français": {"id": 2, "nom": "abélard", "prénom": "béatrice", "âge": 57}}}
 
Process finished with exit code 0

O método [BaseEntity.__str__] utiliza o método [asjson] para exibir a identidade do objeto [BaseEntity] ou dos seus objetos derivados:


# toString
    def __str__(self) -> str:
        return self.asjson()

13.2.4. O método [BaseEntity.fromjson]

O método [BaseEntity.fromjson] permite-lhe inicializar um objeto do tipo [BaseEntity] ou de um tipo derivado a partir de um dicionário JSON. O seu código é o seguinte:

1
2
3
def fromjson(self, json_state: str, silent: bool = False):
        #  update object status from jSON string
        return self.fromdict(json.loads(json_state), silent=silent)
  • Linha 1: O método aceita dois parâmetros:
    • [json_state]: o dicionário JSON utilizado para inicializar o objeto [BaseEntity];
    • [silent]: para indicar se a presença no dicionário JSON de uma chave que não pode ser aceite como propriedade do objeto [BaseEntity] desencadeia uma exceção (silent=False) ou é simplesmente ignorada (silent=True);
  • Linha 3: Começamos por construir o dicionário Python que representa o dicionário JSON e, em seguida, utilizamos o método [fromdict] para inicializar o objeto [BaseEntity] a partir deste dicionário Python;

Aqui está um exemplo (fromjson_01):

#  configure the application
import config

config = config.configure()

#  syspath is configured - imports can be made
from Enseignant import Enseignant
import json

#  a teacher
json1 = json.dumps({"id": 1, "nom": "lourou", "prénom": "paul", "âge": 56})
enseignant1 = Enseignant().fromjson(json1)
enseignant1.show()
  • linha 11: criamos a cadeia JSON de um dicionário;
  • linha 12: um objeto [Enseignant] é inicializado com esta string;
  • linha 13: o professor é apresentado;

Os resultados são os seguintes:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/fromjson_01.py
Enseignant[1, paul, lourou, 56]
 
Process finished with exit code 0

13.2.5. O script [main]

O script [main] resume os vários métodos encontrados:

#  configure the application
import config

config = config.configure()

#  syspath is configured - imports can be made
from BaseEntity import BaseEntity
from MyException import MyException


#  a class
class ChildEntity(BaseEntity):
    #  attributes excluded from class state
    excluded_keys = []

    @staticmethod
    def get_allowed_keys():
        return ["att1", "att2", "att3", "att4"]

    @property
    def att1(self) -> int:
        return self.__att1

    @att1.setter
    def att1(self, value: int):
        if 10 >= value >= 1:
            self.__att1 = value
        else:
            raise MyException(1, f"L'attribut [att1] attend une valeur dans l'intervalle [1,10] ({value})")


#  configuration ChildEntity
ChildEntity.excluded_keys = []
#  instance ChildEntity
child = ChildEntity().fromdict({"att1": 1, "att2": 2})
#  pay attention to property names
#  these are the names used in [excluded_keys] and [included_keys]
print(child.__dict__)
#  properties not prefixed by their class
print(child)

#  instance ChildEntity
try:
    child = ChildEntity().fromdict({"att1": 1, "att5": 5})
    print(child)
except MyException as erreur:
    print(erreur)

#  instance ChildEntity
child = ChildEntity().fromdict({"att1": 1, "att2": 2, "att3": 3, "att4": 4})
print(child)

#  exclusion of certain keys from instance status
ChildEntity.excluded_keys = ['att3']
print(child)

#  a key is explicitly excluded from the display
#  it is added to those excluded globally at class level
print(child.asdict(excluded_keys=["_ChildEntity__att1"]))
print(child.asjson(excluded_keys=["att2"]))

#  class interest in the dictionary
#  it can check the validity of its contents
try:
    child = ChildEntity().fromdict({"att1": 20})
except MyException as erreur:
    print(erreur)

#  instance ChildEntity
child1 = ChildEntity().fromdict({"att1": 1, "att2": 2, "att3": 3, "att4": 4})
#  instance ChildEntity containing another instance ChildEntity
child2 = ChildEntity().fromdict({"att1": 10, "att2": 20, "att3": 30, "att4": child1})
print(child2)

#  included_keys has priority over excluded_keys which are then ignored
ChildEntity.excluded_keys = ['_ChildEntity__att1', 'att2']
print(child.asdict(included_keys=["_ChildEntity__att1", "att3"], excluded_keys=["att3", "att4"]))

Os resultados da execução são os seguintes:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/main.py
{'_ChildEntity__att1': 1, 'att2': 2}
{"att1": 1, "att2": 2}
MyException[2, la clé [att5] n'est pas autorisée]
{"att1": 1, "att2": 2, "att3": 3, "att4": 4}
{"att1": 1, "att2": 2, "att4": 4}
{'att2': 2, 'att4': 4}
{"att1": 1, "att4": 4}
MyException[1, L'attribut [att1] attend une valeur dans l'intervalle [1,10] (20)]
{"att1": 10, "att2": 20, "att4": {"att1": 1, "att2": 2, "att4": 4}}
{'att1': 1, 'att3': 3}
 
Process finished with exit code 0

Observe a linha 2 dos resultados: é a propriedade [ChildEntity.__dict__] (linha 38 do código) que nos permite determinar os nomes das propriedades a incluir nas listas [included_keys] e [excluded_keys]. Observe, ainda na linha 2 dos resultados, que, dependendo de a propriedade ser definida dentro da classe através de um getter/setter ou criada como se criasse uma chave de dicionário, ela pode ou não ser prefixada com o nome da classe [ChildEntity].