{"id":218,"date":"2024-01-25T16:18:06","date_gmt":"2024-01-25T16:18:06","guid":{"rendered":"https:\/\/learnpython.elegantwallp.com\/?p=218"},"modified":"2024-01-25T16:18:07","modified_gmt":"2024-01-25T16:18:07","slug":"python-metaclass-example","status":"publish","type":"post","link":"https:\/\/learnpython.elegantwallp.com\/2024\/01\/25\/python-metaclass-example\/","title":{"rendered":"Python Metaclass Example"},"content":{"rendered":"\n<p><strong>Summary<\/strong>: in this tutorial, you\u2019ll learn about a Python metaclass example that creates classes with many features.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Introduction to the Python metaclass example<\/h2>\n\n\n\n<p>The following defines a\u00a0<code>Person<\/code>\u00a0class with two attributes\u00a0<code>name<\/code>\u00a0and\u00a0<code>age<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><code>class Person: def __init__(self, name, age): self.name = name self.age = age @property def name(self): return self._name @name.setter def name(self, value): self._name = value @property def age(self): return self._age @age.setter def age(self, value): self._age = value def __eq__(self, other): return self.name == other.name and self.age == other.age def __hash__(self): return hash(f'{self.name, self.age}') def __str__(self): return f'Person(name={self.name},age={self.age})' def __repr__(self): return f'Person(name={self.name},age={self.age})'<\/code><small>Code language: Python (python)<\/small><\/code><\/pre>\n\n\n\n<p>Typically, when defining a new class, you need to:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Define a list of object\u2019s properties.<\/li>\n\n\n\n<li>Define an\u00a0<code>__init__<\/code>\u00a0method to initialize object\u2019s attributes.<\/li>\n\n\n\n<li>Implement the\u00a0<code>__str__<\/code>\u00a0and\u00a0<code>__repr__<\/code>\u00a0methods to represent the objects in human-readable and machine-readable formats.<\/li>\n\n\n\n<li>Implement the\u00a0<code>__eq__<\/code>\u00a0method to compare objects by values of all properties.<\/li>\n\n\n\n<li>Implement the\u00a0<code>__hash__<\/code>\u00a0method to use the objects of the class as keys of a\u00a0dictionary\u00a0or elements of a\u00a0set.<\/li>\n<\/ul>\n\n\n\n<p>As you can see, it requires a lot of code.<\/p>\n\n\n\n<p>Imagine you want to define a Person class like this and automagically has all the functions above:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><code>class Person: props = &#91;'first_name', 'last_name', 'age']<\/code><small>Code language: Python (python)<\/small><\/code><\/pre>\n\n\n\n<p>To do that, you can use a metaclass.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Define a metaclass<\/h2>\n\n\n\n<p>First, define the&nbsp;<code>Data<\/code>&nbsp;metaclass that inherits from the&nbsp;<code><a href=\"https:\/\/www.pythontutorial.net\/python-oop\/python-type-class\/\">type<\/a><\/code>&nbsp;class:<code>class Data(type): pass<\/code><small>Code language: Python (python)<\/small><\/p>\n\n\n\n<p>Second, override the\u00a0<code>__new__<\/code>\u00a0method to return a new class object:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><code>class Data(type): def __new__(mcs, name, bases, class_dict): class_obj = super().__new__(mcs, name, bases, class_dict) return class_obj<\/code><small>Code language: Python (python)<\/small><\/code><\/pre>\n\n\n\n<p>Note that the\u00a0<code>__new__<\/code>\u00a0method is a\u00a0static method\u00a0of the\u00a0<code>Data<\/code>\u00a0metaclass. And you don\u2019t need to use the\u00a0<code>@staticmethod<\/code>\u00a0decorator because Python treats it special.<\/p>\n\n\n\n<p>Also, the&nbsp;<code>__new__<\/code>&nbsp;method creates a new class like the&nbsp;<code>Person<\/code>&nbsp;class, not the instance of the&nbsp;<code>Person<\/code>&nbsp;class.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Create property objects<\/h2>\n\n\n\n<p>First, define a\u00a0<code>Prop<\/code>\u00a0class that accepts an attribute name and contains three methods for creating a property object(<code>set<\/code>,\u00a0<code>get<\/code>, and\u00a0<code>delete<\/code>). The\u00a0<code>Data<\/code>\u00a0metaclass will use this\u00a0<code>Prop<\/code>\u00a0class for adding property objects to the class.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><code>class Prop: def __init__(self, attr): self._attr = attr def get(self, obj): return getattr(obj, self._attr) def set(self, obj, value): return setattr(obj, self._attr, value) def delete(self, obj): return delattr(obj, self._attr)<\/code><small>Code language: Python (python)<\/small><\/code><\/pre>\n\n\n\n<p>Second, create a new static method\u00a0<code>define_property()<\/code>\u00a0that creates a property object for each attribute from the\u00a0<code>props<\/code>\u00a0list:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><code>class Data(type): def __new__(mcs, name, bases, class_dict): class_obj = super().__new__(mcs, name, bases, class_dict) Data.define_property(class_obj) return class_obj @staticmethod def define_property(class_obj): for prop in class_obj.props: attr = f'_{prop}' prop_obj = property( fget=Prop(attr).get, fset=Prop(attr).set, fdel=Prop(attr).delete ) setattr(class_obj, prop, prop_obj) return class_obj<\/code><small>Code language: Python (python)<\/small><\/code><\/pre>\n\n\n\n<p>The following defines the\u00a0<code>Person<\/code>\u00a0class that uses the\u00a0<code>Data<\/code>\u00a0metaclass:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><code>class Person(metaclass=Data): props = &#91;'name', 'age']<\/code><small>Code language: Python (python)<\/small><\/code><\/pre>\n\n\n\n<p>The\u00a0<code>Person<\/code>\u00a0class has two properties\u00a0<code>name<\/code>\u00a0and\u00a0<code>age<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><code>pprint(Person.__dict__)<\/code><small>Code language: Python (python)<\/small><\/code><\/pre>\n\n\n\n<p>Output:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><code>mappingproxy({'__dict__': &lt;attribute '__dict__' of 'Person' objects>, '__doc__': None, '__module__': '__main__', '__weakref__': &lt;attribute '__weakref__' of 'Person' objects>, 'age': &lt;property object at 0x000002213CA92090>, 'name': &lt;property object at 0x000002213C772A90>, 'props': &#91;'name', 'age']})<\/code><small>Code language: Python (python)<\/small><\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Define __init__ method<\/h2>\n\n\n\n<p>The following defines an\u00a0<code>init<\/code>\u00a0static method and assign it to the\u00a0<code>__init__<\/code>\u00a0attribute of the class object:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><code>class Data(type): def __new__(mcs, name, bases, class_dict): class_obj = super().__new__(mcs, name, bases, class_dict) <em># create property<\/em> Data.define_property(class_obj) <em># define __init__<\/em> setattr(class_obj, '__init__', Data.init(class_obj)) return class_obj @staticmethod def init(class_obj): def _init(self, *obj_args, **obj_kwargs): if obj_kwargs: for prop in class_obj.props: if prop in obj_kwargs.keys(): setattr(self, prop, obj_kwargs&#91;prop]) if obj_args: for kv in zip(class_obj.props, obj_args): setattr(self, kv&#91;0], kv&#91;1]) return _init <em># more methods<\/em><\/code><small>Code language: Python (python)<\/small><\/code><\/pre>\n\n\n\n<p>The following creates a new instance of the\u00a0<code>Person<\/code>\u00a0class and initialize its attributes:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><code>p = Person('John Doe', age=25) print(p.__dict__)<\/code><small>Code language: Python (python)<\/small><\/code><\/pre>\n\n\n\n<p>Output:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><code>{'_age': 25, '_name': 'John Doe'}<\/code><small>Code language: Python (python)<\/small><\/code><\/pre>\n\n\n\n<p>The&nbsp;<code>p.__dict__<\/code>&nbsp;contains two attributes&nbsp;<code>_name<\/code>&nbsp;and&nbsp;<code>_age<\/code>&nbsp;based on the predefined names in the&nbsp;<code>props<\/code>&nbsp;list.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Define __repr__ method<\/h2>\n\n\n\n<p>The following defines the\u00a0<code>repr<\/code>\u00a0static method that returns a function and uses it for the\u00a0<code>__repr__<\/code>\u00a0attribute of the class object:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><code>class Data(type): def __new__(mcs, name, bases, class_dict): class_obj = super().__new__(mcs, name, bases, class_dict) <em># create property<\/em> Data.define_property(class_obj) <em># define __init__<\/em> setattr(class_obj, '__init__', Data.init(class_obj)) <em># define __repr__<\/em> setattr(class_obj, '__repr__', Data.repr(class_obj)) return class_obj @staticmethod def repr(class_obj): def _repr(self): prop_values = (getattr(self, prop) for prop in class_obj.props) prop_key_values = (f'{key}={value}' for key, value in zip(class_obj.props, prop_values)) prop_key_values_str = ', '.join(prop_key_values) return f'{class_obj.__name__}({prop_key_values_str})' return _repr<\/code><small>Code language: Python (python)<\/small><\/code><\/pre>\n\n\n\n<p>The following creates a new instance of the\u00a0<code>Person<\/code>\u00a0class and displays it:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><code>p = Person('John Doe', age=25) print(p)<\/code><small>Code language: Python (python)<\/small><\/code><\/pre>\n\n\n\n<p>Output:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><code>Person(name=John Doe, age=25)<\/code><small>Code language: Python (python)<\/small><\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Define __eq__ and __hash__ methods<\/h2>\n\n\n\n<p>The following defines the\u00a0<code>eq<\/code>\u00a0and\u00a0<code>hash<\/code>\u00a0methods and assigns them to the\u00a0<code>__eq__<\/code>\u00a0and\u00a0<code>__hash__<\/code>\u00a0of the class object of the metaclass:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><code>class Data(type): def __new__(mcs, name, bases, class_dict): class_obj = super().__new__(mcs, name, bases, class_dict) <em># create property<\/em> Data.define_property(class_obj) <em># define __init__<\/em> setattr(class_obj, '__init__', Data.init(class_obj)) <em># define __repr__<\/em> setattr(class_obj, '__repr__', Data.repr(class_obj)) <em># define __eq__ &amp; __hash__<\/em> setattr(class_obj, '__eq__', Data.eq(class_obj)) setattr(class_obj, '__hash__', Data.hash(class_obj)) return class_obj @staticmethod def eq(class_obj): def _eq(self, other): if not isinstance(other, class_obj): return False self_values = &#91;getattr(self, prop) for prop in class_obj.props] other_values = &#91;getattr(other, prop) for prop in other.props] return self_values == other_values return _eq @staticmethod def hash(class_obj): def _hash(self): values = (getattr(self, prop) for prop in class_obj.props) return hash(tuple(values)) return _hash<\/code><small>Code language: Python (python)<\/small><\/code><\/pre>\n\n\n\n<p>The following creates two instances of the Person and compares them. If the values of all properties are the same, they will be equal. Otherwise, they will not be equal:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><code>p1 = Person('John Doe', age=25) p2 = Person('Jane Doe', age=25) print(p1 == p2) <em># False<\/em> p2.name = 'John Doe' print(p1 == p2) <em># True<\/em><\/code><small>Code language: Python (python)<\/small><\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Put it all together<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code><code>from pprint import pprint class Prop: def __init__(self, attr): self._attr = attr def get(self, obj): return getattr(obj, self._attr) def set(self, obj, value): return setattr(obj, self._attr, value) def delete(self, obj): return delattr(obj, self._attr) class Data(type): def __new__(mcs, name, bases, class_dict): class_obj = super().__new__(mcs, name, bases, class_dict) <em># create property<\/em> Data.define_property(class_obj) <em># define __init__<\/em> setattr(class_obj, '__init__', Data.init(class_obj)) <em># define __repr__<\/em> setattr(class_obj, '__repr__', Data.repr(class_obj)) <em># define __eq__ &amp; __hash__<\/em> setattr(class_obj, '__eq__', Data.eq(class_obj)) setattr(class_obj, '__hash__', Data.hash(class_obj)) return class_obj @staticmethod def eq(class_obj): def _eq(self, other): if not isinstance(other, class_obj): return False self_values = &#91;getattr(self, prop) for prop in class_obj.props] other_values = &#91;getattr(other, prop) for prop in other.props] return self_values == other_values return _eq @staticmethod def hash(class_obj): def _hash(self): values = (getattr(self, prop) for prop in class_obj.props) return hash(tuple(values)) return _hash @staticmethod def repr(class_obj): def _repr(self): prop_values = (getattr(self, prop) for prop in class_obj.props) prop_key_values = (f'{key}={value}' for key, value in zip(class_obj.props, prop_values)) prop_key_values_str = ', '.join(prop_key_values) return f'{class_obj.__name__}({prop_key_values_str})' return _repr @staticmethod def init(class_obj): def _init(self, *obj_args, **obj_kwargs): if obj_kwargs: for prop in class_obj.props: if prop in obj_kwargs.keys(): setattr(self, prop, obj_kwargs&#91;prop]) if obj_args: for kv in zip(class_obj.props, obj_args): setattr(self, kv&#91;0], kv&#91;1]) return _init @staticmethod def define_property(class_obj): for prop in class_obj.props: attr = f'_{prop}' prop_obj = property( fget=Prop(attr).get, fset=Prop(attr).set, fdel=Prop(attr).delete ) setattr(class_obj, prop, prop_obj) return class_obj class Person(metaclass=Data): props = &#91;'name', 'age'] if __name__ == '__main__': pprint(Person.__dict__) p1 = Person('John Doe', age=25) p2 = Person('Jane Doe', age=25) print(p1 == p2) <em># False<\/em> p2.name = 'John Doe' print(p1 == p2) <em># True<\/em><\/code><small>Code language: Python (python)<\/small><\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Decorator<\/h2>\n\n\n\n<p>The following defines a class called\u00a0<code>Employee<\/code>\u00a0that uses the\u00a0<code>Data<\/code>\u00a0metaclass:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><code>class Employee(metaclass=Data): props = &#91;'name', 'job_title'] if __name__ == '__main__': e = Employee(name='John Doe', job_title='Python Developer') print(e)<\/code><small>Code language: Python (python)<\/small><\/code><\/pre>\n\n\n\n<p>Output:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><code>Employee(name=John Doe, job_title=Python Developer)<\/code><small>Code language: Python (python)<\/small><\/code><\/pre>\n\n\n\n<p>It works as expected. However, specifying the metaclass is quite verbose. To improve this, you can use a function\u00a0decorator.<\/p>\n\n\n\n<p>First, define a function decorator that returns a new class which is an instance of the\u00a0<code>Data<\/code>\u00a0metaclass:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><code>def data(cls): return Data(cls.__name__, cls.__bases__, dict(cls.__dict__))<\/code><small>Code language: Python (python)<\/small><\/code><\/pre>\n\n\n\n<p>Second, use the\u00a0<code>@data<\/code>\u00a0decorator for any class that uses the\u00a0<code>Data<\/code>\u00a0as the metaclass:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><code>@data class Employee: props = &#91;'name', 'job_title']<\/code><small>Code language: Python (python)<\/small><\/code><\/pre>\n\n\n\n<p>The following shows the complete code:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><code>class Prop: def __init__(self, attr): self._attr = attr def get(self, obj): return getattr(obj, self._attr) def set(self, obj, value): return setattr(obj, self._attr, value) def delete(self, obj): return delattr(obj, self._attr) class Data(type): def __new__(mcs, name, bases, class_dict): class_obj = super().__new__(mcs, name, bases, class_dict) <em># create property<\/em> Data.define_property(class_obj) <em># define __init__<\/em> setattr(class_obj, '__init__', Data.init(class_obj)) <em># define __repr__<\/em> setattr(class_obj, '__repr__', Data.repr(class_obj)) <em># define __eq__ &amp; __hash__<\/em> setattr(class_obj, '__eq__', Data.eq(class_obj)) setattr(class_obj, '__hash__', Data.hash(class_obj)) return class_obj @staticmethod def eq(class_obj): def _eq(self, other): if not isinstance(other, class_obj): return False self_values = &#91;getattr(self, prop) for prop in class_obj.props] other_values = &#91;getattr(other, prop) for prop in other.props] return self_values == other_values return _eq @staticmethod def hash(class_obj): def _hash(self): values = (getattr(self, prop) for prop in class_obj.props) return hash(tuple(values)) return _hash @staticmethod def repr(class_obj): def _repr(self): prop_values = (getattr(self, prop) for prop in class_obj.props) prop_key_values = (f'{key}={value}' for key, value in zip(class_obj.props, prop_values)) prop_key_values_str = ', '.join(prop_key_values) return f'{class_obj.__name__}({prop_key_values_str})' return _repr @staticmethod def init(class_obj): def _init(self, *obj_args, **obj_kwargs): if obj_kwargs: for prop in class_obj.props: if prop in obj_kwargs.keys(): setattr(self, prop, obj_kwargs&#91;prop]) if obj_args: for kv in zip(class_obj.props, obj_args): setattr(self, kv&#91;0], kv&#91;1]) return _init @staticmethod def define_property(class_obj): for prop in class_obj.props: attr = f'_{prop}' prop_obj = property( fget=Prop(attr).get, fset=Prop(attr).set, fdel=Prop(attr).delete ) setattr(class_obj, prop, prop_obj) return class_obj class Person(metaclass=Data): props = &#91;'name', 'age'] def data(cls): return Data(cls.__name__, cls.__bases__, dict(cls.__dict__)) @data class Employee: props = &#91;'name', 'job_title']<\/code><small>Code language: Python (python)<\/small><\/code><\/pre>\n\n\n\n<p>Python 3.7 provided a\u00a0<code>@dataclass<\/code>\u00a0decorator specified in the\u00a0PEP 557\u00a0that has some features like the\u00a0<code>Data<\/code>\u00a0metaclass. Also, the\u00a0dataclass\u00a0offers more features that help you save time when working with classes.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Summary: in this tutorial, you\u2019ll learn about a Python metaclass example that creates classes with many features. Introduction to the Python metaclass example The following defines a\u00a0Person\u00a0class with two attributes\u00a0name\u00a0and\u00a0age: Typically, when defining a new class, you need to: As you can see, it requires a lot of code. Imagine you want to define a [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[32],"tags":[],"class_list":["post-218","post","type-post","status-publish","format-standard","hentry","category-8-metaprogramming-exceptions"],"_links":{"self":[{"href":"https:\/\/learnpython.elegantwallp.com\/wp-json\/wp\/v2\/posts\/218","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/learnpython.elegantwallp.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/learnpython.elegantwallp.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/learnpython.elegantwallp.com\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/learnpython.elegantwallp.com\/wp-json\/wp\/v2\/comments?post=218"}],"version-history":[{"count":1,"href":"https:\/\/learnpython.elegantwallp.com\/wp-json\/wp\/v2\/posts\/218\/revisions"}],"predecessor-version":[{"id":219,"href":"https:\/\/learnpython.elegantwallp.com\/wp-json\/wp\/v2\/posts\/218\/revisions\/219"}],"wp:attachment":[{"href":"https:\/\/learnpython.elegantwallp.com\/wp-json\/wp\/v2\/media?parent=218"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/learnpython.elegantwallp.com\/wp-json\/wp\/v2\/categories?post=218"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/learnpython.elegantwallp.com\/wp-json\/wp\/v2\/tags?post=218"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}