Is there a way to create a "docstring" for objects that hold data?

Let’s say I have a dictionary that holds numerical data, such as a measurements. And another programmer is reading my code, and can see that this is a dictionary that holds key:value pairs that look like items:measurements.

But it’s not clear what particular aspect of that item is being measured, or what unit the data is expressed in.

Technically I could make a very long variable name to make that clear. So instead of just measured_circles{} I could call my dictionary measured_diameters_of_circles_in_inches. But that’s too long a variable name. And if there’s other info I’d like to include about the data in the dictionary, such as what types of items are measured, that would make the variable name even longer.

I could instead opt to make a comment. But this too would be a very long comment. And comments are supposed to help others understand what your code is doing, not describe details of an object. A comment would also not be tied to the object in any way, such that if there’s a reference to the object somewhere else, that comment would be accessible there.

So is there a way to create something like a “docstring” for dictionaries (or at least for a few other more complex data structures), where you could include descriptive information about the data inside it, for FYI purposes?

You could create a class which sole purpose is for the dictionary, and use the built in __repr()__ method to provide this information. (I’m trying to just give a little nudge, but if you need more help, feel free to ask!)

1 Like
>>> class Foo:
	'''
docstring
'''
	pass

>>> help(Foo)
Help on class Foo in module __main__:

class Foo(builtins.object)
 |  docstring
 |  
 |  Data descriptors defined here:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)

>>> 
2 Likes

As for a dictionary, you could always give it a docstring key but then it would be treated as data, so maybe not such a good idea.

1 Like

Yes, this is what I was looking for! The repr() function works elegantly, though I suppose I could also create an independent description method for the class. If you have any thoughts on the pros and cons of either do share!

For others reading along who may still be confused about how to make this work, here is my code. Both result in the same output, but the syntax to access the description text differs slightly.

By modifying repr():

class MyDict:
    '''docstring'''

    my_dict = {'pizza':12, 'table': 36}

    def __repr__(self):
        return 'This string describes the data in the dictionary.'


# Create an instance of the class
my_dict = MyDict()

# Access dictionary
print(my_dict.my_dict) # outputs '{'piiza': 12, 'table': 36}'

# Access description of content in the dictionary
print(repr(my_dict)) # outputs 'This string describes the data in the dictionary.'

By defining a new method:

class MyDict:
    '''docstring'''

    my_dict = {'pizza':12, 'table': 36}

    def description(self):
        return 'This string describes the data in the dictionary.'


# Create an instance of the class
my_dict = MyDict()

# Access dictionary
print(my_dict.my_dict) # outputs '{'piiza': 12, 'table': 36}'

# Access description of content in the dictionary
print(my_dict.description()) # outputs 'This string describes the data in the dictionary.'

__repr__() is a representation of the instance variables. __docstring__ is the documentation of the promises made by the class, just as it would apply to the promises made by a function. Purpose and data are completely separate concerns.

Angle class
from math import factorial
from math import pi as PI
class Angle:
  '''A class to give angles their own sine and cos series'''
  def __init__(self, x):
    '''param x - angle in radians'''
    self.x = x
    self.sin = f"{self.sine():.5}"
    self.cos = f"{self.cosine():.5}"
    self.tan = f"{self.sine() / self.cosine():.5}"

  def term(self, n, k=0):
    '''general term implementation'''
    a = 2 * n + k
    return ((-1) ** n * (self.x ** a)) / factorial (a)

  def sine(self):
    '''series to determine the sine of angle in radians'''
    return sum(map(lambda p: self.term(p, 1), range(0, 6)))

  def cosine(self):
    '''series to determine the cosine of angle in radians'''
    return sum(map(lambda p: self.term(p), range(0, 6)))
alpha = Angle(PI / 6)
print (alpha.sine())
print (alpha.sin)
print (alpha.cosine())
print (alpha.cos)
print (alpha.sine() / alpha.cosine())
print (alpha.tan)
0.4999999999999643
0.5
0.8660254037835535
0.86603
0.5773502691901746
0.57735
help()
>>> help(alpha)
Help on Angle in module __main__ object:

class Angle(builtins.object)
 |  Angle(x)
 |  
 |  A class to give angles their own sine and cosine series
 |  
 |  Methods defined here:
 |  
 |  __init__(self, x)
 |      param x - angle in radians
 |  
 |  cosine(self)
 |      series to determine the cosine of angle in radians
 |  
 |  sine(self)
 |      series to determine the sine of angle in radians
 |  
 |  term(self, n, k=0)
 |      general term implementation
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)

>>> 

Now let’s add a representation method…

  def __repr__(self):
    return f"sin({self.x/PI} * PI) = {self.sin}\ncos({self.x/PI} * PI) = {self.cos}\ntan({self.x/PI} * PI) = {self.tan}"
>>> print (alpha)
sin(0.16666666666666666 * PI) = 0.5
cos(0.16666666666666666 * PI) = 0.86603
tan(0.16666666666666666 * PI) = 0.57735
>>> 

The clever observer will reduce this to 1 / 6 which times PI gives us the radian angle. We factored out PI in the representation to get down to this rational number.

1 Like
import fractions
# ...
  def __repr__(self):
    '''rational factors included in representation'''
    theta = fractions.Fraction(self.x/PI).limit_denominator()
    return f"""
    sin({theta} * PI) = {self.sin}
    cos({theta} * PI) = {self.cos}
    tan({theta} * PI) = {self.tan}
    """
# ...
>>> print (alpha)

    sin(1/6 * PI) = 0.5
    cos(1/6 * PI) = 0.86603
    tan(1/6 * PI) = 0.57735
    
>>> 

In all of this do not lose sight of the fact that we have only a singular piece of data, self.x. Strikes me as a perfect data object for a Node class object. Hmmm.

Okay but I don’t necessarily want to do any calculations on my dictionary. I have a pretty simple question: I just want a dictionary object with a de facto description field so that anyone can examine it and determine what kind of data is and is not allowed into the dictionary. Maybe there’s something I’m not understanding here but your code examples don’t include a dictionary and therefore don’t make clear how to make this work with a dictionary. Where does the dictionary go? Is x the dictionary passed as a parameter into a specific instance of the class? Or should the dictionary be a class variable? It’s really not clear.

And where does the description go? The docstring of the class? The docstring of a user-created method? Either, since they’re both accessible through the help() function? I’m just more confused.

(To add a little context: I first read the Python docs on dictionaries and found nothing built in that could be a descriptor field, hence I asked the question. But I’m still in the middle of the first lesson on Classes in Codecademy, so I’m not terribly familiar with Classes yet.)

1 Like

That’s where a class comes in, or a factory function, both of which can be written to control data types and values that go into the dictionary. The latter would be regulated only at time of creation, though.

The instances ARE dictionaries. There are four keys: self.x, self.sin, self.cos, and self.tan, along with the included methods inherited from the class.

As illustrated above, each component can have its own docstring, always written immediately following its signature line (where help() and __docstring__ can pick it up).

The one inside the class describes the overall purpose and function of the class. The ones inside the methods describe the parameters, operations and returns. It’s simpler this way since the documentation is localized with the code it applies to.

To reiterate, documentation is not data, and should not be physically mixed with data, hence there is no docstring for a dictionary.

1 Like

When we first start learning about classes all the newness distracts from the main goal… Define custom data types. Above we created an Angle data type.

>>> type(alpha)
<class '__main__.Angle'>
>>> 

Our type above has no error checking but a production version would have that. We would want to know that only a number is inputted, but would fall short on determining if the value is actually radians. Numbers don’t have units, concepts do. As for the ratios, they don’t have units, either since the units cancel out on division (soh, cah, toa).

Think of a class as a data wrapper, and when working with classes, design from the smallest data object up to the larger implementation that takes these small objects and threads them together. Keep data front and center and know that it is the determining factor in the design of your class, or custom data type.

1 Like