summaryrefslogtreecommitdiff
path: root/src/05_oop.md
blob: 7d0eacd3b993e8ca15bdd7ecf90870be691cb758 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
# Programación Orientada a Objetos

La *programación orientada a objetos* u *object oriented programming* (OOP) es
un paradigma de programación que envuelve python de pies a cabeza. A pesar de
que python se define como un lenguaje de programación multiparadigma, la
programación orientada a objetos es el paradigma principal de éste. A pesar de
que varias de las características que tratamos en el apartado anterior se
corresponden más con un lenguaje de programación funcional, en python **todo**
(o casi todo) es una clase.

Python usa una programación orientada a objetos basada en clases[^class], a
diferencia de otros lenguajes como JavaScript, donde la orientación a objetos
está basada en prototipos[^proto]. No es el objetivo de este documento el de
contarte cuales son las diferencias entre ambas, pero es interesante que sepas
de su existencia, ya que es una de las pocas diferencias que existen entre
estos dos lenguajes de amplio uso en la actualidad.

[^class]: <https://en.wikipedia.org/wiki/Class-based_programming>
[^proto]: <https://en.wikipedia.org/wiki/Prototype-based_programming>

## Programación basada en clases

Tras haber hecho una afirmación tan categórica como que en python todo son
clases, es nuestra obligación entrar a definir lo que son y qué implica la
programación basada en clases.

Los objetos, *objects*, son entidades que encapsulan un estado, un
comportamiento y una identidad capaz de separarlos de otras entidades. Una
clase, *class*, es la definición de estos objetos.

Saliendo de la definición filosófica y trayéndola a un nivel de andar por casa,
puedes aclararte sabiendo que las clases son la definición enciclopédica de
algo, mientras que los objetos son el propio objeto, persona o animal descrito.

Llevándolo al ejemplo de un perro, la clase es la definición de qué es un perro
y los objetos son los distintos perros que te puedes encontrar en el mundo. La
definición de perro indica qué características ha de tener un ente para ser un
perro, como ser un animal, concretamente doméstico, qué anatomía debe tener,
cómo debe comportarse, etc. Mientras que el propio perro es uno de los casos de
esa definición.

Cada perro tiene una **identidad propia** y es independiente de los otros,
tiene un **comportamiento** concreto (corre, salta, ladra...) y tiene un
**estado** (está despierto o dormido, tiene una edad determinada...).

La diferencia entre una clase y un objeto tiene lógica si lo piensas desde la
perspectiva de que python no tiene ni idea de lo que es un perro y tú tienes
que explicárselo. Una vez lo haces, declarando tu clase, puedes crear
diferentes perros y ponerlos a jugar. Lo bonito de programar es que tu programa
es tu mundo y tú decides lo que es para ti (o para tu programa) un perro.

A nivel práctico, los objetos son grupos de datos (el *estado*) y funciones (la
*funcionalidad*). Estas funciones son capaces de alterar los datos del propio
objeto y no de otro (se intuye el concepto de *identidad*). Analizándolo desde
el conocimiento que ya tienes, es lógico pensar que un objeto es, por tanto,
una combinación de valores y funciones accesible a modo de elemento único.
Exactamente de eso se trata.

Existe una terminología técnica, eso sí, para referirse a esos valores y a esas
funciones. Normalmente los valores se conocen como *propiedades* del objeto y
las funciones se conocen como *métodos*. Así que siempre que hagamos referencia
a cualquiera de estas dos palabras clave debes recordar que hacen referencia a
la programación orientada a objetos.

### Fundamento teórico

La programación basada en clases se basa en tres conceptos fundamentales que
repasaremos aquí de forma rápida para razonar el interés de la programación
orientada a objetos sobre otros paradigmas.

La **encapsulación**[^encapsulation] trata de crear datos con sus métodos
propios para alterarlos de modo que restrinjan el acceso directo al contenido
de estos datos con el fin de asegurar una coherencia o robustez interna. Puedes
entender esto como una forma de esconder información o como mi profesor de
programación II en la universidad solía decir: «Las patatas se pelan en la
cocina del restaurante, no en el comedor». La utilidad de la encapsulación es
la de aislar secciones del programa para tener total control sobre su
contenido gracias a tener total control de la vía de acceso a estos datos. A
nivel práctico este concepto puede usarse para, por ejemplo, obligar a que un
objeto sólo pueda ser alterado en incrementos controlados en lugar de poder
pisarse con un valor arbitrario.

La **herencia**[^inheritance] es un truco para reutilizar código de forma
agresiva que, casualmente, sirve como una buena forma de razonar. Aporta la
posibilidad de crear nuevas *clases* a partir de clases ya existentes.
Volviendo a la simplificación anterior, si una clase es una definición
enciclopédica de un concepto, como un perro, puede estar basada en otra
descripción para evitar contar todo lo relacionado con ella. En el caso del
perro, el perro es un animal. Animal podría ser otra clase definida previamente
de la que el perro heredara y recibiera gran parte de su descripción genérica
para sólo cubrir puntos que necesite especificar como el tamaño, la forma, el
tipo de animal, el comportamiento concreto, etc. Existe la posibilidad de hacer
herencias múltiples también ya que algunos conceptos pueden describirse en dos
superclases distintas: un perro es un animal (vive, muere, se alimenta, se
reproduce) y también es terrestre (camina sobre una superficie, etc). Ambos
conceptos son independientes: los coches también son terrestres pero no son
animales y los peces también son animales pero no terrestres.

Y, finalmente, el **polimorfismo**[^polymorphism]. La propia etimología de la
palabra define con bastante precisión el concepto, pero aplicarlo a la
programación orientada a objetos no es tan evidente. Existen varios tipos de
polimorfismo pero el más sencillo es entender el *subtyping*[^subtyping]. Una
vez lo comprendas el resto será evidente. Si volvemos al ejemplo del perro,
para ciertos comportamientos, nos da igual que tratemos de perros, de peces o
de pájaros, todos son animales y todos los animales se comportan de la misma
forma. Es decir, todas las subclases señaladas comparten el comportamiento de
la superclase animal. Si esto es cierto, puede suponerse que en cualquier caso
en el que se espere un objeto de la clase animal es seguro usar una subclase de
ésta.

Visto desde otra perspectiva, las subclases comparten comportamiento porque
reutilizan las funciones de la clase principal o las redefinen (*herencia*),
pero podemos asegurar que todas las subclases tienen un conjunto de funciones
con la misma estructura, independientemente de lo que hagan, que aseguran que
siempre van a ser compatibles. El nombre de esta cualidad viene a que un perro
puede tomar la forma de un animal.

Los otros tipos de polimorfismo explotan el mismo comportamiento de diferentes
maneras, mientras que recuerdes que es posible programar de modo que el tipo de
los datos que trates sea indiferente o pueda variar es suficiente. Otro ejemplo
de esto son los operadores matemáticos, que son capaces de funcionar en
cualquier tipo de número (integer, float, complex, etc.) de la misma manera, ya
que todos son números, al fin y al cabo.

Entender estos conceptos a nivel intuitivo, sin necesidad de entrar en los
detalles específicos de cada uno, es interesante para cualquier programador y
facilita de forma radical la comprensión de muchas de las decisiones de diseño
tomadas en python y en proyectos relacionados aunque también, por supuesto, de
otros lenguajes y herramientas.

[^encapsulation]: https://en.wikipedia.org/wiki/Encapsulation_(computer_programming)
[^inheritance]: https://en.wikipedia.org/wiki/Inheritance_(object-oriented_programming)
[^polymorphism]: https://en.wikipedia.org/wiki/Polymorphism_(computer_science)
[^subtyping]: https://en.wikipedia.org/wiki/Subtyping


## Sintaxis

En el siguiente ejemplo se muestra la sintaxis básica a la hora de crear una
clase y después instanciar dos nuevos objetos `bobby` y `beltza`. Los puntos
(`.`) se utilizan para indicar a quién pertenece el método o propiedad al que
se hace referencia (*identidad*). De este modo, no ocurrirá lo mismo cuando el
perro (`Dog`) `bobby` ladre (`bark`) que cuando lo haga el perro `beltza`.

Los métodos describen la *funcionalidad* asociada a los perros en general, pero
además, la función `bark` los describe en particular, haciendo que cada perro
tome su nombre (`name`), una propiedad o dicho de otro modo, su *estado*.

``` python
class Dog:
    type = "canine"
    def __init__(self, name):
        self.name = name
    def bark(self):
        print("Woof! My name is " + self.name)

bobby  = Dog("Bobby")
beltza = Dog("Beltza")

bobby.name    # Bobby
beltza.name   # Beltza

bobby.type    # canine
beltza.type   # canine

bobby.bark()  # Prints "Woof! My name is Bobby"
beltza.bark() # Prints "Woof! My name is Beltza"
```

Queda por aclarar, sin embargo, qué es la función `__init__` y por qué tiene un
nombre tan extraño y qué es `type = canine`, que lo trataremos en próximos
apartados de este capítulo.

Antes de entrar en esos dos puntos, que tratan conceptos algo más avanzados, es
interesante ver cómo definir clases mediante la herencia. Basta con introducir
una lista de clases de las que heredar en la definición de la clase, entre
paréntesis, como si de argumentos de entrada de una función se tratara, tal y
como se muestra en la clase `Dog` del siguiente ejemplo ejecutado en la REPL:

``` python
>>> class Animal:
...     def live(self):
...             print("I'm living")
...
>>> class Terrestrial:
...     def move(self):
...             print("I'm moving on the surface")
...
>>> class Dog(Animal, Terrestrial):
...     def bark(self):
...             print("woof!")
...     def move(self):
...             print("I'm walking on the surface")
...
>>> bobby = Dog()
>>> bobby.bark()
woof!
>>> bobby.live()
"I'm living"
>>> bobby.move()
"I'm walking on the surface"
```

El ejemplo muestra un claro uso de la herencia. La clase `Dog` hereda
automáticamente las funciones asociadas a las superclases, pero es capaz de
definir las propias e incluso redefinir algunas. Independientemente de la
redefinición del método `move`, cualquier perro (`Dog`) va a ser capaz de
moverse por la superficie, porque la superclase `Terrestrial` ya le da los
métodos necesarios para hacerlo. Lo que ocurre es que cualquier subclase de
`Terrestrial` tiene la ocasión moverse (`move`) a su manera: en el caso del
perro, caminando.

> NOTA: La herencia es interesante, pero tampoco debe caerse en la psicosis de
> añadir demasiadas superclases. En ocasiones las superclases son necesarias,
> sobre todo cuando aprovechar el polimorfismo facilita el trabajo, pero
> usarlas de forma agresiva genera código extremadamente complejo sin razón.


### Métodos de objeto o funciones de clase: `self`

Te habrás fijado que los métodos reciben un parámetro de entrada llamado `self`
que no se utiliza a la hora de llamarlos: al hacer `bobby.bark()` no se
introduce ningún argumento de entrada a la función `bark`.

Sin embargo, si no se añade el argumento de entrada a la definición del método
`bark` y se llama a `bobby.bark()` pasa lo siguiente:

``` python
>>> class Dog:
...     def bark():
...             pass
...
>>> bobby = Dog()
>>> bobby.bark()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: bark() takes 0 positional arguments but 1 was given
```

Python dice que `bark` espera `0` argumentos posicionales pero se le ha
entregado `1`, que nosotros no hemos metido en la llamada, claro está. Así que
ha debido de ser él.

Efectivamente, python introduce un argumento de entrada en los métodos, el
argumento de entrada que por convención se suele llamar `self`. Este parámetro
es el propio `bobby` en este caso.

> NOTA: Por convención se le denomina `self`. Tú le puedes llamar como te
> apetezca pero, si pretendes que otros programadores te entiendan, mejor
> `self`.

Para explicar por qué ocurre esto es necesario diferenciar bien entre clase y
objeto. Tal y como hemos hecho antes con las definiciones enciclopédicas
(*clase*) y los conceptos del mundo real que encajan en la definición
(*objeto*). Los objetos también se conocen como instancias, son piezas de
información independiente que han sido creadas a partir de la definición que la
clase aportaba.

En python las clases tienen la posibilidad de tener funciones, que definen el
comportamiento de la clase y no el de los objetos que se crean desde ellas.
Ten en cuenta que las clases también deben procesarse y ocupan un espacio en la
memoria, igual que te ocurre a ti, puedes conocer un concepto y su
comportamiento y luego muchos casos que cumplan ese concepto y ambas cosas
son independientes. Esta posibilidad aporta mucha flexibilidad y permite
definir clases complejas.

Ahora bien, para python las funciones de clase y los métodos (de los objetos,
si no no se llamarían métodos), se implementan de la misma manera. Para la
clase ambas cosas son lo mismo. Sin embargo, el comportamiento del operador
punto (`.`), que dice a quién pertenece la función o método, es diferente si el
valor de la izquierda es una clase o un objeto. Introduciendo en el segundo
caso el propio objeto como primer parámetro de entrada, el `self` del que
hablamos, para que la clase sepa qué objeto tiene que alterar. Este es el
mecanismo de la *identidad* del que antes hablamos y no llegamos a definir en
detalle. Cada objeto es único, y a través del `self` se accede a él.

Es un truco interesante para no almacenar las funciones en cada uno de los
objetos como método. En lugar de eso, se mantienen en la definición de la clase
y cuando se llama al método, se busca de qué clase es el objeto y se llama a la
función de la clase con el objeto como argumento de entrada.

Dicho de otra forma, `bobby.bark()` es equivalente a `Dog.bark( bobby )`.

Ilustrado en un ejemplo más agresivo, puedes comprobar que en función de a
través de qué elemento se acceda a la función `bark` python la interpreta de
forma distinta. A veces como función (*function*) y otras veces como método
(*method*), en función de si se accede desde la clase o desde el objeto:

``` python
>>> class Dog:
...     def bark(self):
...             pass
...
>>> type ( Dog.bark)
<class 'function'>
>>> type ( bobby.bark )
<class 'method'>
```

> NOTA: También te habrás fijado, y si no lo has hecho es momento de hacerlo,
> que los nombres de las clases empiezan por mayúscula en los ejemplos (`Dog`)
> mientras que los objetos comienzan en minúscula (`bobby`). Se trata de otra
> convención ampliamente utilizada para saber diferenciar entre uno y otro de
> forma sencilla. Es evidente cuál es la clase y el objeto con los nombres que
> hemos tratado en los ejemplos, pero en otros casos puede no serlo y con este
> sencillo truco facilitas la lectura de tu código. Hay muchas ocasiones en las
> que esta convención se ignora, así que cuidado.  
> Prueba a hacer `type(int)` en la terminal.

### Variables de clase



### Encapsulación explícita

Es posible que te encuentres en alguna ocasión con métodos o propiedades,
*campos* en general, cuyo nombre comience por `_` o por `__`. Se trata de casos
en los que esas propiedades o métodos quieren ocultarse del exterior.

El uso de `_` al inicio del nombre de un campo es una convención que avisa de
que este campo no debe accederse desde el exterior de la clase y su objetivo es
usarlo desde el interior de ésta.

Esta convención se llevó al extremo en algún momento y se decidió crear un caso
en el que esta convención inicial tuviera cierta funcionalidad añadida para las
dobles barras bajas (`__`) que impidiera un acceso accidental a esos campos
conocido como *name mangling*.

#### Campos privados: *name mangling*

El *name mangling* es un truco que hace python para asegurarse de que no se
entra por accidente a las secciones que empiezan por `__`. Añade
`_nombredeclase` al inicio de los campos, transformando su nombre final y
dificultando el acceso por accidente.

Ese acceso accidental no sólo es para que el programador no acceda, ya que, si
se esfuerza la suficiente, va a poder hacerlo de igual modo, si no para que el
propio python no acceda al campo que no corresponde. El hecho de añadir el
nombre de la clase al campo crea una brecha en la herencia, haciendo que los
campos no se hereden de la forma esperada.

En una subclase en la que los campos de la clase madre han sido marcados con
`__`, la herencia hace que estos campos se hereden con el nombre cambiado que
contiene el nombre de la superclase. De este modo, es difícil para la subclase
pisar estos campos ya que tendría que definirlos manualmente con el nombre
cambiado. Crear nuevos campos con `__` no funcionaría, ya que, al haber
cambiado de clase, el nombre generado será distinto.

Este mecanismo es un truco para crear *campos privados*, concepto bastante
común en otros lenguajes como Java o C++, que en python es inexistente.

El concepto de los *campos privados* es interesante en la programación
orientada a objetos. Pensando en la *encapsulación*, es lógico que a veces las
clases definan métodos o propiedades que sólo los objetos creados a partir de
ellas conozcan y que los objetos creados de clases heredadas no. Este es el
método que python tiene para aportar esta funcionalidad.

Es interesante añadir, por otro lado, que python es un lenguaje de programación
muy dinámico por lo que la propia definición de las clases, y muchas cosas más,
puede alterarse una vez creadas. Esto significa que el hecho de ocultar campos
no es más que un acuerdo tácito entre programadores porque, si quisieran,
podrían definir todo de nuevo. Trucos como este sirven para que el programador
sea consciente de que está haciendo cosas que se supone que no debería hacer.
Cuando programes en python, tómate esto como pistas que te indican cómo se
supone que deberías estar usando las clases.

### Acceso a la superclase