Lección 8 Programación Orientada a Objetos
La Estudiaremos la Poo, es un area de especial interés para todo desarrollador
Conferencia 8
- Programación orientada a objetos
- Clases
raise
- Decoradores
- Conexión con trabajos anteriores en este curso
- Métodos de clase
- Métodos estáticos
- Herencia
- Herencia y Excepciones
- Sobrecarga del operador
- Resumiendo
Programación orientada a objetos
Existen diferentes paradigmas de programación. A medida que aprenda otros idiomas, comenzará a reconocer patrones como estos.
Hasta este punto, ha trabajado procedimentalmente paso a paso.
La programación orientada a objetos (POO) es una solución convincente a los problemas relacionados con la programación.
Para comenzar, escriba
code student.py
otouch student.py
en la ventana de terminal y codifique de la siguiente manera:name = input("Name: ") house = input("House: ") print(f"{name} from {house}")
Tenga en cuenta que este programa sigue un paradigma de procedimiento paso a paso: muy parecido a lo que ha visto en partes anteriores de este curso.
Basándonos en nuestro trabajo de semanas anteriores, podemos crear funciones para abstraer partes de este programa.
def main(): name = get_name() house = get_house() print(f"{name} from {house}") def get_name(): return input("Name: ") def get_house(): return input("House: ") if __name__ == "__main__": main()
Observe cómo
get_name
yget_house
abstraiga algunas de las necesidades de nuestramain
función. Además, observe cómo las líneas finales del código anterior le indican al compilador que ejecute lamain
función.Podemos simplificar aún más nuestro programa almacenando al estudiante como archivo
tuple
. Atuple
es una secuencia de valores. A diferencia de alist
, atuple
no se puede modificar. En espíritu, estamos devolviendo dos valores.def main(): name, house = get_student() print(f"{name} from {house}") def get_student(): name = input("Name: ") house = input("House: ") return name, house if __name__ == "__main__": main()
Observe cómo
get_student
regresaname, house
.Empaquetando eso
tuple
, de modo que podamos devolver ambos elementos a una variable llamadastudent
, podemos modificar nuestro código de la siguiente manera.def main(): student = get_student() print(f"{student[0]} from {student[1]}") def get_student(): name = input("Name: ") house = input("House: ") return (name, house) if __name__ == "__main__": main()
Observe que
(name, house)
le dice explícitamente a cualquiera que lea nuestro código que estamos devolviendo dos valores dentro de uno. Además, observe cómo podemos indexartuple
s usandostudent[0]
ostudent[1]
.tuple
Los s son inmutables, lo que significa que no podemos cambiar esos valores. La inmutabilidad es una forma mediante la cual podemos programar de manera defensiva.def main(): student = get_student() if student[0] == "Padma": student[1] = "Ravenclaw" print(f"{student[0]} from {student[1]}") def get_student(): name = input("Name: ") house = input("House: ") return name, house if __name__ == "__main__": main()
Observe que este código produce un error. Dado que
tuple
s son inmutables, no podemos reasignar el valor destudent[1]
.Si quisiéramos brindar flexibilidad a nuestros compañeros programadores, podríamos utilizar lo
list
siguiente.def main(): student = get_student() if student[0] == "Padma": student[1] = "Ravenclaw" print(f"{student[0]} from {student[1]}") def get_student(): name = input("Name: ") house = input("House: ") return [name, house] if __name__ == "__main__": main()
Tenga en cuenta que las listas son mutables. Es decir, el orden de
house
yname
puede ser cambiado por un programador. Puede decidir utilizar esto en algunos casos en los que desee brindar más flexibilidad a costa de la seguridad de su código. Después de todo, si el orden de esos valores se puede cambiar, los programadores que trabajan con usted podrían cometer errores en el futuro.También se podría utilizar un diccionario en esta implementación. Recuerde que los diccionarios proporcionan un par clave-valor.
def main(): student = get_student() print(f"{student['name']} from {student['house']}") def get_student(): student = {} student["name"] = input("Name: ") student["house"] = input("House: ") return student if __name__ == "__main__": main()
Observe que en este caso se devuelven dos pares clave-valor. Una ventaja de este enfoque es que podemos indexar este diccionario usando las claves.
Aún así, nuestro código se puede mejorar aún más. Observe que hay una variable innecesaria. Podemos eliminarlo
student = {}
porque no necesitamos crear un diccionario vacío.def main(): student = get_student() print(f"{student['name']} from {student['house']}") def get_student(): name = input("Name: ") house = input("House: ") return {"name": name, "house": house} if __name__ == "__main__": main()
Observe que podemos utilizar
{}
llaves en lareturn
declaración para crear el diccionario y devolverlo todo en la misma línea.Podemos proporcionar nuestro caso especial con Padma en nuestra versión de diccionario de nuestro código.
def main(): student = get_student() if student["name"] == "Padma": student["house"] = "Ravenclaw" print(f"{student['name']} from {student['house']}") def get_student(): name = input("Name: ") house = input("House: ") return {"name": name, "house": house} if __name__ == "__main__": main()
Observe cómo, de manera similar en espíritu a nuestras iteraciones anteriores de este código, podemos utilizar los nombres clave para indexar en nuestro diccionario de estudiantes.
Clases
Las clases son una forma mediante la cual, en la programación orientada a objetos, podemos crear nuestro propio tipo de datos y darles nombres.
Una clase es como un molde para un tipo de datos: donde podemos inventar nuestro propio tipo de datos y darles un nombre.
Podemos modificar nuestro código de la siguiente manera para implementar nuestra propia clase llamada
Student
:class Student: ... def main(): student = get_student() print(f"{student.name} from {student.house}") def get_student(): student = Student() student.name = input("Name: ") student.house = input("House: ") return student if __name__ == "__main__": main()
Observe por convención que
Student
está en mayúscula. Además, observe que...
simplemente significa que luego regresaremos para terminar esa parte de nuestro código. Además, observe que enget_student
, podemos crear unastudent
claseStudent
usando la sintaxisstudent = Student()
. Además, observe que utilizamos "notación de puntos" para acceder a los atributos de esta variablestudent
de claseStudent
.Cada vez que creas una clase y utilizas ese modelo para crear algo, creas lo que se llama un "objeto" o una "instancia". En el caso de nuestro código,
student
es un objeto.Además, podemos sentar algunas bases para los atributos que se esperan dentro de un objeto cuya clase es
Student
. Podemos modificar nuestro código de la siguiente manera:class Student: def __init__(self, name, house): self.name = name self.house = house def main(): student = get_student() print(f"{student.name} from {student.house}") def get_student(): name = input("Name: ") house = input("House: ") student = Student(name, house) return student if __name__ == "__main__": main()
Observe que dentro de
Student
, estandarizamos los atributos de esta clase. Podemos crear una función dentro declass Student
, llamada “método”, que determine el comportamiento de un objeto de claseStudent
. Dentro de esta función, tomaname
yhouse
le pasa y asigna estas variables a este objeto. Además, observe cómo el constructorstudent = Student(name, house)
llama a esta función dentro de laStudent
clase y crea un archivostudent
.self
se refiere al objeto actual que se acaba de crear.Podemos simplificar nuestro código de la siguiente manera:
class Student: def __init__(self, name, house): self.name = name self.house = house def main(): student = get_student() print(f"{student.name} from {student.house}") def get_student(): name = input("Name: ") house = input("House: ") return Student(name, house) if __name__ == "__main__": main()
Observe cómo
return Student(name, house)
se simplifica la iteración anterior de nuestro código donde la declaración del constructor se ejecutaba en su propia línea.Puede obtener más información en la documentación de clases de Python .
raise
El programa orientado a objetos le anima a encapsular toda la funcionalidad de una clase dentro de la definición de clase. ¿Qué pasa si algo sale mal? ¿Qué pasa si alguien intenta escribir algo al azar? ¿Qué pasa si alguien intenta crear un estudiante sin nombre? Modifique su código de la siguiente manera:
class Student: def __init__(self, name, house): if not name: raise ValueError("Missing name") if house not in ["Gryffindor", "Hufflepuff", "Ravenclaw", "Slytherin"]: raise ValueError("Invalid house") self.name = name self.house = house def main(): student = get_student() print(f"{student.name} from {student.house}") def get_student(): name = input("Name: ") house = input("House: ") return Student(name, house) if __name__ == "__main__": main()
Observe cómo verificamos ahora que se proporcione un nombre y se designe una casa adecuada. Resulta que podemos crear nuestras propias excepciones que alertan al programador sobre un posible error creado por el usuario llamado
raise
. En el caso anterior, nos planteaValueError
un mensaje de error específico.Da la casualidad de que Python te permite crear una función específica mediante la cual puedes imprimir los atributos de un objeto. Modifique su código de la siguiente manera:
class Student: def __init__(self, name, house, patronus): if not name: raise ValueError("Missing name") if house not in ["Gryffindor", "Hufflepuff", "Ravenclaw", "Slytherin"]: raise ValueError("Invalid house") self.name = name self.house = house self.patronus = patronus def __str__(self): return f"{self.name} from {self.house}" def main(): student = get_student() print(student) def get_student(): name = input("Name: ") house = input("House: ") patronus = input("Patronus: ") return Student(name, house, patronus) if __name__ == "__main__": main()
Observe cómo
def __str__(self)
proporciona un medio por el cual un estudiante regresa cuando lo llaman. Por lo tanto, ahora puedes, como programador, imprimir un objeto, sus atributos o casi cualquier cosa que desees relacionada con ese objeto.__str__
es un método integrado que viene con las clases de Python. ¡Da la casualidad de que también podemos crear nuestros propios métodos para una clase! Modifique su código de la siguiente manera:class Student: def __init__(self, name, house, patronus=None): if not name: raise ValueError("Missing name") if house not in ["Gryffindor", "Hufflepuff", "Ravenclaw", "Slytherin"]: raise ValueError("Invalid house") if patronus and patronus not in ["Stag", "Otter", "Jack Russell terrier"]: raise ValueError("Invalid patronus") self.name = name self.house = house self.patronus = patronus def __str__(self): return f"{self.name} from {self.house}" def charm(self): match self.patronus: case "Stag": return "🐴" case "Otter": return "🦦" case "Jack Russell terrier": return "🐶" case _: return "🪄" def main(): student = get_student() print("Expecto Patronum!") print(student.charm()) def get_student(): name = input("Name: ") house = input("House: ") patronus = input("Patronus: ") or None return Student(name, house, patronus) if __name__ == "__main__": main()
Observe cómo definimos nuestro propio método
charm
. A diferencia de los diccionarios, las clases pueden tener funciones integradas llamadas métodos. En este caso, definimos nuestrocharm
método donde casos específicos tienen resultados específicos. Además, observe que Python tiene la capacidad de utilizar emojis directamente en nuestro código.Antes de seguir adelante, eliminemos nuestro código patronus. Modifique su código de la siguiente manera:
class Student: def __init__(self, name, house): if not name: raise ValueError("Invalid name") if house not in ["Gryffindor", "Hufflepuff", "Ravenclaw", "Slytherin"]: raise ValueError("Invalid house") self.name = name self.house = house def __str__(self): return f"{self.name} from {self.house}" def main(): student = get_student() student.house = "Number Four, Privet Drive" print(student) def get_student(): name = input("Name: ") house = input("House: ") return Student(name, house) if __name__ == "__main__": main()
Observe que solo tenemos dos métodos:
__init__
y__str__
.
Decoradores
Las propiedades se pueden utilizar para reforzar nuestro código. En Python, definimos propiedades usando funciones "decoradores", que comienzan con
@
. Modifique su código de la siguiente manera:class Student: def __init__(self, name, house): if not name: raise ValueError("Invalid name") self.name = name self.house = house def __str__(self): return f"{self.name} from {self.house}" # Getter for house @property def house(self): return self._house # Setter for house @house.setter def house(self, house): if house not in ["Gryffindor", "Hufflepuff", "Ravenclaw", "Slytherin"]: raise ValueError("Invalid house") self._house = house def main(): student = get_student() print(student) def get_student(): name = input("Name: ") house = input("House: ") return Student(name, house) if __name__ == "__main__": main()
Observe cómo hemos escrito
@property
arriba una función llamadahouse
. Hacerlo se definehouse
como una propiedad de nuestra clase. Conhouse
una propiedad, obtenemos la capacidad de definir cómo_house
se debe establecer y recuperar algún atributo de nuestra clase. De hecho, ahora podemos definir una función llamada "establecedora", a través de@house.setter
, que se llamará cada vez que se establezca la propiedad de la casa, por ejemplo, constudent.house = "Gryffindor"
. Aquí, hemos hecho que nuestro configurador valide los valores dehouse
por nosotros. Observe cómo aumentamos aValueError
si el valor dehouse
no es ninguna de las casas de Harry Potter; de lo contrario, usaremoshouse
para actualizar el valor de_house
. ¿Por qué_house
y nohouse
?house
es una propiedad de nuestra clase, con funciones mediante las cuales un usuario intenta establecer nuestro atributo de clase._house
es ese atributo de clase en sí. El guión bajo inicial,_
, indica a los usuarios que no necesitan (¡y de hecho, no deberían!) modificar este valor directamente. sólo_house
debe configurarse a través del setter. Observe cómo la propiedad simplemente devuelve ese valor de , nuestro atributo de clase que presumiblemente ha sido validado usando nuestro definidor. Cuando un usuario llama , obtiene el valor de a través de nuestro "captador".house``house``_house``house``student.house``_house``house
Además del nombre de la casa, también podemos proteger el nombre de nuestro estudiante. Modifique su código de la siguiente manera:
class Student: def __init__(self, name, house): self.name = name self.house = house def __str__(self): return f"{self.name} from {self.house}" # Getter for name @property def name(self): return self._name # Setter for name @name.setter def name(self, name): if not name: raise ValueError("Invalid name") self._name = name @property def house(self): return self._house @house.setter def house(self, house): if house not in ["Gryffindor", "Hufflepuff", "Ravenclaw", "Slytherin"]: raise ValueError("Invalid house") self._house = house def main(): student = get_student() print(student) def get_student(): name = input("Name: ") house = input("House: ") return Student(name, house) if __name__ == "__main__": main()
Observe cómo, al igual que el código anterior, proporcionamos un captador y un definidor para el nombre.
Puede obtener más información en la documentación de métodos de Python .
Conexión con trabajos anteriores en este curso
Si bien no se indicó explícitamente en partes anteriores de este curso, usted ha estado utilizando clases y objetos durante todo el proceso.
Si profundizas en la documentación de
int
, verás que es una clase con un constructor. Es un modelo para crear objetos de tipoint
. Puede obtener más información en la documentación de Python deint
.Las cuerdas también son una clase. Si usó
str.lower()
, estaba usando un método que venía dentro de lastr
clase. Puede obtener más información en la documentación de Python destr
.list
También es una clase. Al mirar esa documentaciónlist
, puede ver los métodos que contiene, comolist.append()
. Puede obtener más información en la documentación de Python delist
.dict
También es una clase dentro de Python. Puede obtener más información en la documentación de Python dedict
.Para ver cómo has estado usando las clases todo el tiempo, ve a tu consola y escribe
code type.py
y luego codifica de la siguiente manera:print(type(50))
Observe cómo al ejecutar este código, se mostrará que la clase de
50
esint
.También podemos aplicar esto a
str
lo siguiente:print(type("hello, world"))
Observe cómo la ejecución de este código indicará que es de la clase
str
.También podemos aplicar esto a
list
lo siguiente:print(type([]))
Observe cómo la ejecución de este código indicará que es de la clase
list
.También podemos aplicar esto
list
usando el nombre de la clase incorporada de Pythonlist
de la siguiente manera:print(type(list()))
Observe cómo la ejecución de este código indicará que es de la clase
list
.También podemos aplicar esto a
dict
lo siguiente:print(type({}))
Observe cómo la ejecución de este código indicará que es de la clase
dict
.También podemos aplicar esto
dict
usando el nombre de la clase integrada de Pythondict
de la siguiente manera:print(type(dict()))
Observe cómo la ejecución de este código indicará que es de la clase
dict
.
Métodos de clase
A veces queremos agregar funcionalidad a una clase misma, no a instancias de esa clase.
@classmethod
es una función que podemos usar para agregar funcionalidad a una clase en su conjunto.A continuación se muestra un ejemplo de cómo no utilizar un método de clase. En la ventana de su terminal, escriba
code hat.py
y codifique lo siguiente:import random class Hat: def __init__(self): self.houses = ["Gryffindor", "Hufflepuff", "Ravenclaw", "Slytherin"] def sort(self, name): print(name, "is in", random.choice(self.houses)) hat = Hat() hat.sort("Harry")
Observe cómo cuando pasamos el nombre del estudiante al sombrero seleccionador, este nos dirá qué casa está asignada al estudiante. Observe que
hat = Hat()
crea una instancia dehat
. Lasort
funcionalidad siempre es manejada por la instancia de la claseHat
. Al ejecutarhat.sort("Harry")
, pasamos el nombre del estudiante alsort
método de la instancia particular deHat
, que hemos llamadohat
.Sin embargo, es posible que queramos ejecutar la
sort
función sin crear una instancia particular del sombrero seleccionador (¡después de todo, solo hay uno!). Podemos modificar nuestro código de la siguiente manera:import random class Hat: houses = ["Gryffindor", "Hufflepuff", "Ravenclaw", "Slytherin"] @classmethod def sort(cls, name): print(name, "is in", random.choice(cls.houses)) Hat.sort("Harry")
Observe cómo se elimina el
__init__
método porque no necesitamos crear una instancia de un sombrero en ninguna parte de nuestro código.self
, por lo tanto, ya no es relevante y se elimina. Especificamos estosort
como@classmethod
, reemplazándoloself
porcls
. Finalmente, observe cómoHat
se escribe en mayúscula por convención cerca del final de este código, porque este es el nombre de nuestra clase.Volviendo a,
students.py
podemos modificar nuestro código de la siguiente manera, abordando algunas oportunidades perdidas relacionadas con@classmethod
s:class Student: def __init__(self, name, house): self.name = name self.house = house def __str__(self): return f"{self.name} from {self.house}" @classmethod def get(cls): name = input("Name: ") house = input("House: ") return cls(name, house) def main(): student = Student.get() print(student) if __name__ == "__main__": main()
Observe que
get_student
se elimina y se crea un@classmethod
llamado .get
Ahora se puede llamar a este método sin tener que crear un estudiante primero.
Métodos estáticos
- Resulta que además de
@classmethod
s, que son distintos de los métodos de instancia, también existen otros tipos de métodos. - Usar
@staticmethod
puede ser algo que quizás desees explorar. Si bien no se trata explícitamente en este curso, puede ir y aprender más sobre los métodos estáticos y su distinción de los métodos de clase.
Herencia
La herencia es quizás la característica más poderosa de la programación orientada a objetos.
Da la casualidad de que puedes crear una clase que "hereda" métodos, variables y atributos de otra clase.
En la terminal ejecuta
code wizard.py
. Codifique de la siguiente manera:class Wizard: def __init__(self, name): if not name: raise ValueError("Missing name") self.name = name ... class Student(Wizard): def __init__(self, name, house): super().__init__(name) self.house = house ... class Professor(Wizard): def __init__(self, name, subject): super().__init__(name) self.subject = subject ... wizard = Wizard("Albus") student = Student("Harry", "Gryffindor") professor = Professor("Severus", "Defense Against the Dark Arts") ...
Observe que hay una clase arriba llamada
Wizard
y una clase llamadaStudent
. Además, observe que hay una clase llamadaProfessor
. Tanto los estudiantes como los profesores tienen nombres. Además, tanto los estudiantes como los profesores son magos. Por lo tanto, ambosStudent
yProfessor
heredan las características deWizard
. Dentro de la clase “secundaria”Student
,Student
puede heredar de la clase “padre” o “super”Wizard
a medida que la líneasuper().__init__(name)
ejecuta elinit
método deWizard
. Finalmente, observe que las últimas líneas de este código crean un mago llamado Albus, un estudiante llamado Harry, y así sucesivamente.
Herencia y Excepciones
Si bien acabamos de introducir la herencia, la hemos estado usando todo el tiempo durante el uso de excepciones.
Da la casualidad de que las excepciones se dan en una jerarquía, donde hay clases de niños, padres y abuelos. Estos se ilustran a continuación:
BaseException +-- KeyboardInterrupt +-- Exception +-- ArithmeticError | +-- ZeroDivisionError +-- AssertionError +-- AttributeError +-- EOFError +-- ImportError | +-- ModuleNotFoundError +-- LookupError | +-- KeyError +-- NameError +-- SyntaxError | +-- IndentationError +-- ValueError ...
Puede obtener más información en la documentación de excepciones de Python .
Sobrecarga del operador
Algunos operadores como
+
y-
pueden "sobrecargarse" de modo que puedan tener más habilidades más allá de la simple aritmética.En la ventana de su terminal, escriba
code vault.py
. Luego, codifique de la siguiente manera:class Vault: def __init__(self, galleons=0, sickles=0, knuts=0): self.galleons = galleons self.sickles = sickles self.knuts = knuts def __str__(self): return f"{self.galleons} Galleons, {self.sickles} Sickles, {self.knuts} Knuts" def __add__(self, other): galleons = self.galleons + other.galleons sickles = self.sickles + other.sickles knuts = self.knuts + other.knuts return Vault(galleons, sickles, knuts) potter = Vault(100, 50, 25) print(potter) weasley = Vault(25, 50, 100) print(weasley) total = potter + weasley print(total)
Observe cómo el
__str__
método devuelve una cadena formateada. Además, observe cómo el__add__
método permite la suma de los valores de dos bóvedas.self
es lo que está a la izquierda del+
operando.other
es lo que es correcto del+
.Puede obtener más información en la documentación de Python sobre sobrecarga de operadores .
Resumiendo
Ahora ha aprendido un nivel completamente nuevo de capacidad a través de la programación orientada a objetos.
- Programación orientada a objetos
- Clases
raise
- Métodos de clase
- Métodos estáticos
- Herencia
- Sobrecarga del operador