Ricardo Pérez López
IES Doñana, curso 2025/2026
La programación funcional es un paradigma de programación declarativa basado en el uso de definiciones, expresiones y funciones matemáticas.
Tiene su origen teórico en el cálculo lambda, un sistema matemático creado en 1930 por Alonzo Church.
Los lenguajes funcionales se pueden considerar azúcar sintáctico (es decir, una forma equivalente pero sintácticamente más sencilla) del cálculo lambda.
En programación funcional, una función define un cálculo a realizar a partir de unos datos de entrada, con la propiedad de que el resultado de la función sólo puede depender de esos datos de entrada.
Eso significa que una función no puede tener estado interno ni su resultado puede depender del estado del programa.
Además, una función no puede producir ningún efecto observable fuera de ella (los llamados efectos laterales), salvo calcular y devolver su resultado.
Esto quiere decir que en programación funcional no existen los efectos laterales, o se dan de forma muy localizada en partes muy concretas e imprescindibles del programa.
Por todo lo expuesto anteriormente, se dice que las funciones en programación funcional son funciones puras, es decir, funciones que lo único que hacen es calcular su resultado (sin ningún otro efecto) y en las que ese resultado sólo depende de los datos de entrada.
Además, los valores nunca cambian porque no tienen estado interno que se pueda alterar con el tiempo.
Como consecuencia de todo lo anterior, en programación funcional es posible sustituir cualquier expresión por su valor, propiedad que se denomina transparencia referencial.
En programación funcional, las funciones también son valores, por lo que se consideran a éstas como ciudadanas de primera clase.
Un programa funcional está formado únicamente por definiciones de valores y por expresiones que hacen uso de los valores definidos.
Por tanto, en programación funcional, ejecutar un programa equivale a evaluar una expresión.
Para describir el proceso llevado a cabo por el programa no es necesario bajar al nivel de la máquina, sino que basta con interpretarlo como un sistema de evaluación de expresiones.
Esa evaluación de expresiones se lleva a cabo mediante reescrituras que usan las definiciones para tratar de alcanzar la forma normal de la expresión.
En programación funcional, el valor de una expresión depende, exclusivamente, de los valores de las subexpresiones que la forman.
Dichas subexpresiones, además, pueden ser sustituidas libremente por otras que tengan el mismo valor.
A esta propiedad se la denomina transparencia referencial.
Formalmente, se puede definir así:
Transparencia referencial:
Si p = q, entonces f(p) = f(q).
En la práctica, eso significa que la evaluación de una expresión no puede provocar efectos laterales y que su valor no puede depender del momento en el que se evalúe la expresión (la expresión siempre va a valer lo mismo).
En consecuencia, un requisito para conseguir la transparencia referencial es que las expresiones no cambien de valor dependiendo de cuándo se evalúen.
Es decir: una expresión en programación funcional siempre debe tener el mismo valor.
Por tanto, en programación funcional no se permite que la misma expresión, evaluada en dos momentos diferentes, devuelva como resultado dos valores diferentes.
Asimismo, el valor de una expresión tampoco debe depender del orden en el que se evalúen sus subexpresiones.
Los efectos laterales son aquellos que provocan un cambio de estado irremediable en el sistema, que además son observables fuera del contexto donde se producen y que puede dar lugar a que una misma expresión tenga dos valores según el momento en el que se evalúe.
Por ejemplo, las instrucciones de E/S (entrada y salida) provocan efectos laterales, ya que:
Al leer un dato de la entrada (ya sea el teclado, un archivo del disco, una base de datos…) estamos afectando al estado del dispositivo de entrada, y además no se sabe de antemano qué valor se va a recibir, ya que éste proviene del exterior y no lo controlamos.
Al escribir un dato en la salida (ya sea la pantalla, un archivo, una base de datos…) estamos realizando un cambio que afecta irremediablemente al estado del dispositivo de salida.
En posteriores temas veremos que existe un paradigma (el paradigma imperativo) que se basa principalmente en provocar efectos laterales.
Uno de los requisitos para alcanzar la transparencia referencial es que no existan efectos laterales.
Por tanto, en programación funcional no están permitidos los efectos laterales.
Eso significa que:
Al evaluar una expresión no se pueden provocar efectos laterales.
Si esto ocurriera, no podríamos sustituir una expresión por su valor.
El valor de una expresión no puede depender de un efecto lateral ni verse afectado por la existencia de efectos laterales.
Si esto ocurriera, la expresión podría tener valores distintos en momentos distintos.
En cualquiera de los dos casos, se rompería la transparencia referencial.
Cuando escribimos programas (y algoritmos) nos interesa abstraernos del funcionamiento detallado de la máquina que va a ejecutar esos programas.
Nos interesa buscar una metáfora, un símil de lo que significa ejecutar el programa.
De la misma forma que un arquitecto crea modelos de los edificios que se pretenden construir, los programadores podemos usar modelos que simulan en esencia el comportamiento de nuestros programas.
Esos modelos se denominan modelos computacionales o modelos de ejecución.
Los modelos de ejecución nos permiten razonar sobre los programas sin tener que ejecutarlos.
Definición:
Modelo de ejecución:
Es una herramienta conceptual que permite a los programadores razonar sobre el funcionamiento de un programa sin tener que ejecutarlo directamente en el ordenador.
Podemos definir diferentes modelos de ejecución dependiendo, principalmente, de:
El paradigma de programación utilizado (ésto sobre todo).
El lenguaje de programación con el que escribamos el programa.
Los aspectos que queramos estudiar de nuestro programa.
En programación funcional, un programa es una expresión y lo que hacemos al ejecutarlo es evaluar dicha expresión, usando para ello las definiciones de operadores y funciones predefinidas por el lenguaje, así como las definidas por el programador en el código fuente del programa.
Recordemos que la evaluación de una expresión, en esencia, es el proceso de sustituir, dentro de ella, unas subexpresiones por otras que, de alguna manera bien definida, estén más cerca del valor a calcular, y así hasta calcular el valor de la expresión al completo.
Por ello, la ejecución de un programa funcional se puede modelar como un sistema de reescritura al que llamaremos modelo de sustitución.
El modelo de sustitución es un buen modelo de ejecución para la programación funcional gracias a que se cumple la transparencia referencial.
La ventaja del modelo de sustitución es que no necesitamos recurrir a pensar que debajo de todo esto hay un ordenador con una determinada arquitectura hardware, que almacena los datos en celdas de la memoria principal, que ejecuta ciclos de instrucción en la CPU, que las instrucciones modifican los datos de la memoria…
Todo resulta mucho más fácil que eso, ya que todo se reduce a evaluar expresiones, reescribiendo unas subexpresiones por otras, sin importar aspectos secundarios como la tecnología, el momento en el que se evalúan, el orden en el que se evalúan, etc.
Y la evaluación de expresiones no requiere pensar que hay un ordenador que lleva a cabo el proceso de evaluación.
Esto se debe a que la programación funcional se basa en el cálculo lambda, que es un modelo teórico matemático.
Ya estudiamos que evaluar una expresión consiste en encontrar su forma normal.
En programación funcional:
Los intérpretes alcanzan este objetivo a través de múltiples pasos de reducción de las expresiones para obtener otra equivalente más simple.
Toda expresión posee un valor definido, y ese valor no depende del orden ni el momento en el que se evalúe.
El significado de una expresión es su valor, y no puede ocurrir ningún otro efecto, ya sea oculto o no, en ninguna operación que se utilice para calcularlo.
Los valores que comparten características y propiedades comunes se agrupan en conjuntos llamados tipos de datos o, simplemente, tipos.
Por tanto, un tipo (o tipo de datos) es un conjunto de valores:
Tipo (de datos):
Es un conjunto de valores.
Se dice que «un valor pertenece a un tipo» cuando pertenece a ese conjunto (es decir, cuando es uno de los elementos de ese conjunto).
Por ejemplo, el tipo entero representa el conjunto de los números enteros. Los diferentes números enteros pertenecen al tipo entero.
En general, las operaciones se definen de forma que sólo pueden actuar sobre valores de determinados tipos.
O dicho de otra forma: esas son las operaciones que tiene sentido realizar sobre esos valores.
Esto es así porque recordemos que las operaciones actúan como funciones que están definidas sobre un dominio, que es un subconjunto del conjunto origen. Ese conjunto origen sería, a grandes rasgos, el tipo de los valores sobre los que puede actuar.
Por ejemplo: sobre un valor de tipo cadena se puede realizar la operación longitud (pero no la raíz cuadrada), y sobre dos enteros se pueden realizar las operaciones de suma y producto.
En resumen, podemos decir que un tipo:
Es un conjunto de valores.
Que, indirectamente, define también el conjunto de operaciones válidas que se pueden realizar sobre dichos valores.
Recordemos que un dato puede tomar valores.
Por extensión, el tipo de un dato es el conjunto de los posibles valores que puede tomar ese dato.
En cierta forma, el tipo de un dato es como una etiqueta, característica o atributo que va asociado al dato y que define una cualidad muy importante del mismo.
Se dice que «un dato es de un tipo», o que «un dato tiene un tipo» o que «un dato pertenece a un tipo» cuando ese dato tiene (o puede tener) un valor de ese tipo.
Dependiendo del lenguaje de programación utilizado, el tipo de un dato puede venir definido:
Implícitamente, como el tipo del valor que tiene actualmente el dato.
Explícitamente, asociando el tipo al dato mediante una instrucción especial llamada declaración.
Como los datos tienen (o representan) valores, las operaciones también pueden actuar sobre datos.
En realidad, lo que hacen las operaciones es actuar sobre los valores que tienen esos datos.
Por tanto, también se puede decir que las operaciones que se pueden realizar sobre un dato dependen del tipo de ese dato.
Y, por extensión, podemos decir que un tipo de datos:
Es el conjunto de valores que puede tomar un dato de ese tipo.
Que, indirectamente, define también el conjunto de operaciones válidas que se pueden realizar sobre datos de ese tipo.
Definiciónes ampliadas:
Tipo (o tipo de datos):
Es un conjunto de valores que, indirectamente, define también el conjunto de operaciones que se pueden realizar sobre esos valores.
Tipo de un dato:
Es el tipo que tiene ese dato, es decir, una característica o atributo del dato que define el conjunto de valores que puede tomar ese dato y, en consecuencia, también las operaciones que se pueden realizar sobre ese dato.
Igualmente, por extensión podemos definir también el tipo de una expresión como el tipo al que pertenece el valor de la expresión:
Tipo de una expresión:
Es el tipo del valor resultante de evaluar dicha expresión.
Los tipos de un lenguaje de programación tienen un nombre (un identificador) que los representa.
Ejemplos en Python:
El tipo int
define el conjunto de los
números enteros, sobre los que se pueden realizar,
entre otras, las operaciones aritméticas.
Se corresponde más o menos con el símbolo matemático \mathbb{Z}, que ya hemos usado antes y que representa el conjunto de los números enteros en Matemáticas.
El tipo float
define el conjunto de los
números reales, sobre los que se pueden realizar
también operaciones aritméticas.
Se corresponde más o menos con el símbolo matemático \mathbb{R}, que representa el conjunto de los números reales en Matemáticas.
El tipo str
define el conjunto de las
cadenas, sobre las que se pueden realizar otras
operaciones (concatenación, repetición,
etc.).
¿Por qué decimos «más o menos»?
type
La función type
devuelve el tipo de un valor:
Es muy útil para saber el tipo de una expresión compleja:
El sistema de tipos de un lenguaje es el conjunto de reglas que asigna un tipo a cada elemento del programa.
Exceptuando a los lenguajes no tipados (Ensamblador, código máquina, Forth…) todos los lenguajes tienen su propio sistema de tipos, con sus características.
El sistema de tipos de un lenguaje depende también del paradigma de programación que soporte el lenguaje. Por ejemplo, en los lenguajes orientados a objetos, el sistema de tipos se construye a partir de los conceptos propios de la orientación a objetos (clases, interfaces…).
Cuando se intenta realizar una operación sobre un dato cuyo tipo no admite esa operación, se produce un error de tipos.
Ese error puede ocurrir cuando:
Los operandos de un operador no pertenecen al tipo que el operador necesita (ese operador no está definido sobre datos de ese tipo).
Los argumentos de una función o método no son del tipo esperado.
Por ejemplo:
es incorrecto porque el operador +
no está definido
sobre un entero y una cadena (no se pueden sumar un número y una
cadena).
En caso de que exista un error de tipos, lo que ocurre dependerá de si estamos usando un lenguaje interpretado o compilado:
Si el lenguaje es interpretado (Python):
El error se localizará durante la ejecución del programa y el intérprete mostrará un mensaje de error advirtiendo del mismo en el momento justo en que la ejecución alcance la línea de código errónea, para acto seguido finalizar la ejecución del programa.
Si el lenguaje es compilado (Java):
Es muy probable que el comprobador de tipos del compilador detecte el error de tipos durante la compilación del programa, es decir, antes incluso de ejecutarlo. En tal caso, se abortará la compilación para impedir la generación de código objeto erróneo.
Un lenguaje de programación es fuertemente tipado (o de tipado fuerte) si no se permiten violaciones de los tipos de datos.
Es decir, un valor de un tipo concreto no se puede usar como si fuera de otro tipo distinto a menos que se haga una conversión explícita.
Un lenguaje es débilmente tipado (o de tipado débil) si no es de tipado fuerte.
En los lenguajes de tipado débil se pueden hacer operaciones entre datos cuyo tipos no son los que espera la operación, gracias al mecanismo de conversión implícita.
Existen dos mecanismos de conversión de tipos:
Conversión implícita o coerción: cuando el intérprete convierte un valor de un tipo a otro sin que el programador lo haya solicitado expresamente.
Conversión explícita o casting: cuando el programador solicita expresamente la conversión de un valor de un tipo a otro usando alguna construcción u operación del lenguaje.
Los lenguajes de tipado fuerte no realizan conversiones implícitas de tipos salvo excepciones muy concretas (por ejemplo, conversiones entre enteros y reales en expresiones aritméticas).
Los lenguajes de tipado débil se caracterizan, precisamente, por realizar conversiones implícitas cuando, en una expresión, el tipo de un valor no se corresponde con el tipo necesario.
Ejemplo:
Python es un lenguaje fuertemente tipado, por lo que no podemos hacer lo siguiente (da un error de tipos):
En cambio, PHP es un lenguaje débilmente tipado y la expresión anterior en PHP es perfectamente válida (y vale cinco).
El motivo es que el sistema de tipos de PHP convierte
implícitamente la cadena "3"
en el entero 3
cuando se usa
en una operación de suma (+
).
Es importante entender que la conversión de tipos no modifica el dato original, sino que devuelve un nuevo dato a partir del dato original pero con el tipo cambiado.
\text{Tipos básicos}\begin{cases} \text{Números}\begin{cases} \text{Enteros} \\ \text{Reales} \end{cases} \\ \text{Cadenas} \\ \text{Funciones} \\ \text{Lógicos (o \textit{booleanos})} \end{cases}
Hay dos tipos numéricos básicos en Python: los enteros y los reales.
Los enteros se representan con el tipo
int
.
Sólo contienen parte entera, y sus literales se escriben con dígitos
sin punto decimal (ej: 13
).
Los reales se representan con el tipo
float
.
Contienen parte entera y parte fraccionaria, y sus literales se
escriben con dígitos y con punto decimal separando ambas partes (ej:
4.87
).
Los números en notación exponencial (2e3
) también
son reales (2e3
= 2.0\times10^3).
Las operaciones que se pueden realizar con los números son los que cabría esperar (aritméticas, trigonométricas, matemáticas en general).
Los enteros y los reales generalmente se pueden combinar en una misma expresión aritmética y suele resultar en un valor real, ya que se considera que los reales contienen a los enteros.
4 + 3.5
devuelve 7.5
.Por ello, y aunque el lenguaje sea de tipado fuerte, se permite
la conversión implícita entre datos de tipo int
y
float
dentro de una misma expresión para realizar las
operaciones correspondientes.
En el ejemplo anterior, el valor entero 4
se convierte
implícitamente en el real 4.0
debido a
que el otro operando de la suma es un valor real (3.5
).
Finalmente, se obtiene un valor real (7.5
).
Las cadenas son secuencias de cero o más caracteres codificados en Unicode.
En Python se representan con el tipo str
.
Un literal de tipo cadena se escribe encerrando sus caracteres
entre comillas simples ('
) o dobles ("
).
Ejemplos:
"hola"
'Manolo'
"27"
También se pueden escribir literales de tipo cadena encerrándolos
entre triples comillas ('''
o """
).
Estos literales se usan para escribir cadenas formadas por varias líneas. La sintaxis de las triples comillas respeta los saltos de línea. Por ejemplo:
No es lo mismo 27
que "27"
.
27
es un número entero (un literal de tipo int
).
"27"
es una cadena (un literal de tipo str
).
Una cadena vacía es aquella que no contiene
ningún carácter. Se representa con los literales ''
,
""
, ''''''
o """"""
.
Si necesitamos meter el carácter de la comilla simple
('
) o doble ("
) en un literal de tipo cadena,
tenemos dos opciones:
Delimitar la cadena con el otro tipo de comillas. Por ejemplo:
'Pepe dijo: "Yo no voy.", así que no fuimos.'
"Bienvenido, Señor O'Halloran."
«Escapar» la comilla, poniéndole delante una barra
inclinada hacia la izquierda (\
):
"Pepe dijo: \"Yo no voy.\", así que no fuimos."
'Bienvenido, Señor O\'Halloran.'
Las tuplas (datos de tipo tuple
) son una
generalización de las cadenas.
Una tupla es una secuencia de elementos que no tienen por qué ser caracteres, sino que cada uno de ellos pueden ser de cualquier tipo (números, cadenas, booleanos, …, incluso otras tuplas).
Los literales de tipo tupla se representan enumerando sus elementos separados por comas y encerrados entre paréntesis.
Por ejemplo:
Si sólo tiene un elemento, hay que poner una coma detrás:
Al igual que ocurre con las cadenas, tenemos las operaciones
t[0]
,
t[1:]
y
+
(concatenación).
Con la concatenación se pueden crear nuevas tuplas a partir de otras tuplas.
Por ejemplo:
Eso significa que, si t
es
una tupla no vacía, se cumple que
t == (t[0],) + t[1:]
.
Esta propiedad es similar (aunque no exactamente igual) a la que se cumple en las cadenas no vacías.
En programación funcional, las funciones también son datos:
La única operación que se puede realizar sobre
una función es llamarla, que sintácticamente se
representa poniendo paréntesis (
)
justo a
continuación de la función.
Dentro de los paréntesis se ponen los argumentos que se aplican a la función en esa llamada (si es que los necesita), separados por comas.
Por tanto, max
es la
función en sí (un valor de tipo función) , y
max(3, 4)
es una llamada a la función max
con los
argumentos 3
y 4
.
Recordemos que las funciones no tienen expresión canónica, por lo que el intérprete no intentará nunca visualizar un valor de tipo función.
Hemos visto que en Python las conversiones de tipos deben ser explícitas, es decir, que debemos indicar en todo momento qué dato queremos convertir a qué tipo.
Para ello existen funciones cuyo nombre coincide con el tipo al
que queremos convertir el dato: str
, int
y
float
, entre otras.
Convertir un dato a cadena suele funcionar siempre, pero convertir una cadena a otro tipo de dato puede fallar dependiendo del contenido de la cadena:
Recordando lo que dijimos anteriormente, la conversión de tipos no modifica el dato original, sino que devuelve un nuevo dato a partir del dato original pero con el tipo cambiado.
Las funciones de conversión de tipos hacen precisamente eso: devuelven un nuevo dato con un determinado tipo a partir del dato original que reciben como argumento.
Por tanto, la expresión int('24')
devuelve el entero 24
pero no
cambia en modo alguno la cadena '24'
que ha recibido como argumento.
Operador | Descripción | Ejemplo | Resultado | Comentarios |
---|---|---|---|---|
+ |
Suma | 3 + 4 |
7 |
|
- |
Resta | 3 - 4 |
-1 |
|
* |
Producto | 3 * 4 |
12 |
|
/ |
División | 3 / 4 |
0.75 |
Devuelve un float |
% |
Módulo | 4 % 3 8 % 3 |
1 2 |
Resto de la división |
** |
Exponente | 3 ** 4 |
81 |
Devuelve 3^4 |
// |
División entera hacia abajo | 4 // 3 -4 // 3 |
1 -2 |
?? |
Operador | Descripción | Ejemplo | Resultado |
---|---|---|---|
+ |
Concatenación | 'ab' + 'cd'
'ab' 'cd' |
'abcd' |
* |
Repetición | 'ab' * 3 3 * 'ab' |
'ababab' 'ababab' |
[0] |
Primer carácter | 'hola'[0] |
'h' |
[1:] |
Resto de cadena | 'hola'[1:] |
'ola' |
Función | Descripción | Ejemplo | Resultado |
---|---|---|---|
abs( n) |
Valor absoluto | abs(-23) |
23 |
len( cad) |
Longitud de la cadena | len('hola') |
4 |
max( n_1(, n_2)^+) |
Valor máximo | max(2, 5, 3) |
5 |
min( n_1(, n_2)^+) |
Valor mínimo | min(2, 5, 3) |
2 |
round( n[, p]) |
Redondeo | round(23.493) round(23.493, 1) |
23 23.5 |
type( v) |
Tipo del valor | type(23.5) |
<class 'float'> |
Python incluye una gran cantidad de funciones matemáticas
agrupadas dentro del módulo math
.
Los módulos en Python son conjuntos de funciones (y más cosas) que se pueden importar dentro de nuestra sesión o programa.
Son la base de la programación modular, que ya estudiaremos.
Para importar una función de un módulo se puede usar la
orden from
. Por
ejemplo, para importar la función gcd
del módulo math
se haría:
Una vez importada, la función ya se puede usar directamente como cualquier otra.
También se puede importar directamente el módulo en
sí usando la orden import
.
Al importar el módulo, lo que se importan no son sus funciones,
sino el propio módulo, el cual es un objeto (de tipo
module
) al que se accede a través
de su nombre y cuyos atributos son (entre otras cosas)
las funciones que están definidas dentro del módulo.
Por eso, para poder llamar a una función del módulo usando esta
técnica, debemos indicar el nombre del módulo, seguido de un punto
(.
) y el nombre de la función:
Eso significa que podríamos ampliar nuestra gramática para permitir que el nombre de una función en una llamada pudiera contener la parte del módulo:
(
[⟨lista_argumentos⟩])
.
]identificador
identificador
Pero técnicamente no es necesario, ya que las funciones contenidas en un módulo se invocan como si fueran métodos que se ejecutan sobre el objeto módulo, por lo que la sintaxis es la misma que para los métodos y está ya recogida en nuestra gramática:
.
⟨método⟩(
[⟨lista_argumentos⟩])
identificador
Esto nos dice que hay una relación muy estrecha entre funciones y métodos (de hecho, los métodos son funciones que se invocan de una forma especial).
De hecho, cuando el objeto es un módulo, no hablamos de métodos sino de funciones (los módulos no contienen métodos).
No es lo mismo math
, que
math.gcd
, que math.gcd(16, 6)
:
math
es un módulo
(un objeto de tipo module
).
math.gcd
es una
función (no es un método porque math
es un
módulo).
math.gcd(16, 6)
es una llamada a función.
La lista completa de funciones que incluye el módulo math
se puede consultar en su
documentación:
El lenguaje Python es, principalmente, un lenguaje orientado a objetos.
De hecho, todos los datos en Python son objetos
que tienen sus propios atributos (métodos, entre otros) a los que se le
puede acceder usando el operador punto (.
).
Por ello, en Python los términos «dato», «valor» y «objeto» son sinónimos en la práctica.
Los números, las cadenas, los módulos, las funciones… todos son objetos.
Incluso los métodos son objetos, ya que, en realidad, son funciones contenidas dentro de otros objetos, y las funciones son objetos.
Hasta los tipos (como int
o str
) son
objetos que tienen sus propios atributos.
Entraremos a estudiar más en detalle estas características cuando veamos la programación orientada a objetos.
operator
El módulo operator
contiene, en forma de funciones, las operaciones básicas que hasta ahora
hemos utilizado en forma de operadores:
Operador | Operación | Función en el módulo operator |
---|---|---|
+ |
Suma | add |
- |
Resta | sub |
- |
Cambio de signo | neg |
* |
Multiplicación | mul |
/ |
División | truediv |
% |
Módulo | mod |
** |
Exponente | pow |
// |
División entera hacia abajo | floordiv |
Gracias al módulo operator
, podemos reescribir con
funciones las expresiones que utilizan operadores.
Por ejemplo, la expresión:
se puede reescribir como:
Pasar los operadores de una expresión a funciones es un ejercicio muy interesante que ayuda a entender en qué orden se evalúan las subexpresiones y por qué.
En Python, en una llamada a función, los argumentos se evalúan siempre antes que la propia llamada (y de izquierda a derecha).
La expresión 3 * (4 + 5) - 10
se evalúa así:
Y la expresión sub(mul(3, add(4, 5)), 10)
se evalúa así:
sub(mul(3, add(4, 5)), 10) # se evalúa sub (devuelve la función resta)
= sub(mul(3, add(4, 5)), 10) # se evalúa mul (devuelve la función multiplicación)
= sub(mul(3, add(4, 5)), 10) # se evalúa 3 (devuelve 3)
= sub(mul(3, add(4, 5)), 10) # se evalúa add (devuelve la función suma)
= sub(mul(3, add(4, 5)), 10) # se evalúa 4 (devuelve 4)
= sub(mul(3, add(4, 5)), 10) # se evalúa 5 (devuelve 5)
= sub(mul(3, add(4, 5)), 10) # se evalúa add(4, 5) (devuelve 9)
= sub(mul(3, 9), 10) # se evalúa mul(3, 9) (devuelve 27)
= sub(27, 10) # se evalúa 10 (devuelve 10)
= sub(27, 10) # se evalúa sub(27, 10) (devuelve 17)
= 17
Igualmente, en la documentación podemos encontrar una lista de métodos interesantes que operan sobre cadenas:
https://docs.python.org/3/library/stdtypes.html#string-methods
Un dato lógico o booleano es aquel que puede tomar uno de dos posibles valores, que se denotan normalmente como verdadero y falso.
Esos dos valores tratan de representar los dos valores de verdad de la lógica y el álgebra booleana.
Su nombre proviene de George Boole, matemático que definió por primera vez un sistema algebraico para la lógica a mediados del S. XIX.
En Python, el tipo de dato lógico se representa como bool
y sus
posibles valores son False
y True
.
Esos dos valores son formas especiales para los enteros
0
y 1
,
respectivamente.
Los operadores relacionales son operadores que toman dos operandos (que usualmente deben ser del mismo tipo) y devuelven un valor booleano.
Los más conocidos son los operadores de comparación, que sirven para comprobar si un dato es menor, mayor o igual que otro, según un orden preestablecido.
Los operadores de comparación que existen en Python son:
< > <= >= == !=
Por ejemplo:
Las operaciones lógicas se representan mediante operadores lógicos, que son aquellos que toman uno o dos operandos booleanos y devuelven un valor booleano.
Las operaciones básicas del álgebra de Boole se llaman suma, producto y complemento.
En lógica proposicional (un tipo de lógica matemática que tiene estructura de álgebra de Boole), se llaman:
Operación | Operador |
---|---|
Disyunción | \lor |
Conjunción | \land |
Negación | \neg |
En Python se representan como or
, and
y not
,
respectivamente.
Una tabla de verdad es una tabla que muestra el valor lógico de una expresión compuesta, para cada uno de los valores lógicos que puedan tomar sus componentes.
Se usan para definir el significado de las operaciones lógicas y también para verificar que se cumplen determinadas propiedades.
Las tablas de verdad de los operadores lógicos son:
A | B | A\lor{}B |
---|---|---|
F | F | F |
F | V | V |
V | F | V |
V | V | V |
A | B | A\land{}B |
---|---|---|
F | F | F |
F | V | F |
V | F | F |
V | V | V |
A | \neg{}A |
---|---|
F | V |
V | F |
Que traducido a Python sería:
A |
B |
A or B |
---|---|---|
False |
False |
False |
False |
True |
True |
True |
False |
True |
True |
True |
True |
A |
B |
A and B |
---|---|---|
False |
False |
False |
False |
True |
False |
True |
False |
False |
True |
True |
True |
A |
not A |
---|---|
False |
True |
True |
False |
Ley asociativa: \begin{cases} \forall a,b,c \in \mathbb{B}: (a \lor b) \lor c = a \lor (b \lor c) \\ \forall a,b,c \in \mathbb{B}: (a \land b) \land c = a \land (b \land c) \end{cases}
Ley conmutativa: \begin{cases} \forall a,b \in \mathbb{B}: a \lor b = b \lor a \\ \forall a,b \in \mathbb{B}: a \land b = b \land a \end{cases}
Ley distributiva: \begin{cases} \forall a,b,c \in \mathbb{B}: a \lor (b \land c) = (a \lor b) \land (a \lor c) \\ \forall a,b,c \in \mathbb{B}: a \land (b \lor c) = (a \land b) \lor (a \land c) \end{cases}
Elemento neutro: \begin{cases} \forall a \in \mathbb{B}: a \lor F = a \\ \forall a \in \mathbb{B}: a \land V = a \end{cases}
Elemento complementario: \begin{cases} \forall a \in \mathbb{B}; \exists \lnot a \in \mathbb{B}: a \lor \lnot a = V \\ \forall a \in \mathbb{B}; \exists \lnot a \in \mathbb{B}: a \land \lnot a = F \end{cases}
Si (\mathbb{B},\lnot,\lor,\land) cumple lo anterior, entonces es un álgebra de Boole.
Ley asociativa:
Ley conmutativa:
Ley distributiva:
Ley de idempotencia: \begin{cases} \forall a \in \mathbb{B}: a \lor a = a \\ \forall a \in \mathbb{B}: a \land a = a \end{cases}
Ley del elemento absorbente: \begin{cases} \forall a \in \mathbb{B}: a \lor V = V \\ \forall a \in \mathbb{B}: a \land F = F \end{cases}
Ley de identidad: \begin{cases} \forall a \in \mathbb{B}: a \lor F = a \\ \forall a \in \mathbb{B}: a \land V = a \end{cases}
Ley de absorción: \begin{cases} \forall a \in \mathbb{B}: a \lor (a \land b) = a \\ \forall a \in \mathbb{B}: a \land (a \lor b) = a \end{cases}
Ley de involución: \forall a \in \mathbb{B}: \lnot \lnot a = a
Ley del complemento: \begin{cases} \lnot V = F \\ \lnot F = V \end{cases}
Leyes de De Morgan: \begin{cases} \forall a,b \in \mathbb{B}: \lnot ({a \lor b}) = \lnot a \land \lnot b \\ \forall a,b \in \mathbb{B}: \lnot ({a \land b}) = \lnot a \lor \lnot b \end{cases}
Ley de idempotencia:
Ley del elemento absorbente:
Ley de identidad:
Ley de absorción:
Otra forma de representar los operadores y los valores del álgebra de Boole es mediante la notación de la lógica binaria.
Según la notación de la lógica binaria, los diferentes valores y operaciones del álgebra de Boole se representan de la siguiente forma:
Valor u operación | Notación |
---|---|
Valor verdadero | 1 |
Valor falso | 0 |
Producto de A y B | A \cdot
B AB |
Suma de A y B | A + B |
Complemento de A | \overline{A} |
Por ejemplo, las leyes de DeMorgan, que en Python se escriben así:
se escribirían así según la notación de la lógica binaria:
\overline{A + B} = \overline{A} \cdot \overline{B} \overline{AB} = \overline{A} + \overline{B}
Esta notación se emplea principalmente en el diseño de circuitos digitales.
Decimos que dos expresiones lógicas P y Q son equivalentes (y se representa como P \equiv Q, aunque nosotros lo representaremos simplemente como P = Q) si tienen las mismas tablas de verdad, es decir, si se cumple que P vale V cuando Q también, y viceversa.
Para demostrar que se cumple una equivalencia en el álgebra de Boole, se pueden usar dos técnicas:
Demostrar que la propiedad es un teorema que se puede deducir de los axiomas y de otros teoremas ya demostrados.
Usar las tablas de verdad para comprobar si los valores de verdad de P y Q coinciden exactamente.
Por ejemplo, supongamos que queremos demostrar la siguiente propiedad: A(A + B) = A
Podemos demostrarlo siguiendo la siguiente secuencia de razonamientos:
\begin{array}{ccl} & A(A + B) \\ = & & \text{\{Ley distributiva\}} \\ & AA + AB \\ = & & \text{\{Ley de idempotencia\}} \\ & A + AB \\ = & & \text{\{Ley de absorción\}} \\ & A \end{array}
También podemos obtener sus tablas de verdad y comprobar que son idénticas:
A | B | A + B | A(A + B) |
---|---|---|---|
0 | 0 | 0 | 0 |
0 | 1 | 1 | 0 |
1 | 0 | 1 | 1 |
1 | 0 | 1 | 1 |
Como podemos observar, las columnas de A y de A(A + B) son idénticas, lo que significa que toman siempre los mismos valores de verdad y, por tanto, ambas expresiones son equivalentes.
Las expresiones lógicas (o booleanas) se pueden usar para comprobar si se cumple una determinada condición.
Las condiciones en un lenguaje de programación se representan mediante expresiones lógicas cuyo valor (verdadero o falso) indica si la condición se cumple o no se cumple.
Con el operador ternario podemos hacer que el resultado de una expresión varíe entre dos posibles opciones dependiendo de si se cumple o no una condición.
El operador ternario se llama así porque es el único operador en Python que actúa sobre tres operandos.
El uso del operador ternario permite crear lo que se denomina una expresión condicional.
Su sintaxis es:
if
⟨condición⟩
else
⟨valor_si_falso⟩donde:
⟨condición⟩ debe ser una expresión lógica
⟨valor_si_cierto⟩ y ⟨valor_si_falso⟩ pueden ser expresiones de cualquier tipo
El valor de la expresión completa será ⟨valor_si_cierto⟩ si la ⟨condición⟩ es cierta; en caso contrario, su valor será ⟨valor_si_falso⟩.
Ejemplo:
evalúa a 25
.
El operador ternario, así como los operadores lógicos and
y or
, se evalúan
siguiendo una estrategia según la cual no siempre se evalúan
todos sus operandos.
La expresión condicional:
if
⟨condición⟩
else
⟨valor_si_falso⟩se evalúa de la siguiente forma:
Primero siempre se evalúa la ⟨condición⟩.
Si es True
, evalúa
⟨valor_si_cierto⟩.
Si es False
, evalúa
⟨valor_si_falso⟩.
Por tanto, en la expresión condicional nunca se evalúan todos sus operandos, sino sólo los estrictamente necesarios.
Además, no se evalúan de izquierda a derecha, como es lo normal.
La evaluación de los operadores and
y or
sigue un
proceso similar:
Primero se evalúa el operando izquierdo.
El operando derecho sólo se evalúa si el izquierdo no proporciona la información suficiente para determinar el resultado de la operación.
Esto es así porque:
True or
\;\underline{x}
siempre es igual a True
, valga lo
que valga \underline{x}.
False and
\;\underline{x}
siempre es igual a False
, valga lo
que valga \underline{x}.
En ambos casos no es necesario evaluar \underline{x}.
Durante la fase de análisis sintáctico, el compilador o el intérprete traducen el programa fuente en una representación intermedia llamada árbol sintáctico.
Resulta conveniente comprender qué forma tiene ese árbol sintáctico para entender adecuadamente cómo se evalúan las expresiones y, más concretamente, en qué orden se van evaluando las subexpresiones.
En un árbol sintáctico, las hojas representan valores, mientras que los nodos intermedios representan operaciones.
Si una expresión está correctamente escrita según la sintaxis del lenguaje, sólo tendrá un único árbol sintáctico equivalente.
En caso contrario, es que la expresión no es sintácticamente correcta, o bien que la gramática del lenguaje no está bien diseñada.
Si la expresión contiene llamadas a funciones, se haría:
El nodo aplica representa la llamada a la función representada por su primer hijo, pasándole los argumentos representados por el resto de sus hijos.
Por tanto, tendrá tantos hijos como parámetros tenga la función, más uno (la propia función).
Para evaluar una expresión representada por su árbol sintáctico, se va recorriendo éste siguiendo un orden que dependerá del lenguaje de programación uilizado.
En Python se sigue un esquema de recorrido llamado primero en profundidad, donde se van visitando los nodos del árbol de izquierda a derecha y de arriba abajo, buscando siempre el nodo que está más al fondo.
La idea es que, antes de evaluar un nodo, debemos evaluar primero todos sus nodos hijos, en orden, de izquierda a derecha.
De esta forma, para evaluar (reducir) un nodo, debemos reducir primero todos sus nodos hijo antes de reducir el propio nodo.
Si el nodo no tiene hijos, entonces se podrá evaluar directamente.
La evaluación consiste en ir sustituyendo unos subárboles por otros más reducidos hasta acabar teniendo un árbol que represente la forma normal de la expresión a evaluar.
Por ejemplo, en la expresión 2 + 3 * 5
,
representada por este árbol:
El orden en el que vamos evaluando los nodos sería el siguiente:
2
, 3
, 5
, *
,
+
La evaluación se realizaría de la siguiente forma, donde en azul destacamos los nodos que ya están evaluados:
Paso 1: Se empieza visitando la raíz
+
pero, como tiene hijos, antes de evaluarlo se pasa a
visitar su primer hijo (2
).
Paso 2: Como estamos en el nodo 2
y
éste no tiene hijos, se puede evaluar directamente, ya que es un nodo
hoja y, por tanto, representa un valor. La evaluación del nodo no cambia
el nodo ni lo sustituye por ningún otro.
Paso 3: Volvemos al padre del nodo
2
, que es el nodo raíz +
, el cual todavía no
lo podemos evaluar porque aún le queda otro nodo hijo por evaluar (el
nodo *
), así que bajamos hasta él. Éste, a su vez, tampoco
se puede evaluar porque tiene hijos que hay que evaluar antes, el
primero de los cuales es el nodo 3
, así que evaluamos
3
, que se evalúa directamente ya que es un nodo hoja.
Paso 4: Volvemos al padre del nodo
3
, que es el nodo *
, el cual todavía no lo
podemos evaluar porque aún le queda otro nodo hijo por evaluar (el nodo
5
), así que bajamos hasta éste, el cual se evalúa
directamente ya que es un nodo hoja.
Paso 5: Volvemos al padre del nodo
5
, que es el nodo *
, el cual ya se puede
evaluar porque ya se han evaluado todos sus hijos, así que se realiza la
operación 3 * 5
,
dando como resultado 15
, por lo que
el subárbol que cuelga del nodo *
se reduce y se sustituye
por un único nodo hoja 15
.
Paso 6: Volvemos al padre del que ahora es el
nodo 15
, que es el nodo +
, el cual ya se puede
evaluar porque ya se han evaluado todos sus hijos, así que se realiza la
operación 2 + 15
,
dando como resultado 17
, por lo que
el subárbol que cuelga del nodo +
se reduce y se sustituye
por un único nodo hoja 17
.
Como ya se ha reducido el nodo raíz, la evaluación de la expresión ha terminado, dando como resultado un árbol que representa a la forma normal de la expresión inicial.
Recordar que este orden concreto de evaluación (primero en profundidad, donde se evalúan primero todos los nodos hijos antes de evaluar al nodo padre) es uno más de entre varios órdenes de evaluación que existen.
El orden de evaluación concreto que se use dependerá del lenguaje de programación utilizado.
Incluso dentro de un mismo lenguaje, podemos encontrarnos con algunas operaciones concretas que no siguen este orden de evaluación, aunque el resto de las operaciones sí lo hagan.
Por ejemplo, los operadores lógicos o el operador ternario en Python se evalúan siguiendo un orden diferente al indicado aquí, como ya veremos más adelante.
Hasta ahora, hemos visto que la función abs
de Python
tiene la siguiente signatura:
abs(
x:
int)
->
int
Pero sabemos que también puede actuar sobre números reales, por lo que también podría tener la siguiente signatura:
abs(
x:
float)
->
float
En realidad, podríamos definir la función abs
de Python
con la siguiente signatura:
abs(
x:
Number)
->
Number
donde Number
es un tipo que
representa a todos los tipos numéricos en Python (como int
o float
).
Eso quiere decir que el parámetro \underline{x} de la función abs
admite un
valor de cualquier tipo numérico, ya sea un entero o un real.
Por tanto, Number
es
un tipo que representa a varios tipos a la
vez.
Cuando eso ocurre, decimos que ese tipo es polimórfico.
Por eso podemos afirmar que Number
es un tipo polimórfico en
Python.
De la misma forma (aunque se utiliza menos), podemos decir que un valor polimórfico es un valor que pertenece a un tipo polimórfico.
Asimismo, una operación polimórfica es aquella en cuya signatura aparece algún tipo polimórfico.
Por ejemplo, la función abs
definida
con un parámetro de tipo Number
sería polimórfica, ya que ese parámetro tendría un tipo
polimórfico.
Un mismo operador, nombre de función o nombre de método puede representar varias operaciones diferentes, dependiendo del tipo de los operandos o argumentos sobre los que actúa.
Un ejemplo sencillo en Python es el operador +
:
Cuando actúa sobre números, representa la operación de suma:
Cuando actúa sobre cadenas, representa la concatenación de cadenas:
Cuando esto ocurre, decimos que el operador (o la función, o el método) está sobrecargado.
Es decir, es como si el operador +
representara dos
operaciones distintas con dos signaturas distintas:
+(
a:
Number,
b
:
Number)
->
Number
+(
a:
str,
b
:
str)
->
str
de forma que, al usar el operador en una expresión del tipo:
⟨expr⟩_1 +
⟨expr⟩_2
el intérprete llamará a una de las dos operaciones, dependiendo de los tipos de ⟨expr⟩_1 y ⟨expr⟩_2.
La sobrecarga no es polimorfismo, pero induce un cierto tipo de polimorfismo que se denomina polimorfismo ad-hoc.
Esto es así porque tener varias operaciones diferentes con el mismo nombre pero con distinta signatura, equivale a tener una sola operación polimórfica donde algunos operandos pueden tomar un valor de varios tipos.
Por ejemplo, los tipos de a y b representarían a la vez a
Number
y str
.
Una operación puede tener forma de operador, de función o de método.
También podemos encontrarnos operaciones con más de una forma.
Por ejemplo, ya vimos anteriormente la operación «longitud», que consiste en determinar el número de caracteres que tiene una cadena. Esta operación se puede hacer:
Con la función len
, pasando la
cadena como argumento:
Con el método __len__
ejecutado sobre la cadena:
De hecho, en Python hay operaciones que tienen las tres formas. Por ejemplo, ya vimos anteriormente la operación potencia, que consiste en elevar un número a la potencia de otro (x^y). Esta operación se puede hacer:
Con el operador **
:
Con la función pow
:
Con el método __pow__
:
La forma más general de representar una operación es la función, ya que cualquier operación se puede expresar en forma de función (cosa que no ocurre con los operadores y los métodos).
Los operadores y los métodos son formas sintácticas especiales para representar operaciones que se podrían representar igualmente mediante funciones.
Por eso, al hablar de operaciones, y mientras no se diga lo contrario, podremos suponer que están representadas como funciones.
Eso implica que los conceptos de conjunto origen, conjunto imagen, dominio, rango, aridad, argumento, resultado, composición y asociación (o correspondencia), que estudiamos cuando hablamos de las funciones, también existen en los operadores y los métodos.
Es decir: todos esos son conceptos propios de cualquier operación, da igual la forma que tenga esta.
Muchos lenguajes de programación no permiten definir nuevos operadores, pero sí permiten definir nuevas funciones (o métodos, dependiendo del paradigma utilizado).
En algunos lenguajes, los operadores son casos particulares de funciones (o métodos) y se pueden definir como tales. Por tanto, en estos lenguajes se pueden crear nuevos operadores definiendo nuevas funciones (o métodos).
Dos operaciones son iguales si devuelven resultados iguales para argumentos iguales.
Este principio recibe el nombre de principio de extensionalidad.
Principio de extensionalidad:
f = g si y sólo si f(x) = g(x) para todo x.
Por ejemplo: una función que calcule el doble de su argumento multiplicándolo por 2, sería exactamente igual a otra función que calcule el doble de su argumento sumándolo consigo mismo.
En ambos casos, las dos funciones devolverán siempre los mismos resultados ante los mismos argumentos.
Cuando dos operaciones son iguales, podemos usar una u otra indistintamente.