¿Agregar cadenas de documentos a las tuplas con nombre?

85

¿Es posible agregar una cadena de documentación a una tupla con nombre de una manera fácil?

Lo intenté

from collections import namedtuple

Point = namedtuple("Point", ["x", "y"])
"""
A point in 2D space
"""

# Yet another test

"""
A(nother) point in 2D space
"""
Point2 = namedtuple("Point2", ["x", "y"])

print Point.__doc__ # -> "Point(x, y)"
print Point2.__doc__ # -> "Point2(x, y)"

pero eso no es suficiente. ¿Es posible hacerlo de otra manera?

Rickard
fuente

Respuestas:

53

Puede lograr esto creando una clase contenedora simple y vacía alrededor del valor devuelto de namedtuple. Contenido de un archivo que creé ( nt.py):

from collections import namedtuple

Point_ = namedtuple("Point", ["x", "y"])

class Point(Point_):
    """ A point in 2d space """
    pass

Luego, en Python REPL:

>>> print nt.Point.__doc__
 A point in 2d space 

O podrías hacer:

>>> help(nt.Point)  # which outputs...
Ayuda en la clase Point en el módulo nt:

clase Point (Point)
 | Un punto en el espacio 2d
 |  
 | Orden de resolución del método:
 | Punto
 | Punto
 | __builtin __. tupla
 | __builtin __. objeto
 ...

Si no le gusta hacerlo a mano cada vez, es trivial escribir una especie de función de fábrica para hacer esto:

def NamedTupleWithDocstring(docstring, *ntargs):
    nt = namedtuple(*ntargs)
    class NT(nt):
        __doc__ = docstring
    return NT

Point3D = NamedTupleWithDocstring("A point in 3d space", "Point3d", ["x", "y", "z"])

p3 = Point3D(1,2,3)

print p3.__doc__

que salidas:

A point in 3d space
Mark Rushakoff
fuente
2
¿Las subclases no convertirán el namedtupleen un "objeto" completo? ¿Perdiendo así algunas de las ganancias de rendimiento de las tuplas con nombre?
exhuma
5
Si agrega __slots__ = ()a la subclase derivada, puede conservar las ventajas de memoria y rendimiento de usarnamedtuple
ali_m
Todavía agrega otro nivel al MRO, que no está justificado para una cadena de documentos. Sin embargo, uno puede simplemente asignar __doc__y guardar una cadena de documentos personalizada en el objeto original.
Bachsau
70

En Python 3, no se necesita ningún contenedor, ya que los __doc__atributos de los tipos se pueden escribir.

from collections import namedtuple

Point = namedtuple('Point', 'x y')
Point.__doc__ = '''\
A 2-dimensional coordinate

x - the abscissa
y - the ordinate'''

Esto se corresponde estrechamente con una definición de clase estándar, donde la cadena de documentos sigue al encabezado.

class Point():
    '''A 2-dimensional coordinate

    x - the abscissa
    y - the ordinate'''
    <class code>

Esto no funciona en Python 2.

AttributeError: attribute '__doc__' of 'type' objects is not writable.

Terry Jan Reedy
fuente
64

Encontré esta vieja pregunta a través de Google mientras me preguntaba lo mismo.

Solo quería señalar que puede ordenarlo aún más llamando a namedtuple () directamente desde la declaración de la clase:

from collections import namedtuple

class Point(namedtuple('Point', 'x y')):
    """Here is the docstring."""
ParejaOnduladoLíneas
fuente
8
Importante que incluyas __slots__ = ()en la clase. De lo contrario, crea un __dict__para sus attrs, perdiendo la naturaleza liviana de namedtuple.
BoltzmannBrain
34

¿Es posible agregar una cadena de documentación a una tupla con nombre de una manera fácil?

Sí, de varias formas.

Escritura de subclase.NamedTuple - Python 3.6+

A partir de Python 3.6 podemos usar una classdefinición con typing.NamedTupledirectamente, con una cadena de documentación (¡y anotaciones!):

from typing import NamedTuple

class Card(NamedTuple):
    """This is a card type."""
    suit: str
    rank: str

En comparación con Python 2, __slots__no es necesario declarar vacío . En Python 3.8, no es necesario ni siquiera para las subclases.

Tenga en cuenta que la declaración __slots__no puede ser no vacía.

En Python 3, también puede modificar fácilmente el documento en una tupla con nombre:

NT = collections.namedtuple('NT', 'foo bar')

NT.__doc__ = """:param str foo: foo name
:param list bar: List of bars to bar"""

Lo que nos permite ver la intención para ellos cuando les pedimos ayuda:

Help on class NT in module __main__:

class NT(builtins.tuple)
 |  :param str foo: foo name
 |  :param list bar: List of bars to bar
...

Esto es realmente sencillo en comparación con las dificultades que tenemos para lograr lo mismo en Python 2.

Python 2

En Python 2, necesitará

  • subclase la tupla nombrada, y
  • declarar __slots__ == ()

Declarar __slots__es una parte importante que las otras respuestas aquí pasan por alto .

Si no declara __slots__, puede agregar atributos ad-hoc mutables a las instancias, lo que introduce errores.

class Foo(namedtuple('Foo', 'bar')):
    """no __slots__ = ()!!!"""

Y ahora:

>>> f = Foo('bar')
>>> f.bar
'bar'
>>> f.baz = 'what?'
>>> f.__dict__
{'baz': 'what?'}

Cada instancia creará un __dict__cuándo __dict__se accede por separado (la falta de __slots__no impedirá de otro modo la funcionalidad, pero la ligereza de la tupla, la inmutabilidad y los atributos declarados son características importantes de las tuplas con nombre).

También querrá un __repr__, si desea que lo que se repite en la línea de comando le dé un objeto equivalente:

NTBase = collections.namedtuple('NTBase', 'foo bar')

class NT(NTBase):
    """
    Individual foo bar, a namedtuple

    :param str foo: foo name
    :param list bar: List of bars to bar
    """
    __slots__ = ()

__repr__se necesita una como esta si crea la base namedtuple con un nombre diferente (como hicimos anteriormente con el argumento de cadena de nombre 'NTBase'):

    def __repr__(self):
        return 'NT(foo={0}, bar={1})'.format(
                repr(self.foo), repr(self.bar))

Para probar la repr, instancia, luego prueba la igualdad de un pase a eval(repr(instance))

nt = NT('foo', 'bar')
assert eval(repr(nt)) == nt

Ejemplo de la documentación

Los documentos también dan un ejemplo de este tipo, con respecto a __slots__: le estoy agregando mi propia cadena de documentos:

class Point(namedtuple('Point', 'x y')):
    """Docstring added here, not in original"""
    __slots__ = ()
    @property
    def hypot(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5
    def __str__(self):
        return 'Point: x=%6.3f  y=%6.3f  hypot=%6.3f' % (self.x, self.y, self.hypot)

...

La subclase que se muestra arriba se establece __slots__en una tupla vacía. Esto ayuda a mantener bajos los requisitos de memoria al evitar la creación de diccionarios de instancia.

Esto demuestra el uso en el lugar (como sugiere otra respuesta aquí), pero tenga en cuenta que el uso en el lugar puede volverse confuso cuando observa el orden de resolución del método, si está depurando, por lo que originalmente sugerí usarlo Basecomo sufijo para la base namedtuple:

>>> Point.mro()
[<class '__main__.Point'>, <class '__main__.Point'>, <type 'tuple'>, <type 'object'>]
                # ^^^^^---------------------^^^^^-- same names!        

Para evitar la creación de una __dict__subclase de una clase que la usa, también debe declararla en la subclase. Consulte también esta respuesta para obtener más advertencias sobre el uso__slots__ .

Aaron Hall
fuente
3
Aunque no es tan conciso y claro como las otras respuestas, esta debería ser la respuesta aceptada porque destaca la importancia de __slots__. Sin él, está perdiendo el valor liviano de una tupla con nombre.
BoltzmannBrain
7

Desde Python 3.5, las cadenas de documentación de los namedtupleobjetos se pueden actualizar.

De lo nuevo :

Point = namedtuple('Point', ['x', 'y'])
Point.__doc__ += ': Cartesian coodinate'
Point.x.__doc__ = 'abscissa'
Point.y.__doc__ = 'ordinate'
vishes_shell
fuente
3

No es necesario usar una clase contenedora como sugiere la respuesta aceptada. Simplemente agregue literalmente una cadena de documentos:

from collections import namedtuple

Point = namedtuple("Point", ["x", "y"])
Point.__doc__="A point in 2D space"

Esto da como resultado: (ejemplo de uso ipython3):

In [1]: Point?
Type:       type
String Form:<class '__main__.Point'>
Docstring:  A point in 2D space

In [2]: 

¡Voilà!

A Sz
fuente
1
Nota: Esto sólo es válido para Python 3. En Python 2: AttributeError: attribute '__doc__' of 'type' objects is not writable.
Taylor Edmiston
1

Podría inventar su propia versión de la función de fábrica namedtuple de Raymond Hettinger y agregar un docstringargumento opcional . Sin embargo, sería más fácil, y posiblemente mejor, simplemente definir su propia función de fábrica utilizando la misma técnica básica que en la receta. De cualquier manera, terminarás con algo reutilizable.

from collections import namedtuple

def my_namedtuple(typename, field_names, verbose=False,
                 rename=False, docstring=''):
    '''Returns a new subclass of namedtuple with the supplied
       docstring appended to the default one.

    >>> Point = my_namedtuple('Point', 'x, y', docstring='A point in 2D space')
    >>> print Point.__doc__
    Point(x, y):  A point in 2D space
    '''
    # create a base class and concatenate its docstring and the one passed
    _base = namedtuple(typename, field_names, verbose, rename)
    _docstring = ''.join([_base.__doc__, ':  ', docstring])

    # fill in template to create a no-op subclass with the combined docstring
    template = '''class subclass(_base):
        %(_docstring)r
        pass\n''' % locals()

    # execute code string in a temporary namespace
    namespace = dict(_base=_base, _docstring=_docstring)
    try:
        exec template in namespace
    except SyntaxError, e:
        raise SyntaxError(e.message + ':\n' + template)

    return namespace['subclass']  # subclass object created
martineau
fuente
0

Creé esta función para crear rápidamente una tupla con nombre y documentar la tupla junto con cada uno de sus parámetros:

from collections import namedtuple


def named_tuple(name, description='', **kwargs):
    """
    A named tuple with docstring documentation of each of its parameters
    :param str name: The named tuple's name
    :param str description: The named tuple's description
    :param kwargs: This named tuple's parameters' data with two different ways to describe said parameters. Format:
        <pre>{
            str: ( # The parameter's name
                str, # The parameter's type
                str # The parameter's description
            ),
            str: str, # The parameter's name: the parameter's description
            ... # Any other parameters
        }</pre>
    :return: collections.namedtuple
    """
    parameter_names = list(kwargs.keys())

    result = namedtuple(name, ' '.join(parameter_names))

    # If there are any parameters provided (such that this is not an empty named tuple)
    if len(parameter_names):
        # Add line spacing before describing this named tuple's parameters
        if description is not '':
            description += "\n"

        # Go through each parameter provided and add it to the named tuple's docstring description
        for parameter_name in parameter_names:
            parameter_data = kwargs[parameter_name]

            # Determine whether parameter type is included along with the description or
            # if only a description was provided
            parameter_type = ''
            if isinstance(parameter_data, str):
                parameter_description = parameter_data
            else:
                parameter_type, parameter_description = parameter_data

            description += "\n:param {type}{name}: {description}".format(
                type=parameter_type + ' ' if parameter_type else '',
                name=parameter_name,
                description=parameter_description
            )

            # Change the docstring specific to this parameter
            getattr(result, parameter_name).__doc__ = parameter_description

    # Set the docstring description for the resulting named tuple
    result.__doc__ = description

    return result

Luego puede crear una nueva tupla con nombre:

MyTuple = named_tuple(
    "MyTuple",
    "My named tuple for x,y coordinates",
    x="The x value",
    y="The y value"
)

Luego, cree una instancia de la tupla nombrada descrita con sus propios datos, es decir.

t = MyTuple(4, 8)
print(t) # prints: MyTuple(x=4, y=8)

Al ejecutar a help(MyTuple)través de la línea de comando python3, se muestra lo siguiente:

Help on class MyTuple:

class MyTuple(builtins.tuple)
 |  MyTuple(x, y)
 |
 |  My named tuple for x,y coordinates
 |
 |  :param x: The x value
 |  :param y: The y value
 |
 |  Method resolution order:
 |      MyTuple
 |      builtins.tuple
 |      builtins.object
 |
 |  Methods defined here:
 |
 |  __getnewargs__(self)
 |      Return self as a plain tuple.  Used by copy and pickle.
 |
 |  __repr__(self)
 |      Return a nicely formatted representation string
 |
 |  _asdict(self)
 |      Return a new OrderedDict which maps field names to their values.
 |
 |  _replace(_self, **kwds)
 |      Return a new MyTuple object replacing specified fields with new values
 |
 |  ----------------------------------------------------------------------
 |  Class methods defined here:
 |
 |  _make(iterable) from builtins.type
 |      Make a new MyTuple object from a sequence or iterable
 |
 |  ----------------------------------------------------------------------
 |  Static methods defined here:
 |
 |  __new__(_cls, x, y)
 |      Create new instance of MyTuple(x, y)
 |
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |
 |  x
 |      The x value
 |
 |  y
 |      The y value
 |
 |  ----------------------------------------------------------------------
 |  Data and other attributes defined here:
 |  
 |  _fields = ('x', 'y')
 |  
 |  _fields_defaults = {}
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from builtins.tuple:
 |  
 |  __add__(self, value, /)
 |      Return self+value.
 |  
 |  __contains__(self, key, /)
 |      Return key in self.
 |  
 |  __eq__(self, value, /)
 |      Return self==value.
 |  
 |  __ge__(self, value, /)
 |      Return self>=value.
 |  
 |  __getattribute__(self, name, /)
 |      Return getattr(self, name).
 |  
 |  __getitem__(self, key, /)
 |      Return self[key].
 |  
 |  __gt__(self, value, /)
 |      Return self>value.
 |  
 |  __hash__(self, /)
 |      Return hash(self).
 |  
 |  __iter__(self, /)
 |      Implement iter(self).
 |  
 |  __le__(self, value, /)
 |      Return self<=value.
 |  
 |  __len__(self, /)
 |      Return len(self).
 |  
 |  __lt__(self, value, /)
 |      Return self<value.
 |  
 |  __mul__(self, value, /)
 |      Return self*value.
 |  
 |  __ne__(self, value, /)
 |      Return self!=value.
 |  
 |  __rmul__(self, value, /)
 |      Return value*self.
 |  
 |  count(self, value, /)
 |      Return number of occurrences of value.
 |  
 |  index(self, value, start=0, stop=9223372036854775807, /)
 |      Return first index of value.
 |      
 |      Raises ValueError if the value is not present.

Alternativamente, también puede especificar el tipo de parámetro a través de:

MyTuple = named_tuple(
    "MyTuple",
    "My named tuple for x,y coordinates",
    x=("int", "The x value"),
    y=("int", "The y value")
)
Steven
fuente
-2

No, solo puede agregar cadenas de documentos a módulos, clases y funciones (incluidos los métodos)

Jeffrey Aylesworth
fuente