Medir líneas de código, clases y funciones en Python

Lo cierto es que no importa el número, lo relevante de un código es si funciona y es mantenible. No obstante, como ejercicio de aprendizaje y para contemplar el código desde otra perspectiva, este tipo de métricas pueden resultar divertidas.

Este post surge a raíz de ver un vídeo en YouTube How Large is Your Project? Count The Lines with cloc! y probar la herramienta cloc. Está bien, pero no me convenció del todo. Sin embargo, me picó la curiosidad e investigué un poco sobre como podía obtener este tipo de información por otros medios.

Como por otro lado, yo quería jugar con el módulo ast, me pareció una buena oportunidad para poner en práctica lo aprendido.

Para ver de forma rápida el árbol de ast tenemos varias opciones:

A grades rasgos, con ast lo que hacemos es, a partir de un código fuente, crear un árbol de elementos según la gramática del lenguaje. Con ese árbol, bien podemos visitarlo, bien modificarlo. Es justamente esto lo que hacen herramientas como black cuando formatean el código.

Como guía de supervivencia de ast a la hora de visitar el código ahí va este snippet:

import ast
from _ast import FunctionDef

code = """
def foo(bar):
    print(bar)


foo("bar")
    """

tree = ast.parse(code)
print(ast.dump(tree, indent=4))

# Module(
#     body=[
#         FunctionDef(
#             name='foo',
#             args=arguments(
#                 posonlyargs=[],
#                 args=[
#                     arg(arg='bar')],
#                 kwonlyargs=[],
#                 kw_defaults=[],
#                 defaults=[]),
#             body=[
#                 Expr(
#                     value=Call(
#                         func=Name(id='print', ctx=Load()),
#                         args=[
#                             Name(id='bar', ctx=Load())],
#                         keywords=[]))],
#             decorator_list=[],
#             type_params=[]),
#         Expr(
#             value=Call(
#                 func=Name(id='foo', ctx=Load()),
#                 args=[
#                     Constant(value='bar')],
#                 keywords=[]))],
#     type_ignores=[])


class MyVisitor(ast.NodeVisitor):
    def visit_FunctionDef(self, node: FunctionDef) -> None:
        print(f"FunctionDef {node.name} at line: {node.lineno}")
        # propagate the visit on the children of the input node
        self.generic_visit(node)


my_visitor = MyVisitor()
my_visitor.visit(tree)
# FunctionDef foo at line: 2

Además de usar una clase que herede de ast.NodeVisitor, también hay métodos helper para recorrer el árbol.

import ast

code = """
def foo(bar):
    print(bar)


foo("bar")
    """

tree = ast.parse(code)

# ast.walk, ast.iter_fields, ast.iter_child_nodes, etc.
for node in ast.walk(tree):
    print(f"{node.__class__.__name__}")

# Module
# FunctionDef
# Expr
# arguments
# Expr
# Call
# arg
# Call
# Name
# Constant
# Name
# Name
# Load
# Load
# Load

Habiendo puesto ya en el mapa al módulo ast, ahora toca intentar hacer algo de valor con él y usarlo para medir símbolos en nuestro código parece una buena opción.

En nuestra herramienta yo me he interesado sólo por clases y funciones. Para el número de líneas de código he usado Python puro y duro con lo que, sería muy mejorable si quisiéramos por ejemplo no contar comentarios de tipo docstring o cualquier otra cosa que se te ocurra. Yo (no sé si por pereza o por convencimiento pleno) he llegado a la conclusión de que los docstring son código, así que no me siento culpable por tenerlos en cuenta.

Primero (y para tomarlo como referencia) midamos la librería requests con cloc.

cloc

Ahora, probemos con nuestra propia herramienta:

requests

Un saludo!


Ver también