Cazadores de Mitos: Las propiedades privadas en Python

El otro día discutía con un buen amigo en Twitter sobre lo que el llamaba “falta de private protected y public keywords“ en Python y recordé la tremenda confusión y desinformación que hay en la red en relación a las propiedades y/o métodos “privadas/os“ en el lenguaje creado por Guido Van Rossum.

En esta entrada voy a intentar explicar por qué no existen propiedades ni métodos privados en Python y por qué no son necesarios, por qué existe esta confusión sobre los métodos y propiedades (o atributos) que utilizan el underscore (_ y __) y cual es la auténtica naturaleza de los mismos.

El origen del Mito


Muy posiblemente el origen del mito comenzó con el fantástico libro de Mark Pilgrim Dive into Python que ha sido el primer contacto y libro de referencia por antonomasia para los novicios en Python desde prácticamente su lanzamiento el treinta de octubre del 2000. Yo mismo aprendí con él.

El capítulo 5.9. del citado libro, lamentablemente, se titula “Private Functions“ y en él se dice lo siguiente:

Como en muchos otros lenguajes, Python tiene el concepto de elementos privados:

  • Funciones privadas, que no pueden ser invocadas desde fuera de su módulo

  • Métodos de clase privados, que no pueden ser invocados desde fuera de su clase

  • Atributos privados, que no pueden ser accedidos desde fuera de su clase


Pero a diferencia de la mayoría de lenguajes, lo que hace a una función, método o atributo de Python privado viene determinado enteramente por su nombre.

Si el nombre de una función, el método de una clase o uno de sus atributos comienza con (pero no termina con) dos guiones bajos (underscores), es privado; todo lo demás es público.

Para reforzar esta afirmación recurre a un sencillo ejemplo que demuestra que no se puede invocar un método llamado “__parse“ desde fuera de una clase. Yo voy a escribir otro sencillo ejemplo en el REPL e iré explicando sobre la marcha por qué Mark Pilgrim estaba, sencillamente, equivocado con respecto a este particular en específico.

>> class Gato(object):
>>>    """Clase Gato"""
>>>    def __init__(self):
>>>        super(Gato, self).__init__()
>>>    def __privado(self):
>>>        """Metodo supuestamente privado"""
>>>        print "Soy privado como los intereses del Estado…"
>>>    def un_metodo(self):
>>>        """Metodo supuestamente publico desde el que se accede a __privado"""
>>>        self.__privado()
>>>
>>> g1 = Gato()
>>> g1.__privado()
>>> Traceback (most recent call last): 
>>>   File "<interactive input>", line 1, in ?
>>> AttributeError: 'Gato' object has no attribute '__privado'
>>> 
>>> g1.un_metodo()
Soy privado como los intereses del Estado…
>>>

Vemos como efectivamente no hemos sido capaces de invocar al método __privado de nuestra clase Gato pero sin embargo al invocar al método un_metodo que llama internamente al método “__privado“ si se ha imprimido el mensaje y por lo tanto “demuestra” la teoría explicada por Pilgrim en Dive Into Python.

Pero esto no es más que una desagradable coincidencia. Que el método no sea accesible desde fuera de la clase no tiene nada que ver con la visibilidad o restricción del método sino que es el efecto de un mecanismo de Python conocido como name mangling (presente en otros lenguajes como C++ o Java) y no sirve para lo que por error se cree de manera popular. Vamos a explicarlo con otro ejemplo en la REPL:

>>> class Gatico(Gato):
>>>    """La clase Gatico hereda de la clase Gato"""
>>>    def __init__(self):
>>>        super(Gatico, self).__init__()
>>>    def __privado(self):
>>>        """Estoy sobreescribiendo el metodo privado?"""
>>>        print "Es necesario abrir los datos al publico y dejar de ser privado…"
>>>    def otro_metodo(self):
>>>        """Soy otro metodo y me llamo asi a posta para nos sobreescribir a un_metodo de la clase base"""
>>>        self.__privado()
>>>
>>> g2 = Gatico()
>>> g2.__privado()
>>> Traceback (most recent call last): 
>>>   File "<interactive input>", line 1, in ?
>>> AttributeError: 'Gatico' object has no attribute '__privado'
>>> 
>>> g2.otro_metodo()
Es necesario abrir los datos al publico y dejar de ser privado…
>>>
>>> g2.un_metodo()
Soy privado como los intereses del Estado…
>>>

¿Qué es lo que ha pasado aquí?. Los programadores de Java y de C++ que están leyendo este artículo (porque lo estáis leyendo verdad, os interesa un montón…) se habrán percatado rápidamente de que algo raro está pasando.

En Java no está permitido sobreescribir métodos privados, si el programador nombra a un método privado en una clase heredada exactamente igual que un método privado de su clase base, sencillamente estará creando un método completamente nuevo en la subclase y este ocultará al método de la superclase.

En C++ se puede sobreescribir cualquier método que sea virtual, de hecho, es una práctica aceptada y recomendable sobreescribir solo funciones virtuales privadas y nunca públicas pero si sobreescribimos un método privado de una clase base en una subclase y lo invocamos desde la subclase, debería de llamarse al método adecuado, es decir, al nuevo método que sobreescribe al de la clase base aunque sea invocado por un método de la clase base desde la subclase. Este es uno de los principios del polimorfismo en C++.

Por lo tanto es extraño que en Python hayamos sobreescrito el método “__privado“ pero sin embargo el método “un_metodo“ siga pudiendo invocar a la implementación del método “__privado“ de su propia clase. De hecho, esto ocurre por que en ningún momento hemos sobreescrito el método “__privado“ de la clase Gato ni es un método privado, ni nunca ha sido intención del lenguaje poder crear métodos privados ni por éste ni por ningún otro medio.

Ahora si que estoy perdido, ¿me lo explica?


Python es un lenguaje potente que permite al programador hacer prácticamente lo que quiera, como por ejemplo, definir algo como True = False y liarla parda o bien sobreescribir una función builtin. Esto es así por la particular visión de como debe ser el lenguaje por parte de los Pythonistas y del BDFL (Benevolent Dictator For Life).

Uno de los principales valores de Python es que trata al programador como mayor de edad y en lugar de imponer restricciones confía en el buen juicio de nosotros, los programadores, para no hacer barbaridades así que la Comunidad define una serie de convenciones sobre el lenguaje.

Entre estas convenciones se encuentra la de nombrar a toda aquella función, todo aquel método o atributo que no sea parte de la API pública (que no métodos ni propiedades) de nuestra clase, librería, framework o módulo y sea perceptible a cambios, la desaparición o el estado obsoleto y olvidado sin previo aviso, con un underscore delante del nombre para informar a los consumidores de dicha API de que se trata de un método interno que puede cambiar sin previo aviso y no está diseñado (ni es útil) para el uso normal de la API.

Si además el nombre de nuestra variable coincide con una clase, método, variable o builtin pero es el único que tiene lógica, por ejemplo, list, sum, rand por convención se añade un underscore al nombre antes de hacer algo “tan guarro“ (para el Zen de Python) como nombrarlo “lst“, “sm“, “rnd“ que no aporta valor semántico.

Por otro lado, para posibilitar que las clases que heredan de clases bases puedan sobreescribir métodos sin romper las llamadas internas de sus clases base a dichos métodos, cualquier método (o función o atributo) que comience con al menos dos underscores (y no acabe en dos underscores, uno si está permitido) es renombrado por el intérprete en la definición de la clase siguiendo la siguiente norma “_classname__methodname“ así que para nuestro ejemplo el método quedaría así “_Gato__privado“ y de hecho podría seguir siendo invocado desde fuera de la clase si así lo queremos:

&gt;&gt;&gt; g1._Gato__privado()
Soy privado como los intereses del Estado...

Y puede ser invocado ya que nunca fue la intención del lenguaje que existieran los métodos privados, ya que en Python, por su propia condición de lenguaje dinámico y sus características, no es necesario. Asimismo, los métodos, funciones o variables que empiezan con un underscore no son importados al usar from bleh import * que por otro lado es una sintaxis considerada “deprectaed“ y mala práctica de programación por el PEP-8.

Conclusión


Ya se que ha sido un artículo ladrillazo pero considero que en vista a la falta total de literatura al respecto era necesario realizar esta aclaración para ayudar así a comprender mejor este maravilloso lenguaje.

En breve escribiré otro artículo explicando porque no es necesario el uso de métodos o propiedades privados. Hasta entonces, happy hacking.

Más Desinformación | Capítulo 5.9. de Dive Into Python

Portada de Genbeta