{"id":158,"date":"2010-05-31T13:46:14","date_gmt":"2010-05-31T12:46:14","guid":{"rendered":"http:\/\/saladtomatonion.com\/blog\/?p=158"},"modified":"2011-11-30T13:52:20","modified_gmt":"2011-11-30T12:52:20","slug":"utilisation-de-__metaclass__-en-python-pour-definir-des-champs-dobjet-types","status":"publish","type":"post","link":"https:\/\/saladtomatonion.com\/blog\/2010\/05\/31\/utilisation-de-__metaclass__-en-python-pour-definir-des-champs-dobjet-types\/","title":{"rendered":"Utilisation de __metaclass__ en python pour d\u00e9finir des champs d&rsquo;objet typ\u00e9s"},"content":{"rendered":"<p><img decoding=\"async\" loading=\"lazy\" class=\"alignleft size-thumbnail wp-image-236\" title=\"python-logo-glassy\" src=\"http:\/\/saladtomatonion.com\/blog\/wp-content\/uploads\/2010\/05\/python-logo-glassy-150x150.png\" alt=\"\" width=\"150\" height=\"150\" \/> Si je veux assigner un un champ d&rsquo;un objet en <a href=\"http:\/\/python.org\/\" target=\"_blank\">python<\/a>, il me suffit de faire : <code>monObject.champ = valeur<\/code>. Python est un langage non statiquement typ\u00e9, et les objets sont des dictionnaires auxquels je peux rajouter des entr\u00e9es \u00e0 volont\u00e9!<br \/>\nCe n&rsquo;est pas dans l&rsquo;esprit de python de v\u00e9rifier les types des donn\u00e9es syst\u00e9matiquement, mais comment faire en cas de r\u00e9elle contrainte sur les dits types?<br \/>\n<!--more--><br \/>\n<br style=\"visibility: invisible; clear: both;\" \/><br \/>\nOn peut d\u00e9j\u00e0 utiliser des champs de type <em>property<\/em> qui permettent de d\u00e9finir des fonctions pour acc\u00e9der aux \u00e9l\u00e9ments de l&rsquo;objet.<\/p>\n<pre name=\"code\" class=\"py:nogutter:nocontrols\"># Une classe dont je souhaite que le champ\r\n# field soit obligatoirement un int\r\nclass MaClasse(object):\r\n    @property # le getter\r\n    def field(self):\r\n        return self._field\r\n\r\n    @field.setter\r\n    def field(self, value):\r\n        if not isinstance(value, int):\r\n            raise TypeError()\r\n        else:\r\n            self._field = value<\/pre>\n<p>\u00c7a marche, mais on se rend compte que je dois coder une fonction de getter et une fonction de setter pour chaque champ que je veux contr\u00f4ler&#8230; et \u00e7a peut rapidement \u00eatre tr\u00e8s contraignant.<\/p>\n<p>Pour faire plus simple&#8230; Je me rappelle que les classes python sont elles-m\u00eames des objets servant de prototypes aux instances. Ce qui veut dire d&rsquo;une part que je peux modifier une classe au runtime, et d&rsquo;autre part qu&rsquo;il existe un constructeur pour une classe. Pour ces deux op\u00e9rations, j&rsquo;utilise le champ __metaclass__ de ma classe qui va me permettre d&rsquo;acc\u00e9der \u00e0 la m\u00e9thode __new__, le constructeur de ma classe.<\/p>\n<p>Je veux utiliser une classe DataField qui me servira de descripteur de champ. Voici \u00e0 quoi je voudrais que ma classe finale ressemble.<\/p>\n<pre name=\"code\" class=\"py:nogutter:nocontrols\">class DataField(object):\r\n    def __init__(self, type):\r\n        self.type = type\r\n\r\nfrom datetime import datetime\r\nclass MaClasse(object):\r\n    id = DataField(int)\r\n    label = DataField(str)\r\n    date = DataField(datetime)<\/pre>\n<p>Il faudrait que chacun de ces DataFields devienne automatiquement une property qui ferait la v\u00e9rification de type dans le code du setter.<\/p>\n<p>C&rsquo;est l\u00e0 que __metaclass__ entre en jeu en permettant de surcharger le code __new__ de la classe.<\/p>\n<pre name=\"code\" class=\"py:nogutter:nocontrols\">class MonMeta(type):\r\n    def __new__(cls, name, parents_cls, attributes):\r\n        # Appel du constructeur parent\r\n        newcls = super(MonMeta, cls).__new__(cls,\r\n                                            name,\r\n                                            parents_cls,\r\n                                            attributes)\r\n        # On boucle sur les attributs\r\n        for attrname, attr in attributes.items():\r\n            # le cas qui nous int\u00e9resse:\r\n            if isinstance(attr, DataField):\r\n                # le nom de la variable qui stocke la valeur\r\n                attr.attrname = attrname\r\n                private_name = '_' + attrname\r\n\r\n                # le getter (voir ci-apr\u00e8s)\r\n                getter = attr.build_getter(private_name)\r\n\r\n                # le setter (voir ci-apr\u00e8s)\r\n                setter = attr.build_setter(private_name)\r\n\r\n                # On g\u00e9n\u00e8re la property\r\n                prop = property(getter, setter)\r\n                # On remplace le champ par la property\r\n                setattr(newcls, attrname, prop)\r\n        return newcls<\/pre>\n<p>Qu&rsquo;est ce donc que ces m\u00e9thodes build_getter et build_setter? Elles ne tombent pas du ciel et il faut les impl\u00e9menter. On les rajoute \u00e0 la classe DataField, de fa\u00e7on \u00e0 obtenir ceci :<\/p>\n<pre name=\"code\" class=\"py:nogutter:nocontrols\">class DataField(object):\r\n    def __init__(self, type):\r\n        self.type = type\r\n\r\n    # Construit et retourne la fonction qui servira de getter\r\n    def build_getter(self, private_name):\r\n        def getter(obj):\r\n            return getattr(obj, private_name, None)\r\n        return getter\r\n    \r\n    # Construit et retourne la fonction qui servira de setter\r\n    def build_setter(self, private_name):\r\n        def setter(obj, value):\r\n            if not isinstance(value, self.type):  \r\n                raise TypeError('%s.%s doit \u00eatre de type %s'  \r\n                            % (obj.__class__.__name__, self.attrname, self.type.__name__))  \r\n            else:  \r\n                setattr(obj, private_name, value)\r\n        return setter<\/pre>\n<p>On instancie une fonction, pour chaque champ, qui d\u00e9crit comment acc\u00e9der \u00e0 la v\u00e9ritable valeur en lecture et en \u00e9criture.<\/p>\n<p>Ensuite, il ne reste plus qu&rsquo;\u00e0 indiquer quel metaclass utiliser dans MaClasse.<\/p>\n<pre name=\"code\" class=\"py:nogutter:nocontrols\">class MaClasse(object):\r\n    __metaclass__ = MonMeta\r\n    .\r\n    .\r\n    .<\/pre>\n<p>Donc, au chargement de ma classe au runtime, la fonction __new__ de sa metaclass sera appel\u00e9e, et mes champs DataField seront remplac\u00e9s par des champs de type property qui feront les bonnes v\u00e9rifications. Je peux m\u00eame avoir des classes h\u00e9riti\u00e8res de MaClasse, la metaclass sera propag\u00e9e.<\/p>\n<pre name=\"code\" class=\"py:nogutter:nocontrols\">&gt;&gt;&gt; o = MaClasse()\r\n&gt;&gt;&gt; o.id = 5\r\n&gt;&gt;&gt; print o.id\r\n5\r\n&gt;&gt;&gt; o.date = \"une chaine\"\r\nTraceback (most recent call last):\r\n  File \"&lt;stdin&gt;\", line 1, in &lt;module&gt;\r\nTypeError: MaClasse.date doit \u00eatre de type datetime<\/pre>\n<p>Pour voir plus loin, on peut penser \u00e0 donner des valeurs par d\u00e9faut, ou bien encore affiner la possibilit\u00e9 de modifier\/effacer des champs&#8230; Adaptez le \u00e0 vos besoins! Pour le d\u00e9tail, je vous laisse voir <a href=\"http:\/\/docs.python.org\/reference\/datamodel.html#customizing-class-creation\" target=\"_blank\">la documentation de __metaclass__<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Si je veux assigner un un champ d&rsquo;un objet en python, il me suffit de faire : monObject.champ = valeur. Python est un langage non statiquement typ\u00e9, et les objets sont des dictionnaires auxquels&#46;&#46;&#46;<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":"","jetpack_publicize_message":"","jetpack_is_tweetstorm":false,"jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":false,"jetpack_social_options":{"image_generator_settings":{"template":"highway","enabled":false}}},"categories":[4],"tags":[39,50,52,40,21,51,38],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/saladtomatonion.com\/blog\/wp-json\/wp\/v2\/posts\/158"}],"collection":[{"href":"https:\/\/saladtomatonion.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/saladtomatonion.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/saladtomatonion.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/saladtomatonion.com\/blog\/wp-json\/wp\/v2\/comments?post=158"}],"version-history":[{"count":34,"href":"https:\/\/saladtomatonion.com\/blog\/wp-json\/wp\/v2\/posts\/158\/revisions"}],"predecessor-version":[{"id":790,"href":"https:\/\/saladtomatonion.com\/blog\/wp-json\/wp\/v2\/posts\/158\/revisions\/790"}],"wp:attachment":[{"href":"https:\/\/saladtomatonion.com\/blog\/wp-json\/wp\/v2\/media?parent=158"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/saladtomatonion.com\/blog\/wp-json\/wp\/v2\/categories?post=158"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/saladtomatonion.com\/blog\/wp-json\/wp\/v2\/tags?post=158"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}