Adición de comportamiento con métodos

Completado

El objetivo final de un sistema es generar resultados útiles. Para ello, debe procesar la entrada. Durante el procesamiento, es posible que necesite la ayuda de varios métodos y datos. En la programación orientada a objetos (OOP), los métodos y los datos se colocan en objetos. Para procesar la entrada y generar un resultado, en OOP necesita métodos.

Métodos en la OOP

Independientemente del paradigma que se use, los métodos pueden llevar a cabo una acción. Esa acción puede ser un cálculo que solo se base en entradas, o bien puede cambiar el valor de una variable.

En la OOP, hay dos tipos de métodos para los objetos:

  • Los métodos externos, que pueden invocar otros objetos.
  • Los métodos internos, que no son accesibles para otros objetos. Además, métodos internos ayudan a realizar una tarea iniciada por la invocación de un método externo.

Con independencia del tipo de método, pueden cambiar el valor del atributo de un objeto, es decir, su estado.

El concepto de estado y quién y qué puede cambiarlo, es un tema importante. Es una parte importante del diseño de las clases y el objeto. Estas preguntas llevan a la siguiente sección, la encapsulación.

Encapsulación: protección de los datos

La idea general de la encapsulación es que los datos de un objeto son internos, algo que solo se refiere al objeto. Los datos son necesarios para el objeto y los métodos para realizar su trabajo, que consiste en llevar a cabo una tarea. Cuando se dice que los datos son internos, significa que se deben proteger frente a otras manipulaciones externas; es decir, de manipulaciones externas sin controlar. ¿Y por qué?

Por qué es necesario

Ahora se explicará por qué otro objeto no debe manipular directamente los datos. Estos son algunos ejemplos:

  • No es necesario conocer el funcionamiento interno. Cuando conduce un coche, pisa un pedal para controlar el embrague o para acelerar o frenar. Como controla el coche desde un nivel superior, no le importa lo que pasa debajo, cómo realiza la acción el coche. Lo mismo sucede con el código. La mayoría de las veces no es necesario saber cómo un objeto hace algo, siempre y cuando haya un método que se pueda invocar que haga lo que quiere.

  • No debería conocer el funcionamiento interno. En lugar de tener un pedal para interactuar con el coche, imagine que tiene un destornillador o un soldador para intentar acelerar. Da miedo, ¿verdad? Sin duda. O bien, imagine que tiene un ejemplo más concreto, una clase Square, con el código siguiente:

    class Square:
         def __init__(self):
             self.height = 2
             self.width = 2
         def set_side(self, new_side):
             self.height = new_side
             self.width = new_side
    
    square = Square()
    square.height = 3 # not a square anymore
    

    En el ejemplo Square, al establecer la variable height se rompe el concepto de lo que es un cuadrado. Debido a cómo se codifica el cuadrado, debe invocar el método set_side() para que el cuadrado funcione correctamente. Se considera más seguro dejar que el objeto se ocupe de sus datos. En casi todas las instancias, debe elegir interactuar a través de un método en lugar de establecer los datos de forma explícita.

Niveles de acceso

¿Cómo puede proteger la clase y el objeto de la manipulación no deseada de los datos? La respuesta es los niveles de acceso. Puede ocultar datos del mundo exterior y de otros objetos, si marca los datos y las funciones con palabras clave concretas. Estas palabras clave se conocen como modificadores de acceso.

Para ocultar los datos, Python agrega prefijos a los nombres de atributo. Un carácter de subrayado inicial, _, es un mensaje al mundo exterior de que es probable que estos datos no se puedan tocar. Al modificar la clase Square, termina con este código:

  class Square:
      def __init__(self):
          self._height = 2
          self._width = 2
      def set_side(self, new_side):
          self._height = new_side
          self._width = new_side

  square = Square()
  square._height = 3 # not a square anymore

Un carácter de subrayado inicial sigue permitiendo que se modifiquen los datos, a los que Python hace referencia como protegidos. ¿Esto se puede mejorar? Sí, con dos subrayados iniciales __, lo que se conoce como privado. La clase Square ahora debería verse como este código:

  class Square:
      def __init__(self):
          self.__height = 2
          self.__width = 2
    def set_side(self, new_side):
          self.__height = new_side
          self.__width = new_side

  square = Square()
  square.__height = 3 # raises AttributeError

Excelente, ningún problema. ¿Se han protegido los datos? Bueno, no del todo. Python simplemente cambia el nombre de los datos subyacentes. Al escribir este código, todavía puede cambiar su valor:

square = Square()
square._Square__height = 3 # is allowed

Muchos otros lenguajes que implementan la protección de datos solucionan este problema de otra manera. Python es único en el sentido de que la protección de datos se parece más a niveles de sugerencia que a una implementación estricta.

¿Qué son los captadores y los establecedores?

Hasta ahora se ha mencionado que los datos en general no se deben manipular desde el exterior. Los datos son la preocupación del objeto. Como sucede con todas las reglas y las recomendaciones estrictas, hay excepciones. A veces es necesario cambiar los datos, o cambiarlos es más sencillo que tener que agregar una cantidad significativa de código.

Los captadores y los establecedores, que también se conocen como descriptores de acceso y mutadores, son métodos dedicados a leer o cambiar los datos. Los captadores se encargan de hacer que los datos internos sean legibles para el exterior, lo que no suena tan mal, ¿verdad? Los establecedores son métodos que pueden cambiar los datos directamente. La idea es que un establecedor actúe como protección para que no se pueda establecer un valor incorrecto. Ahora recuperará la clase Square para ver los captadores y establecedores en acción:

  class Square:
      def __init__(self):
          self.__height = 2
          self.__width = 2
      def set_side(self, new_side):
          self.__height = new_side
          self.__width = new_side
      def get_height(self):
          return self.__height
      def set_height(self, h):
          if h >= 0:
              self.__height = h
          else:
              raise Exception("value needs to be 0 or larger")

  square = Square()
  square.__height = 3 # raises AttributeError

El método set_height() le impide establecer el valor en algo negativo. Si lo hace, inicia una excepción.

Uso de decoradores para captadores y establecedores

Los decoradores son un tema importante en Python. Forman parte de un tema más amplio denominado metaprogramación. Los decoradores son funciones que toman la función como una entrada. La idea es codificar la funcionalidad reutilizable como funciones de decorador y, después, usarla para decorar otras funciones. El propósito es proporcionar a la función una característica que no tenía antes. Un decorador puede, por ejemplo, agregar campos al objeto, medir el tiempo necesario para invocar una función y mucho más.

En el contexto de la OOP y los captadores y establecedores, un decorador @property específico puede ayudarle a eliminar parte del código reutilizable cuando se agregan captadores y establecedores. El decorador @property hace lo siguiente:

  • Crea un campo de respaldo: al decorar una función con el decorador @property, se crea un campo privado de respaldo. Puede invalidar este comportamiento si quiere, pero es estupendo tener un comportamiento predeterminado.
  • Identifica un establecedor: un método de establecedor puede cambiar el campo de respaldo.
  • Identifica un captador: esta función debe devolver el campo de respaldo.
  • Identifica una función de eliminación: esta función puede eliminar el campo.

Ahora se verá este decorador en acción:

class Square:
    def __init__(self, w, h):
        self.__height = h
        self.__width = w
  
    def set_side(self, new_side):
        self.__height = new_side
        self.__width = new_side

    @property
    def height(self):
        return self.__height

    @height.setter
    def height(self, new_value):
        if new_value >= 0:
            self.__height = new_value
        else:
            raise Exception("Value must be larger than 0")

En el código anterior, la función height() está decorada por el decorador @property. Esta acción de decoración crea el campo privado __height. El campo __height no se define en el constructor __init__() porque el decorador ya se encarga de ello. También se produce otra decoración: @height.setter. Esta decoración apunta a un método height() de aspecto similar al establecedor. El nuevo método height toma otro parámetro value como su segundo parámetro.

La capacidad de manipular el alto de forma independiente al ancho seguirá provocando un problema. Debe comprender lo que hace la clase antes de considerar la posibilidad de permitir captadores y establecedores, ya que se puede introducir un riesgo.