Capítulo 4. Programación orientada a objetos y programación funcional
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com
En este capítulo, quiero presentarte dos estilos de programación que probablemente encontrarás en tu carrera de ciencia de datos: la programación orientada a objetos (POO) y la programación funcional (PF). Es muy útil conocer ambos. Aunque nunca escribas código en ninguno de estos estilos, te encontrarás con paquetes que utilizan ampliamente uno u otro. Entre ellos están los paquetes estándar de Python para la ciencia de datos, como pandas y Matplotlib. Me gustaría equiparte con una comprensión de la POO y la FP para que puedas utilizar el código que encuentres de forma más eficaz.
La POO y la FP son paradigmas de programación basados en principios informáticos subyacentes. Algunos lenguajes de programación sólo admiten uno de ellos o favorecen fuertemente uno sobre el otro. Por ejemplo, Java es un lenguaje orientado a objetos. Python admite ambos. La POO es más popular como estilo general en Python, pero también verás el uso ocasional de FP.
Estos estilos también te proporcionan un marco de referencia para dividir tu código. Cuando escribas código, podrías escribir todo lo que quieras hacer como un único script largo. Seguiría funcionando bien, pero es difícil de mantener y depurar. Como se dijo en el Capítulo 1, es importante dividir el código en trozos más pequeños, y tanto la POO como la FP pueden sugerir buenas formas de hacerlo.
En mi código, no me atengo estrictamente a los principios de la programación funcional ni a los de la programación orientada a objetos. A veces defino mis propias clases siguiendo los principios de la POO, y ocasionalmente escribo funciones que se ajustan a los principios de la PF. La mayoría de los programas Python modernos ocupan un término medio que combina ambos paradigmas. En este capítulo, te daré una visión general de ambos estilos para que adquieras una comprensión de los fundamentos de ambos.
Programación Orientada a Objetos
La programación orientada a objetos es muy común en Python. Pero, ¿qué es un "objeto" en este contexto? Puedes pensar en un objeto como una "cosa" que puede describirse con un sustantivo. En el código de la ciencia de datos, algunos objetos comunes podrían ser un DataFrame de pandas, una matriz de NumPy, una figura de Matplotlib o un estimador de scikit-learn.
Un objeto puede contener datos, tiene asociadas algunas acciones y puede interactuar con otros objetos. Por ejemplo, un objeto DataFrame de pandas contiene una lista de nombres de columnas. Una acción asociada a un objeto DataFrame es renombrar las columnas. El DataFrame puede interactuar con un objeto Serie de pandas añadiendo esa serie como nuevacolumna.
También puedes pensar en un objeto como una estructura de datos personalizada. La diseñas para que contenga los datos que desees, de modo que puedas hacer algo con ellos más tarde. Tomando de nuevo como ejemplo un DataFrame de pandas, los diseñadores de pandas idearon una estructura que pudiera contener datos en formato tabular. Luego puedes acceder a los datos en filas y columnas y operar con los datos de esas formas.
En la siguiente sección, presentaré la terminología principal de la programación orientada a objetos y mostraré algunos ejemplos de cómo puedes estar utilizándola ya.
Clases, métodos y atributos
Clases, métodos y atributos son términos importantes que encontrarás en la programación orientada a objetos. Aquí tienes un resumen de cada uno:
-
Una clase define un objeto, y puedes pensar en ella como un plano para hacer más objetos de esa variedad. Un objeto individual es una instancia de esa clase, y cada objeto es una "cosa" individual.
-
Los métodos son algo que puedes hacer a los objetos de esa clase. Definen el comportamiento de ese objeto y pueden modificar sus atributos.
-
Los atributos son variables que constituyen alguna propiedad de esa clase, y cada objeto puede tener distintos datos almacenados en esos atributos.
Todo eso es muy abstracto, así que te daré un ejemplo más concreto. He aquí una forma de adaptar la terminología orientada a objetos al mundo real. El libro que estás leyendo actualmente, Ingeniería de software para científicos de datos, es un objeto de la clase "Libro". Uno de los atributos de este objeto es su número de páginas y otro es el nombre del autor. Un método que puedes invocar sobre este objeto es "leerlo". Hay muchas instancias de la clase "Libro", pero todas tienen un determinado número de páginas, y todas pueden leerse.
En Python, una clase suele nombrarse utilizando CamelCase
, por lo que nombrarías una clase MyClass
en lugar de my_class
. Esta convención te ayuda a identificar las clases más fácilmente. Puedes buscar un atributo utilizando el formato class_instance.attribute
. Puedes llamar a un método utilizando class_instance.method()
(ten en cuenta que esto incluye paréntesis). Los métodos pueden tener argumentos, pero los atributos no.
Por ejemplo, consideremos un DataFrame de pandas. Es probable que estés familiarizado con la sintaxis para crear un nuevo DataFrame:
import
pandas
as
pd
my_dict
=
{
"column_1"
:
[
1
,
2
],
"column_2"
:
[
"a"
,
"b"
]}
df
=
pd
.
DataFrame
(
data
=
my_dict
)
Mirando esto desde una perspectiva orientada a objetos, cuando ejecutas la línea df = pd.DataFrame(data=my_dict)
has inicializado un nuevo objeto de tipo DataFrame, y has pasado algunos datos que se utilizarán para configurar los atributos de eseDataFrame.
Puedes consultar algunos de los atributos de ese DataFrame, así
df
.
columns
df
.
shape
.columns
y .shape
son atributos del objeto df
.
Y puedes llamar a muchos métodos de ese objeto DataFrame, por ejemplo:
df
.
to_csv
(
"file_path"
,
index
=
False
)
.to_csv()
es el método de este ejemplo.
Otro ejemplo familiar de crear un nuevo objeto y llamar a un método procede de scikit-learn. Si estás entrenando un modelo de aprendizaje automático en dos matrices, en las que X_train
contiene las características de entrenamiento y y_train
contiene las etiquetas de entrenamiento, escribirías un código como éste:
from
sklearn.linear_model
import
LogisticRegression
clf
=
LogisticRegression
()
clf
.
fit
(
X_train
,
y_train
)
En este ejemplo, estás inicializando un nuevo objeto clasificador LogisticRegression
y llamando al método .fit()
sobre él.
Aquí tienes otro ejemplo. Éste es el código que crea la Figura 2-3 del Capítulo 2. Aquí se crean dos objetos, un objeto figura Matplotlib y un objeto ejes Matplotlib. A continuación se llaman varios métodos para realizar diversas operaciones con esos objetos, como explicaré en las anotaciones del código:
import
matplotlib
.
pyplot
as
plt
import
numpy
as
np
n
=
np
.
linspace
(
1
,
10
,
1000
)
line_names
=
[
"
Constant
"
,
"
Linear
"
,
"
Quadratic
"
,
"
Exponential
"
,
"
Logarithmic
"
,
"
n log n
"
,
]
big_o
=
[
np
.
ones
(
n
.
shape
)
,
n
,
n
*
*
2
,
2
*
*
n
,
np
.
log
(
n
)
,
n
*
(
np
.
log
(
n
)
)
]
fig
,
ax
=
plt
.
subplots
(
)
fig
.
set_facecolor
(
"
white
"
)
ax
.
set_ylim
(
0
,
50
)
for
i
in
range
(
len
(
big_o
)
)
:
ax
.
plot
(
n
,
big_o
[
i
]
,
label
=
line_names
[
i
]
)
ax
.
set_ylabel
(
"
Relative Runtime
"
)
ax
.
set_xlabel
(
"
Input Size
"
)
ax
.
legend
(
)
fig
.
savefig
(
save_path
,
bbox_inches
=
"
tight
"
)
Inicializa los objetos
figure
yaxes
.Llama al método
set_facecolor
en el objetofig
con un argumentowhite
.Todos los métodos de las siguientes líneas operan sobre el objeto
ax
.Guardar la figura es un método que se ejecuta en el objeto
fig
.
Los objetos figure
y axes
tienen muchos métodos a los que puedes llamar para actualizarlos.
Nota
Matplotlib a veces resulta confuso porque tiene dos tipos de interfaz. Una de ellas está orientada a objetos, y la otra está diseñada para imitar el trazado en MATLAB. Matplotlib se publicó por primera vez en 2003, y sus desarrolladores querían que resultara familiar a las personas acostumbradas a utilizar MATLAB. Hoy en día, es mucho más habitual utilizar la interfaz orientada a objetos, como he mostrado en el ejemplo de código anterior. Pero como el código de la gente depende de los dos tipos de interfaz, ambos deben seguir existiendo. El artículo "Por qué odias Matplotlib" tiene más detalles sobre este tema.
Aunque la terminología que rodea a la POO no te resulte familiar, ya la estarás utilizando con frecuencia en muchos paquetes comunes de la ciencia de datos. El siguiente paso es definir tus propias clases para que puedas utilizar un enfoque orientado a objetos en tu propio código.
Definir tus propias clases
Si quieres escribir tu propio código en un estilo orientado a objetos, tendrás que definir tus propias clases. Te mostraré un par de ejemplos sencillos para hacerlo. El primero repite un texto un número determinado de veces. El segundo utiliza los datos de los Objetivos de Desarrollo Sostenible de la ONU que he utilizado en otros ejemplos a lo largo de este libro. Puedes encontrar más detalles sobre estos datos en "Datos en este libro".
En Python, defines una nueva clase con la declaración class
:
class
RepeatText
():
Es muy común almacenar algunos atributos cada vez que se inicializa una nueva instancia de un objeto. Para ello, Python utiliza un método especial llamado __init__
, que se define así:
def
__init__
(
self
,
n_repeats
):
self
.
n_repeats
=
n_repeats
El primer argumento del método __init__
se refiere a la nueva instancia del objeto que se crea. Por convención, suele llamarse self
. En este ejemplo, el método __init__
toma otro argumento: n_repeats
. La línea self.n_repeats = n_repeats
significa que cada nueva instancia de un objeto RepeatText
tiene un atributo n_repeats
, que debe proporcionarse cada vez que se inicializa un nuevo objeto.
Puedes crear un nuevo objeto RepeatText
así:
repeat_twice
=
RepeatText
(
n_repeats
=
2
)
Entonces puedes acceder al atributo n_repeats
con la siguiente sintaxis:
>>>
(
repeat_twice
.
n_repeats
)
...
2
Definir otro método es similar a definir el método __init__
, pero puedes darle el nombre que quieras, como si fuera una función normal. Como verás más adelante, sigues necesitando el argumento self
si quieres que cada instancia de tu objeto tenga estecomportamiento:
def
multiply_text
(
self
,
some_text
):
((
some_text
+
" "
)
*
self
.
n_repeats
)
Este método buscará el atributo n_repeats
de la instancia de la clase sobre la que actúa. Esto significa que debes crear una instancia de un objeto RepeatText
antes de poder utilizar el método.
Nota
En Python existen métodos especiales que no toman como argumento el parámetro self
: los métodos de clase y los métodos estáticos. Los classmethods se aplican a toda una clase, no sólo a una instancia de una clase, y los staticmethods pueden invocarse sin crear una instancia de la clase. Puedes aprender más sobre ellos en Introducing Python de Bill Lubanovic (O'Reilly, 2019).
Puedes llamar a tu método recién creado de la siguiente manera:
>>>
repeat_twice
.
multiply_text
(
"hello"
)
...
'hello hello'
Aquí tienes la definición completa de la nueva clase:
class
RepeatText
():
def
__init__
(
self
,
n_repeats
):
self
.
n_repeats
=
n_repeats
def
multiply_text
(
self
,
some_text
):
((
some_text
+
" "
)
*
self
.
n_repeats
)
Veamos otro ejemplo. Esta vez vamos a utilizar los datos de los Objetivos de Desarrollo Sostenible de la ONU introducidos en el Capítulo 1. En el siguiente ejemplo, estoy creando un objeto Goal5Data
para contener algunos datos relevantes para el Objetivo 5, "Lograr la igualdad entre los géneros y la autonomía de todas las mujeres y las niñas". Este objeto concreto contendrá datos de una de las metas asociadas a este objetivo, la Meta 5.5: "Garantizar la participación plena y efectiva de la mujer y la igualdad de oportunidades de liderazgo a todos los niveles de decisión en la vida política, económica y pública".
Quiero poder crear un objeto que almacene los datos de cada país para poder manipularlos fácilmente de la misma forma. Aquí está el código para crear la nueva clase y guardar los datos:
class
Goal5Data
(
)
:
def
__init__
(
self
,
name
,
population
,
women_in_parliament
)
:
self
.
name
=
name
self
.
population
=
population
self
.
women_in_parliament
=
women_in_parliament
Este atributo contiene una lista del porcentaje de escaños en el órgano de gobierno del país ocupados por mujeres, por año.
Aquí tienes un método que imprime un resumen de estos datos:
def
print_summary
(
self
):
null_women_in_parliament
=
len
(
self
.
women_in_parliament
)
-
np
.
count_nonzero
(
self
.
women_in_parliament
)
(
f
"There are
{
len
(
self
.
women_in_parliament
)
}
data points for
Indicator
5.5.1
,
'Proportion of seats held by women in national
parliaments
'.")
(
f
"
{
null_women_in_parliament
}
are nulls."
)
De la misma forma que en el ejemplo anterior, puedes crear una nueva instancia de esta clase, así:
usa
=
CountryData
(
name
=
"USA"
,
population
=
336262544
,
women_in_parliament
=
[
13.33
,
14.02
,
14.02
,
...
])
Al llamar al método print_summary
se obtiene el siguiente resultado:
>>>
usa
.
print_summary
()
...
"There are 24 data points for Indicator 5.5.1,
'Proportion of seats held by women in national parliaments'
.
0
are
nulls
.
"
Escribir esto como un método garantiza que el código sea modular, esté bien organizado y sea fácil de reutilizar. También queda muy claro lo que hace, lo que ayudará a quien quiera utilizar tu código.
Utilizaré esta clase en la siguiente sección para mostrarte otro principio de las clases: la herencia.
Principios OOP
A menudo encontrarás estos términos en la programación orientada a objetos: encapsulación, abstracción, herencia y polimorfismo. Los definiré todos en esta sección y mostraré algunos ejemplos de cómo la herencia puede serte útil.
Herencia
Herencia significa que puedes ampliar una clase creando otra que se base en ella. Esto ayuda a reducir la repetición, porque si necesitas una nueva clase estrechamente relacionada con una que ya has escrito, no necesitas duplicar esa clase para hacer un pequeño cambio.
Puede que no necesites utilizar la herencia al definir tus propias clases, pero puede que necesites utilizarla con clases de una biblioteca externa. Verás un par de ejemplos de herencia para la validación de datos más adelante en el libro, en "Validación de datos con Pydantic" y en "Añadir funcionalidad a tu API". En esta sección, quiero ayudarte a detectar y comprender la herencia cuando te encuentres con ella.
Puedes detectar una clase que utiliza la herencia porque tendrá la siguiente sintaxis:
class
NewClass
(
OriginalClass
):
...
La clase NewClass
puede utilizar todos los atributos y métodos de la clase OriginalClass
, pero tú puedes anular cualquiera de ellos que quieras modificar. El término "padre" suele utilizarse para referirse a la clase original, y la nueva clase que hereda de ella suele denominarse clase "hija".
Aquí tienes un ejemplo de una nueva clase, Goal5TimeSeries
que hereda de la clase Goal5Data
de la sección anterior, convirtiéndola en una clase que puede trabajar con datos de series temporales:
class
Goal5TimeSeries
(
Goal5Data
):
def
__init__
(
self
,
name
,
population
,
women_in_parliament
,
timestamps
):
super
()
.
__init__
(
name
,
population
,
women_in_parliament
)
self
.
timestamps
=
timestamps
El método __init__
tiene esta vez un aspecto un poco diferente. Utilizar super()
significa que se llama al método __init__
de la clase padre, que inicializa los atributos name
, population
y women_in_parliament
.
Puedes crear un nuevo objeto Goal5TimeSeries
, así:
india
=
Goal5TimeSeries
(
name
=
"India"
,
population
=
1417242151
,
women_in_parliament
=
[
9.02
,
9.01
,
8.84
,
...
],
timestamps
=
[
2000
,
2001
,
2002
,
...
])
Y puedes seguir accediendo al método desde la clase Goal5Data
:
>>>
india
.
print_summary
()
...
"There are 24 data points for Indicator 5.5.1,
'Proportion of seats held by women in national parliaments'
.
0
are
nulls
.
"
También puedes añadir un nuevo método que sea relevante para la clase hija. Por ejemplo, este nuevo método fit_trendline()
ajusta una línea de regresión a los datos para encontrar su tendencia:
from
scipy
.
stats
import
linregress
class
Goal5TimeSeries
(
Goal5Data
)
:
def
__init__
(
self
,
name
,
population
,
women_in_parliament
,
timestamps
)
:
super
(
)
.
__init__
(
name
,
population
,
women_in_parliament
)
self
.
timestamps
=
timestamps
def
fit_trendline
(
self
)
:
result
=
linregress
(
self
.
timestamps
,
self
.
women_in_parliament
)
slope
=
round
(
result
.
slope
,
3
)
r_squared
=
round
(
result
.
rvalue
*
*
2
,
3
)
return
slope
,
r_squared
Utiliza la función
linregress
descipy
para ajustar una línea recta a través de los datos utilizando la regresión lineal.Calcula el coeficiente de determinación (R-cuadrado) para determinar la bondad de ajuste de la recta.
La llamada al nuevo método devuelve la pendiente de la línea de tendencia y el error cuadrático medio normalizado del ajuste de la línea a los datos:
>>>
india
.
fit_trendline
()
...
(
0.292
,
0.869
)
Si utilizas la herencia en tus propias clases, te permite ampliar las capacidades de las clases que creas. Esto significa menos duplicación de código y ayuda a mantener tu código modular. También es muy útil heredar de clases de una biblioteca externa. De nuevo, esto significa que no duplicas su funcionalidad, pero puedes añadir funciones adicionales.
Encapsulación
La encapsulación significa que tu clase oculta sus detalles al exterior. Sólo puedes ver la interfaz de la clase, no los detalles internos de lo que ocurre. La interfaz está formada por los métodos y atributos que diseñes. No es tan común en Python, pero en otros lenguajes de programación las clases suelen diseñarse con métodos o atributos ocultos o privados que no pueden modificarse desde el exterior.
Sin embargo, el concepto de encapsulación se sigue aplicando en Python, y muchas bibliotecas y aplicaciones se aprovechan de él. pandas es un gran ejemplo de ello. pandas utiliza la encapsulación proporcionando métodos y atributos que te permiten interactuar con los datos manteniendo ocultos los detalles de implementación subyacentes. Un objeto DataFrame encapsula datos y proporciona varios métodos para acceder a ellos, filtrarlos y transformarlos. Como mencioné en el Capítulo 3, los DataFrames de pandas utilizan NumPy bajo el capó, pero no necesitas saberlo para utilizarlos. Puedes utilizar la interfaz DataFrame de pandas para realizar tus tareas, pero si necesitas profundizar más, también puedes utilizar los métodos de NumPy.
Nota
Las interfaces son extremadamente importantes porque a menudo otro código o clases dependerán de la existencia de algún atributo o método, de modo que si cambias esa interfaz puede romperse otro código. Está bien cambiar el funcionamiento interno de tu clase, por ejemplo, cambiar los cálculos dentro de algún método para hacerlo más eficiente. Pero debes hacer que la interfaz sea fácil de usar desde el principio e intentar no cambiarla. Hablaré de las interfaces con más detalle en el capítulo 8.
Abstracción
La abstracción está estrechamente vinculada a la encapsulación. Significa que debes tratar una clase con el nivel de detalle adecuado. Así que puedes optar por mantener los detalles de algún cálculo dentro de un método, o puedes permitir que se acceda a él a través de la interfaz. De nuevo, esto es más habitual en otros lenguajes de programación.
Polimorfismo
El polimorfismo significa que puedes tener la misma interfaz para distintas clases, lo que simplifica tu código y reduce las repeticiones. Es decir, dos clases pueden tener un método con el mismo nombre que produzca un resultado similar, pero el funcionamiento interno es diferente. Las dos clases pueden ser padre e hijo, o no estar relacionadas.
scikit-learn contiene un gran ejemplo de polimorfismo. Cada clasificador tiene el mismo método fit
para entrenar al clasificador sobre unos datos, aunque esté definido como una clase diferente. Aquí tienes un ejemplo de entrenamiento de dos clasificadores diferentes sobre unos datos:
from
sklearn.linear_model
import
LogisticRegression
from
sklearn.ensemble
import
RandomForestClassifier
lr_clf
=
LogisticRegression
()
lr_clf
.
fit
(
X_train
,
y_train
)
rf_clf
=
RandomForestClassifier
()
rf_clf
.
fit
(
X_train
,
y_train
)
Aunque LogisticRegression
y RandomForestClassifier
son clases diferentes, ambas tienen un método .fit()
que toma como argumentos los datos de entrenamiento y las etiquetas de entrenamiento. Compartir el nombre del método te facilita cambiar el clasificador sin cambiar gran parte de tu código.
Esto ha sido un breve resumen de las principales características de la programación orientada a objetos. Es un tema enorme, y te recomiendo Introducing Python de Bill Lubanovic (O'Reilly, 2019) si quieres aprender más.
Programación funcional
Aunque Python admite el paradigma de la programación funcional, no es habitual escribir Python con un estilo puramente FP. Muchos ingenieros de software opinan que otros lenguajes son más adecuados para la PF, como Scala. Sin embargo, merece mucho la pena conocer las útiles funciones de PF disponibles en Python, de las que hablaré a continuación.
La programación funcional, como su nombre indica, consiste en funciones que no cambian. Estas funciones no deben cambiar ningún dato que exista fuera de la función ni modificar ninguna variable global. Para utilizar la terminología correcta, las funciones son inmutables, "puras" y libres de efectos secundarios. No afectan a nada que no se refleje en lo que devuelve la función. Por ejemplo, si tienes una función que añade un elemento a una lista, esa función debe devolver una nueva copia de la lista en lugar de modificar la lista existente. En FP estricto, un programa consiste sólo en evaluar funciones. Éstas pueden estar anidadas (donde una función se define dentro de otra) o las funciones pueden pasarse como argumentos a otras funciones.
-
Es fácil de comprobar porque una función siempre devuelve la misma salida para una entrada dada. No se modifica nada fuera de la función.
-
Es fácil de paralelizar porque los datos no se modifican.
-
Obliga a escribir código modular.
-
Puede ser más conciso y eficaz.
Los conceptos comunes de Python en un estilo funcional incluyen las funciones lambda y las funciones incorporadas map
y filter
. Además, los generadores se escriben a menudo en este estilo, y las comprensiones de listas también pueden considerarse una forma de PF. Otras bibliotecas de FP que merece la pena conocer son itertools
y more-itertools
. En la próxima sección examinaré más detenidamente las funciones lambda y map()
.
Funciones lambda y map()
Las funciones lambda son pequeñas funciones anónimas de Python que puedes utilizar para tareas puntuales rápidas. Se denominan "anónimas" porque no se definen como una función Python normal con un nombre.
Una función lambda tiene la sintaxis
lambda
arguments
:
expression
Una función lambda puede tomar tantos argumentos como quieras, pero sólo puede tener una expresión. Las funciones lambda se utilizan frecuentemente con funciones incorporadas como map
y filter
. Éstas toman funciones como argumentos y luego pueden aplicar la función a cada elemento de un iterable (como una lista).
He aquí un ejemplo sencillo. Utilizando los datos del Objetivo 5 de "Definir tus propias clases", puedes convertir una lista de porcentajes de mujeres en cargos gubernamentales en una lista de proporciones de 0 a 1 utilizando la siguiente función:
usa_govt_percentages
=
[
13.33
,
14.02
,
14.02
,
14.25
,
...
]
usa_govt_proportions
=
list
(
map
(
lambda
x
:
x
/
100
,
usa_govt_percentages
))
Aquí pasan muchas cosas en una sola línea. La función lambda en este caso es lambda x: x/100
. En esta función, x
es una variable temporal que no se utiliza fuera de la función. map()
aplica la función lambda a cada elemento de la lista. Y por último, list()
crea una nueva lista basada en el mapa.
Esto da el siguiente resultado:
>>>
(
usa_govt_proportions
)
...
[
0.1333
,
0.1402
,
0.1402
,
0.1425
,
...
]
Ten en cuenta que los datos originales no se modificaron al aplicar esta función. Se ha creado una nueva lista con los datos modificados.
Aplicar funciones a marcos de datos
De forma similar a la función incorporada map()
anterior, también puedes aplicar funciones a los DataFrames. Esto puede ser especialmente útil si quieres crear una nueva columna basada en una columna existente. De nuevo, puedes utilizar una función que tome otra función como entrada. En pandas, esto es apply()
.
He aquí un ejemplo de aplicación de una función lambda a una columna de un Marco de datos:
df
[
"USA_processed"
]
=
df
[
"United States of America"
]
.
apply
(
lambda
x
:
"Mostly male"
if
x
<
50
else
"Mostly female"
)
En este ejemplo, la columna United States of America
son los datos sobre mujeres en cargos gubernamentales que he estado utilizando a lo largo del capítulo. La función lambda toma el porcentaje de mujeres en cargos gubernamentales y devuelve "Mostly male"
si esa cifra es inferior al 50%, o "Mostly female"
si es igual o superior al 50%.
También puedes utilizar df.apply()
con una función con nombre definida en otro lugar. Aquí tienes la misma función que antes, pero como función con nombre:
def
binary_labels
(
women_in_govt
):
if
women_in_govt
<
50
:
return
"Mostly male"
else
:
return
"Mostly female"
Puedes llamar a esta función en cada fila de una columna pasando el nombre de la función como argumento a la función apply
:
df
[
"USA_processed"
]
=
df
[
"United States of America"
]
.
apply
(
binary_labels
)
Ésta es una solución mejor que escribir una función lambda, porque es posible que quieras reutilizar la función en el futuro, y además puedes probarla y depurarla por separado. También puedes incluir una funcionalidad más compleja que en una función lambda.
Advertencia
La función apply
de pandas es más lenta que las funciones vectorizadas incorporadas porque itera por cada fila de un DataFrame. Así que la mejor práctica es utilizar apply
sólo para algo que no esté ya implementado. Las operaciones numéricas sencillas, como obtener el máximo de una lista, o las opciones de cadena sencillas, como sustituir una cadena por otra, ya están disponibles como funciones vectorizadas más rápidas, por lo que deberías utilizar estas funciones incorporadas siempre que sea posible.
¿Qué paradigma debo utilizar?
Para ser honesto, si sólo estás escribiendo un pequeño guión o trabajando en un proyecto corto por tu cuenta, no necesitas adoptar plenamente ninguno de estos paradigmas. Limítate a guiones modulares que funcionen.
Sin embargo, para proyectos de mayor envergadura, es una gran idea pensar en el tipo de problema al que te enfrentas y si uno de estos paradigmas se adapta bien. Podrías optar por la POO si te encuentras pensando en un conjunto de cosas que necesitan que se les haga algo. Puedes convertir tu espacio problemático en instancias que necesitan tener un comportamiento similar pero atributos o datos diferentes. Un punto importante aquí es que debes tener muchas instancias de alguna clase. No merece la pena escribir una nueva clase si sólo tienes una instancia de ella; eso sólo añade una complejidad extra que no necesitas.
Si quieres hacer cosas nuevas con unos datos que permanecen fijos, FP puede ser una buena opción para ti. También merece la pena considerar FP si tienes una gran cantidad de datos y quieres paralelizar las operaciones que realizas con ellos.
Sin embargo, no hay nada correcto o incorrecto. Puedes ir con tu preferencia personal si trabajas solo, o ir con lo que se utiliza predominantemente en tu equipo para mantener las cosas estandarizadas. Es bueno reconocer cuándo se utilizan estos paradigmas, utilizarlos en el código de otras personas y tomar decisiones sobre lo que funcionaría mejor para tu problema concreto.
Puntos clave
La POO y la PF son paradigmas de programación que encontrarás en el código que leas. La POO se ocupa de los objetos, que son estructuras de datos personalizadas, y la PF se ocupa de las funciones que no cambian los datos subyacentes.
En POO, una clase define nuevos objetos, que pueden tener atributos y métodos. Puedes definir tus propias clases para mantener juntos los métodos y datos asociados, y éste es un gran enfoque a utilizar cuando tienes muchas instancias de objetos similares. Puedes utilizar la herencia para evitar repetir código, y puedes utilizar el polimorfismo para mantener tus interfaces estandarizadas.
En FP, lo ideal es que todo esté dentro de la función. Esto es útil cuando tienes datos que no cambian y quieres hacerles muchas cosas, o quieres paralelizar lo que haces con los datos. Las funciones lambda son el ejemplo más utilizado de FP en Python.
Tu elección de paradigma depende del problema en el que estés trabajando, pero te resultará útil tener conciencia de ambos.
Get Ingeniería de Software para Científicos de Datos now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.