Considere las siguientes definiciones de metaclase/clase:
class Meta(type): """A python metaclass.""" def greet_user(cls): """Print a friendly greeting identifying the class's name.""" print(f"Hello, I'm the class '{cls.__name__}'!") class UsesMeta(metaclass=Meta): """A class that uses `Meta` as its metaclass."""Como sabemos , definir un método en una metaclase significa que es heredado por la clase y puede ser utilizado por la clase. Esto significa que el siguiente código en la consola interactiva funciona bien:
>>> UsesMeta.greet_user() Hello, I'm the class 'UsesMeta'! Sin embargo, una desventaja importante de este enfoque es que se pierde cualquier documentación que hayamos incluido en la definición del método. Si help(UsesMeta) en la consola interactiva, vemos que no hay ninguna referencia al método greet_user , y mucho menos a la cadena de documentación que ponemos en la definición del método:
Help on class UsesMeta in module __main__: class UsesMeta(builtins.object) | A class that uses `Meta` as its metaclass. | | Data descriptors defined here: | | __dict__ | dictionary for instance variables (if defined) | | __weakref__ | list of weak references to the object (if defined) Ahora, por supuesto, el atributo __doc__ para una clase se puede escribir , por lo que una solución sería reescribir las definiciones de metaclase/clase de la siguiente manera:
from pydoc import render_doc from functools import cache def get_documentation(func_or_cls): """Get the output printed by the `help` function as a string""" return '\n'.join(render_doc(func_or_cls).splitlines()[2:]) class Meta(type): """A python metaclass.""" @classmethod @cache def _docs(metacls) -> str: """Get the documentation for all public methods and properties defined in the metaclass.""" divider = '\n\n----------------------------------------------\n\n' metacls_name = metacls.__name__ metacls_dict = metacls.__dict__ methods_header = ( f'Classmethods inherited from metaclass `{metacls_name}`' f'\n\n' ) method_docstrings = '\n\n'.join( get_documentation(method) for method_name, method in metacls_dict.items() if not (method_name.startswith('_') or isinstance(method, property)) ) properties_header = ( f'Classmethod properties inherited from metaclass `{metacls_name}`' f'\n\n' ) properties_docstrings = '\n\n'.join( f'{property_name}\n{get_documentation(prop)}' for property_name, prop in metacls_dict.items() if isinstance(prop, property) and not property_name.startswith('_') ) return ''.join(( divider, methods_header, method_docstrings, divider, properties_header, properties_docstrings, divider )) def __new__(metacls, cls_name, cls_bases, cls_dict): """Make a new class, but tweak `.__doc__` so it includes information about the metaclass's methods.""" new = super().__new__(metacls, cls_name, cls_bases, cls_dict) metacls_docs = metacls._docs() if new.__doc__ is None: new.__doc__ = metacls_docs else: new.__doc__ += metacls_docs return new def greet_user(cls): """Print a friendly greeting identifying the class's name.""" print(f"Hello, I'm the class '{cls.__name__}'!") class UsesMeta(metaclass=Meta): """A class that uses `Meta` as its metaclass.""" Esto "resuelve" el problema; si ahora help(UsesMeta) en la consola interactiva, los métodos heredados de Meta ahora están completamente documentados:
Help on class UsesMeta in module __main__: class UsesMeta(builtins.object) | A class that uses `Meta` as its metaclass. | | ---------------------------------------------- | | Classmethods inherited from metaclass `Meta` | | greet_user(cls) | Print a friendly greeting identifying the class's name. | | ---------------------------------------------- | | Classmethod properties inherited from metaclass `Meta` | | | | ---------------------------------------------- | | Data descriptors defined here: | | __dict__ | dictionary for instance variables (if defined) | | __weakref__ | list of weak references to the object (if defined)Sin embargo, eso es una gran cantidad de código para lograr este objetivo. ¿Hay una mejor manera?
¿Cómo lo hace la biblioteca estándar?
También tengo curiosidad acerca de la forma en que ciertas clases en la biblioteca estándar manejan esto. Si tenemos una definición de Enum así:
from enum import Enum class FooEnum(Enum): BAR = 1 Luego, escribir help(FooEnum) en la consola interactiva incluye este fragmento:
| ---------------------------------------------------------------------- | Readonly properties inherited from enum.EnumMeta: | | __members__ | Returns a mapping of member name->value. | | This mapping lists all enum members, including aliases. Note that this | is a read-only view of the internal mapping. ¿Cómo exactamente logra esto el módulo enum ?
La razón por la que estoy usando metaclases aquí, en lugar de simplemente definir classmethod de clase en el cuerpo de una definición de clase
Algunos métodos que puede escribir en una metaclase, como __iter__ , __getitem__ o __len__ , no se pueden escribir como classmethod s, pero pueden conducir a un código extremadamente expresivo si los define en una metaclase. El módulo enum es un excelente ejemplo de esto.
No he mirado el resto de stdlib, pero EnumMeta logra esto anulando el método __dir__ (es decir, especificándolo en la clase EnumMeta ):
class EnumMeta(type): . . . def __dir__(self): return ( ['__class__', '__doc__', '__members__', '__module__'] + self._member_names_ )La función help() se basa en dir() , que actualmente no siempre brinda resultados consistentes. Es por eso que su método se pierde en la documentación interactiva generada. Hay un problema de Python abierto sobre este tema que explica el problema con más detalle: vea los errores 40098 (especialmente el primer punto).
Mientras tanto, una solución alternativa es definir un __dir__ personalizado en la metaclase:
class Meta(type): """A python metaclass.""" def greet_user(cls): """Print a friendly greeting identifying the class's name.""" print(f"Hello, I'm the class '{cls.__name__}'!") def __dir__(cls): return super().__dir__() + [k for k in type(cls).__dict__ if not k.startswith('_')] class UsesMeta(metaclass=Meta): """A class that uses `Meta` as its metaclass."""que produce:
Help on class UsesMeta in module __main__: class UsesMeta(builtins.object) | A class that uses `Meta` as its metaclass. | | Methods inherited from Meta: | | greet_user() from __main__.Meta | Print a friendly greeting identifying the class's name.Esto es esencialmente lo que hace enum , ¡aunque su implementación es obviamente un poco más sofisticada que la mía! (El módulo está escrito en python, así que para obtener más detalles, simplemente busque "__dir__" en el código fuente ).