Saltar al contenido principal
LibreTexts Español

2.11: Objetos y Clases

  • Page ID
    55091
  • \( \newcommand{\vecs}[1]{\overset { \scriptstyle \rightharpoonup} {\mathbf{#1}} } \) \( \newcommand{\vecd}[1]{\overset{-\!-\!\rightharpoonup}{\vphantom{a}\smash {#1}}} \)\(\newcommand{\id}{\mathrm{id}}\) \( \newcommand{\Span}{\mathrm{span}}\) \( \newcommand{\kernel}{\mathrm{null}\,}\) \( \newcommand{\range}{\mathrm{range}\,}\) \( \newcommand{\RealPart}{\mathrm{Re}}\) \( \newcommand{\ImaginaryPart}{\mathrm{Im}}\) \( \newcommand{\Argument}{\mathrm{Arg}}\) \( \newcommand{\norm}[1]{\| #1 \|}\) \( \newcommand{\inner}[2]{\langle #1, #2 \rangle}\) \( \newcommand{\Span}{\mathrm{span}}\) \(\newcommand{\id}{\mathrm{id}}\) \( \newcommand{\Span}{\mathrm{span}}\) \( \newcommand{\kernel}{\mathrm{null}\,}\) \( \newcommand{\range}{\mathrm{range}\,}\) \( \newcommand{\RealPart}{\mathrm{Re}}\) \( \newcommand{\ImaginaryPart}{\mathrm{Im}}\) \( \newcommand{\Argument}{\mathrm{Arg}}\) \( \newcommand{\norm}[1]{\| #1 \|}\) \( \newcommand{\inner}[2]{\langle #1, #2 \rangle}\) \( \newcommand{\Span}{\mathrm{span}}\)\(\newcommand{\AA}{\unicode[.8,0]{x212B}}\)

    Este libro, al presentar inicialmente Python, mencionó algunas de las características del lenguaje, como su énfasis en “una mejor manera” y código legible. Python también proporciona bastante funcionalidad incorporada a través de módulos importables como sys, re y subprocess.

    Python proporciona otra ventaja: está naturalmente “orientada a objetos”, aunque aún no hayamos discutido este punto. Aunque existen paradigmas en competencia para la mejor manera de diseñar programas, el paradigma orientado a objetos se usa comúnmente para la ingeniería de software y la gestión de grandes proyectos. [1] Sin embargo, incluso para programas pequeños, los conceptos básicos de orientación a objetos pueden facilitar la tarea de programación.

    Un objeto, prácticamente hablando, es un segmento de memoria (RAM) que hace referencia tanto a datos (referidos por variables de instancia) de diversos tipos como a funciones asociadas que puede operar con los datos. [2] Las funciones que pertenecen a objetos se denominan métodos. Dicho de otra manera, un método es una función que está asociada a un objeto, y una variable de instancia es una variable que pertenece a un objeto.

    En Python, los objetos son los datos que hemos estado asociando con variables. Cuáles son los métodos, cómo funcionan y cuáles son los datos (por ejemplo, una lista de números, diccionario de cadenas, etc.) son definidos por una clase: la colección de código que sirve como “blueprint” para objetos de ese tipo y cómo funcionan.

    II.11_1_object_ilustración

    Así, la clase (al igual que el blueprint para una casa) define la estructura de los objetos, pero las variables de instancia de cada objeto pueden referirse a diferentes elementos de datos siempre que se ajusten a la estructura definida (al igual que cómo pueden vivir diferentes familias en casas construidas a partir del mismo blueprint). En Python, cada dato que encontramos de forma rutinaria constituye un objeto. Cada tipo de datos que hemos tratado hasta ahora (listas, cadenas, diccionarios, etc.) tiene una definición de clase, un plano, que lo define. Por ejemplo, las listas tienen datos (números, cadenas o cualquier otro tipo) y métodos como .sort () y .append ().

    alt

    En cierto sentido, llamar a métodos de objeto hace una solicitud del objeto: nums_list.sort () podría interpretarse como “objeto al que hace referencia nums_list, por favor ejecute su método sort ()”. Al recibir este mensaje, el objeto reordenará sus datos. [3]

    Creación de nuevas clases

    Las definiciones para las clases de Python son solo bloques de código, indicados por un nivel adicional de sangría (como bloques de función, bloques de instrucción if y bloques de bucle). Cada definición de clase requiere tres cosas, dos de las cuales ya estamos familiarizados con:

    1. Métodos (funciones) que pertenecen a objetos de la clase.
    2. Variables de instancia referidas a datos.
    3. Un método especial llamado constructor. Este método se llamará automáticamente cada vez que se cree un nuevo objeto de la clase, y debe tener el nombre __init__.

    Una peculiaridad de Python es que cada método de un objeto debe tomar como primer argumento un parámetro llamado self, [4] que usamos para acceder a las variables de instancia. Comencemos definiendo una clase, Gene (los nombres de clase tradicionalmente comienzan con una letra mayúscula): cada objeto Gene tendrá (1) un id (cadena) y (2) una secuencia (también una cadena). Al crear un objeto Gene, debemos definir su id y secuencia pasándolos como parámetros al método __init__.

    Fuera del bloque que define la clase, podemos hacer uso de ella para crear e interactuar con objetos Gene.

    II.11_3_PY_104_GENE_CLASS_1

    (Normalmente no incluimos llamadas print () en el constructor; lo estamos haciendo aquí solo para aclarar el proceso de creación del objeto.) Ejecutándose lo anterior:

    II.11_4_py_104_w_gene_class_1_out

    Tenga en cuenta que aunque cada método (incluido el constructor) toma como su primer parámetro self, no especificamos este parámetro al llamar a métodos para los objetos. (Por ejemplo, .print_id () toma un parámetro self que no especificamos al llamarlo.) Es bastante común olvidar incluir este autoparámetro “implícito”; si lo haces, obtendrás un error como TypeError: print_id () no toma argumentos (1 dado), porque el número de parámetros tomados por el método no coincide con el número dado cuando se llama. Además, cualquier parámetro enviado a la función de creación (Gene (“AY342", “CATTGAC”)) se pasa al constructor (__init__ (self, creationid, creationseq)).

    ¿Qué es el auto? El parámetro self es una variable que se le da al método para que el objeto pueda referirse a “sí mismo”. Al igual que otras personas podrían referirse a ti por tu nombre, podrías referirte a ti mismo como “yo”, como en “yo: recuerda volver a enviar ese manuscrito mañana”.

    Curiosamente, en cierto sentido, los métodos definidos para las clases están rompiendo la primera regla de funciones: ¡están accediendo a variables que no se pasan como parámetros! Esto en realidad está bien. Todo el punto de los objetos es que contienen funciones y datos a los que siempre se puede suponer que las funciones tienen acceso directo.

    Continuemos nuestro ejemplo agregando un método que calcula el contenido GC de la variable de instancia self.sequence. Este método necesita ser incluido en el bloque que define la clase; observe que un método que pertenece a un objeto puede llamar a otro método que pertenece a sí mismo, por lo que podemos calcular el contenido de GC como un par de métodos, al igual que lo hicimos con funciones simples:

    II.11_5_PY_105_GENE_CLASS_GC

    Dando como resultado la salida:

    II.11_6_PY_105_2_GENE_CLASS_GC_Out

    También puede ser útil escribir métodos que nos permitan obtener y establecer las variables de instancia de un objeto. Podríamos agregar a nuestros métodos de definición de clase para obtener y establecer la secuencia, por ejemplo, haciendo que los métodos se refieran a la variable de instancia self.seq.

    II.11_7_py_106_gene_class_getter_setter

    Podríamos hacer uso de esta funcionalidad añadida más adelante en nuestro código con una línea como print (“la secuencia del gen A es" + genea.get_seq ()) o genea.set_seq (“ACTAGGGG”).

    Aunque los métodos pueden devolver valores (como con .base_composition () y .gc_content ()) y realizar alguna acción que modifique el objeto (como con .set_seq ()), el principio de separación comando-consulta establece que no deben hacer ambas cosas a menos que sea absolutamente necesario.

    ¿Es posible que modifiquemos las variables de instancia de un objeto directamente? Tiene sentido que podamos; porque el nombre del objeto génico por sí mismo es self y establece su secuencia vía self.sequence, deberíamos poder establecer la secuencia del objeto génico usando nuestro nombre para ello, GeneA. De hecho, Genea.Sequence = “ACTAGGGG” tendría el mismo resultado que llamar a Genea.set_seq (“ACTAGGGG”), como se definió anteriormente.

    Entonces, ¿por qué podríamos querer usar los métodos “getter” y “setter” en lugar de modificar o leer directamente las variables de instancia de un objeto? La diferencia está un poco relacionada con la cortesía, si no con el objeto en sí, entonces con quien escribió el código para la clase. Mediante el uso de métodos, estamos solicitando que el objeto cambie sus datos de secuencia, mientras que establecer directamente las variables de instancia solo llega y lo cambia, ¡lo cual es un poco como realizar una cirugía a corazón abierto sin el permiso del paciente!

    Esta es una distinción sutil, pero es considerado un negocio serio para muchos programadores. Para ver por qué, supongamos que hay muchos métodos que no funcionarán en absoluto en las secuencias de ARN, por lo que debemos asegurarnos de que la variable de instancia de secuencia nunca tenga ningún carácter U en ella. En este caso, podríamos hacer que el método .set_seq () decida si acepta o no la secuencia:

    II.11_8_py_107_gene_class_getter_setter_check

    Python tiene una declaración assert para este tipo de comprobación de errores. Al igual que una función, toma dos parámetros, pero a diferencia de una función, no se permiten paréntesis.

    II.11_9_py_108_gene_class_getter_setter_assert

    Al usar un assert, si la comprobación no se evalúa como True, entonces el programa se detendrá y reportará el error especificado. El código completo para este ejemplo se puede encontrar en el archivo gene_class.py.

    Usar métodos cuando se trabaja con objetos se trata de encapsulación y dejar que los objetos hagan el mayor trabajo posible. De esa manera, pueden asegurar resultados correctos para que tú (o con quien estés compartiendo código, que podría ser “tú futuro”) no tengas que hacerlo. Los objetos pueden tener cualquier número de variables de instancia, y los métodos pueden acceder y modificarlas, pero es una buena idea asegurarse de que todas las variables de instancia se dejen en un estado coherente para un objeto dado. Por ejemplo, si un objeto Gene tiene una variable de instancia para la secuencia, y otro que contiene su contenido de GC, entonces el contenido de GC debe actualizarse siempre que la secuencia sea. Aún mejor es computar tales cantidades según sea necesario, como hicimos anteriormente. [5]

    Los pasos para escribir una definición de clase son los siguientes:

    1. Decidir qué concepto o entidad representarán los objetos de esa clase, así como qué datos (variables de instancia) y métodos (funciones) tendrán.
    2. Crea un método constructor y haz que inicialice todas las variables de instancia, ya sea con parámetros pasados al constructor, o como vacío (por ejemplo, algo como self.id = “” o self.go_terms = list ()). Aunque las variables de instancia pueden ser creadas por cualquier método, tenerlas todas inicializadas en el constructor proporciona una referencia visual rápida para referirse a la hora de codificar.
    3. Escriba métodos que establezcan u obtengan las variables de instancia, calcule cálculos, llame a otros métodos o funciones, y así sucesivamente. ¡No olvides el parámetro de auto!

    Ejercicios

    1. Crear un programa objects_test.py que defina y utilice una clase. La clase puede ser lo que quieras, pero debe tener al menos dos métodos (distintos del constructor) y al menos dos variables de instancia. Uno de los métodos debe ser una “acción” que se le puede pedir a un objeto de esa clase que realice, y el otro debe devolver una “respuesta”.

      Crea una instancia de tu clase en al menos dos objetos y prueba tus métodos en ellos.

    2. Una vez definidas, las clases (y los objetos de esos tipos) se pueden usar en cualquier lugar, incluyendo otras definiciones de clase. Escribe dos definiciones de clase, una de las cuales contiene múltiples instancias de la otra. Por ejemplo, las variables de instancia en un objeto House podrían referirse a varios objetos Room diferentes. (Para un ejemplo más biológicamente inspirado, un objeto Gene podría tener un self.exons que contiene una lista de objetos Exon.)

      El siguiente ejemplo ilustra esto más a fondo, pero tener algo de práctica primero será beneficioso.

    3. Si las clases implementan algunos métodos especiales, entonces podemos comparar objetos de esos tipos con ==, <, y los otros operadores de comparación.

      Al comparar dos objetos Gene, por ejemplo, podríamos decir que son iguales si sus secuencias son iguales, y GeneA es menor que GeneB si Genea.Seq < GeneB.seq. Así podemos agregar un método especial __eq__ (), el cual, dado el yo habitual y una referencia a otro objeto del mismo tipo llamado otro, devuelve True si consideraríamos los dos iguales y False de otra manera: TambiénII.11_10_py_107_2_comparitor_métodos_1 podemos implementar un __lt__ () método para “menos que”:II.11_11_py_107_3_comparitor_métodos_2 Con estos, Python puede averiguar cómo comparar los objetos Gene con < y ==. Las otras comparaciones se pueden habilitar definiendo __le__ () (for <=), __gt__ () (for >), __ge__ () (for >=) y __ne__ () (for! =).

      imagenFinalmente, si tenemos una lista de objetos Gene genes_list que definen estos comparadores, entonces Python puede ordenar de acuerdo a nuestros criterios de comparación con genes_list.sort () y ordenado (genes_list) .

      Explore estos conceptos definiendo su propio tipo de datos ordenados, implementando __eq__ (), __lt__ () y los otros métodos de comparación. Compara dos objetos de esos tipos con los operadores de comparación estándar y ordena una lista de ellos. También puede intentar implementar un método __repr__ (), que debería devolver una cadena que represente el objeto, habilitando print () (como en print (GeneA)).

    Contando SNPs

    Resulta que se pueden definir múltiples clases que interactúan entre sí: las variables de instancia de una clase personalizada pueden referirse a tipos de objetos personalizados. Considere el archivo trio.subset.vcf, un archivo VCF (formato de llamada variante) para describir polimorfismos de un solo nucleótido (SNP, pronunciado “snips”) a través de individuos en un grupo o población. En este caso, el archivo representa un muestreo aleatorio de SNP de tres personas, una madre, un padre y su hija, en comparación con el genoma humano de referencia. [6]

    II.11_13_PY_108_2_TRIO_Archivo

    Este archivo contiene una variedad de información, incluyendo líneas de encabezado que comienzan con # que describen parte de la codificación que se encuentra en el archivo. Las columnas 1, 2, 3, 4 y 5 representan el número de cromosomas del SNP, la posición del SNP en el cromosoma, la ID del SNP (si se ha descrito previamente en poblaciones humanas), la base presente en la referencia en esa posición y una base alternativa encontrada en uno de los tres miembros de la familia, respectivamente. Otras columnas describen diversa información; este archivo sigue el formato “VCF 4.0”, que se describe con más detalle en http://www.1000genomes.org/node/101. Algunas columnas contienen un. , que indica que la información no está presente; en el caso de la columna ID, éstas representan nuevos polimorfismos identificados en este trío.

    Para este ejemplo, nos interesan las cinco primeras columnas, y las preguntas principales son:

    • ¿Cuántas transiciones (A vs. G o C vs. T) hay dentro de los datos para cada cromosoma?
    • ¿Cuántas transversiones (cualquier otra cosa) hay dentro de los datos para cada cromosoma?

    Es posible que en el futuro tengamos otras preguntas sobre transiciones y transversiones por cromosoma. Para responder a las preguntas anteriores, y para prepararnos para las futuras, comenzaremos definiendo algunas clases para representar a estas diversas entidades. Este ejemplo demostrará ser un poco más largo que otros que hemos estudiado, en parte porque nos permite ilustrar respondiendo múltiples preguntas usando la misma base de código si hacemos algún trabajo extra por adelantado, pero también porque los diseños orientados a objetos tienden a resultar en significativamente más código (una crítica común de usar clases y objetos).

    Clase SNP

    Un objeto SNP contendrá información relevante sobre una sola línea sin encabezado en el archivo VCF. Las variables de instancia incluirían el alelo de referencia (una cadena de un carácter, por ejemplo, “A”), el alelo alternativo (una cadena de un carácter, por ejemplo, “G”), el nombre del cromosoma en el que existe (una cadena, por ejemplo, “1"), la posición de referencia (un número entero, por ejemplo, 799739), y el ID del SNP (por ejemplo, “rs57181708" o “.” ). Debido a que vamos a analizar líneas una a la vez, toda esta información se puede proporcionar en el constructor.

    Los objetos SNP deberían poder responder preguntas: .is_transition () debería devolver True si el SNP es una transición y False si no, mirando las dos variables de instancia de alelo. Del mismo modo, .is_transversion () debería devolver True si el SNP es una transversión y False en caso contrario.

    Clase Cromosómica

    Un objeto cromosómico contendrá datos para un cromosoma individual, incluido el nombre del cromosoma (una cadena, por ejemplo, “1") y todos los objetos SNP que se encuentran en ese cromosoma. Podríamos almacenar los objetos SNP en una lista, pero también podríamos considerar almacenarlos en un diccionario, que asigna ubicaciones SNP (enteros) a los objetos SNP. Entonces no solo podemos obtener acceso a la lista de SNPs (usando el método .values () del diccionario) o la lista de ubicaciones (usando el método .keys () del diccionario), sino que también, dada cualquier ubicación, podemos obtener acceso al SNP en esa ubicación. (Incluso podemos usar .has_key () para determinar si existe un SNP en una ubicación determinada.)

    alt

    El constructor de cromosomas inicializará el nombre del cromosoma como self.chrname, pero el diccionario snps comenzará como vacío.

    Un objeto Cromosoma también debería ser capaz de responder preguntas: .count_transition () debería decirnos el número de SNP de transición, y .count_transversion () debería devolver el número de SNP de transversión. También vamos a necesitar alguna manera de agregar un objeto SNP al diccionario SNP de un cromosoma porque comienza vacío. Lo lograremos con un método.add_snp (), que tomará toda la información de un SNP, creará el nuevo objeto SNP y lo agregará al diccionario. Si ya existe un SNP en esa ubicación, debería ocurrir un error, porque nuestro programa no debería aceptar archivos VCF que tengan múltiples filas con la misma posición para el mismo cromosoma.

    Para la estrategia general, una vez que tengamos nuestras clases definidas (y depuradas), la parte “ejecutable” de nuestro programa será bastante simple: necesitaremos mantener una colección de objetos Cromosoma con los que podamos interactuar para agregar SNP, y al final simplemente recorreremos estos y preguntaremos a cada uno cuántos transiciones y transversiones que tiene. También tiene sentido mantener estos objetos cromosómicos en un diccionario, siendo las claves los nombres de los cromosomas (cadenas) y los valores son los objetos Cromosómicos. Llamaremos a este diccionario chrnames_to_chrs.

    II.11_15_Chrnames_to_chrs_fig

    A medida que recorremos cada línea de entrada (leyendo un nombre de archivo dado en sys.argv [1]), lo dividiremos y verificaremos si el nombre del cromosoma está en el diccionario con .has_key (). Si es así, le pediremos al objeto en esa ranura que agregue un SNP con .add_snp (). Si no, entonces primero tendremos que crear un nuevo objeto Cromosoma, pedirle a .add_snp (), y finalmente agregarlo al diccionario. Por supuesto, todo esto debería suceder solo para líneas que no sean de cabecera.

    Empezaremos con la clase SNP, y luego la clase Cromosoma. Aunque es difícil de mostrar aquí, es una buena idea trabajar y depurar cada método a su vez (con declaraciones ocasionales print ()), comenzando por los constructores. Debido a que un SNP es solo un SNP si el alelo de referencia y alternativo difieren, afirmaremos esta condición en el constructor para que se produzca un error si alguna vez intentamos crear un SNP no polimórfico (que en realidad no sería un SNP en absoluto).

    II.11_16_PY_109_SNP_EX1

    Observe el atajo que tomamos en el código anterior para el método .is_transversion (), que llama al método .is_transition () y devuelve la respuesta opuesta. Este tipo de codificación “atajo” tiene sus beneficios y desventajas. Un beneficio es que podemos reutilizar métodos en lugar de copiar y pegar para producir muchos fragmentos similares de código, lo que reduce el área de superficie potencial para que ocurran errores. Una desventaja es que tenemos que tener más cuidado, en este caso, hemos tenido que asegurarnos de que los alelos difieran (a través de la afirmación en el constructor), por lo que un SNP debe ser una transición o una transversión. (¿Es esto realmente cierto? ¿Y si alguien intentara crear un objeto SNP con caracteres que no sean ADN? Siempre es prudente considerar formas en que el código podría ser inadvertidamente mal utilizado.)

    Lo anterior muestra el inicio del script y la clase SNP; código como este podría probarse con solo unas pocas líneas:

    II.11_17_PY_110_SNP_EX2

    Aunque en última instancia no dejaremos estas líneas de prueba adentro, proporcionan una buena verificación de cordura para el código. Si estas comprobaciones estuvieran envueltas en una función que pudiera llamarse cada vez que hagamos cambios en el código, tendríamos lo que se conoce como prueba unitaria, o una colección de código (a menudo una o más funciones), con el propósito específico de probar la funcionalidad de otro código para su corrección. [7] Estos pueden ser especialmente útiles ya que el código cambia a lo largo del tiempo.

    Sigamos con la clase Cromosoma. Tenga en cuenta que el método.add_snp () contiene afirmaciones de que la ubicación del SNP no es un duplicado y que el nombre del cromosoma para el nuevo SNP coincide con el self.chrname del cromosoma.

    II.11_18_PY_111_SNP_EX3

    Ahora podemos escribir los métodos para .count_transiciones () y .count_transversion (). Debido a que nos hemos asegurado de que cada objeto SNP sea una transición o una transversión, y no se dupliquen ubicaciones dentro de un cromosoma, el método .count_transversion () puede hacer uso directo del método .count_transition () y el número total de SNP almacenados a través de len (self. locations_to_snps). (Alternativamente, podríamos hacer un count_transversion () que opere de manera similar a count_transition () haciendo un bucle sobre todos los objetos SNP.)

    II.11_19_PY_111_2_SNP_EX3

    El código de prueba correspondiente se encuentra a continuación. Aquí estamos usando declaraciones assert, pero también podríamos usar líneas como print (chr1.count_transition ()) y asegurarnos de que la salida sea la esperada.

    II.11_20_PY_112_SNP_EX4

    Con las definiciones de clase creadas y depuradas, podemos escribir la parte “ejecutable” del programa, preocupada por analizar el archivo de entrada (a partir de un nombre de archivo dado en sys.argv [1]) e imprimir los resultados. Primero, la porción de código que verifica si el usuario ha dado un nombre de archivo (y produce algún texto de ayuda si no) y lee los datos en. Nuevamente, estamos almacenando una colección de objetos cromosómicos en un diccionario chrnames_to_chrs. Para cada línea VCF, determinamos si ya existe un Cromosoma con ese nombre: si es así, le pedimos a ese objeto a .add_snp (). Si no, creamos un nuevo objeto Cromosoma, se lo pedimos a .add_snp (), y lo agregamos al diccionario.

    II.11_21_PY_113_SNP_EX5

    En la línea chr_obj = chrnames_to_chrs [chrname] anterior, estamos definiendo una variable que hace referencia al objeto Cromosoma en el diccionario, y después de eso estamos pidiendo a ese objeto que agregue el SNP con .add_snp (). Podríamos haber combinado estos dos con sintaxis como chrnames_to_chrs [chrname] .add_snp ().

    Finalmente, un pequeño bloque de código imprime los resultados haciendo un bucle sobre las claves del diccionario, accediendo a cada objeto Cromosoma y preguntándole el número de transiciones y transversiones:

    II.11_22_PY_114_SNP_EX6

    Tendremos que eliminar o comentar el código de prueba (particularmente las pruebas que esperábamos que fallaran) para ver los resultados. Pero una vez que hagamos eso, podemos ejecutar el programa (llamado snps_ex.py).

    II.11_23_PY_114_2_SNP_EX_Run

    Lo que hemos creado aquí no es poca cosa, ¡con casi 150 líneas de código! Y sin embargo, cada pieza de código está encapsulada de alguna manera; incluso el long for-loop representa el código para analizar el archivo de entrada y rellenar el diccionario chrnames_to_chrs. Al nombrar claramente nuestras variables, métodos y clases podemos ver rápidamente lo que hace cada entidad. Podemos razonar sobre este programa sin demasiada dificultad al más alto nivel de abstracción pero también profundizar para entender cada pieza individualmente. Como beneficio, podemos reutilizar o adaptar fácilmente este código de una manera poderosa agregando o modificando métodos.

    Una extensión: Búsqueda de regiones densas en SNP

    El recuento de transiciones y transversiones por cromosoma para este archivo VCF podría haberse logrado sin definir clases y objetos. Pero una de las ventajas de dedicar algún tiempo a organizar el código por adelantado es que podemos responder más fácilmente preguntas relacionadas sobre los mismos datos.

    Supongamos que, habiendo determinado el número de transiciones y transversiones por cromosoma, ahora estamos interesados en determinar la región más densa de SNP de cada cromosoma. Hay varias formas en las que podríamos definir la densidad de SNP, pero elegiremos una fácil: dada una región desde las posiciones l a m, la densidad es el número de SNP que ocurren dentro de l y m dividido por el tamaño de la región, ml + 1, veces 1,000 (para SNP por 1,000 pares de bases).

    II.11_24_SNP_densidad_cálculo

    Para que un objeto Cromosoma pueda decirnos la región de mayor densidad, necesitará poder calcular la densidad para cualquier región dada contando los SNP en esa región. Podemos comenzar agregando a la clase cromosómica un método que compute la densidad de SNP entre dos posiciones l y m.

    II.11_25_PY_114_3_SNP_Densidad_Región

    Después de depurar este método y asegurar que funcione, podemos escribir un método que encuentre la región de mayor densidad. Pero, ¿cómo debemos definir nuestras regiones? Digamos que queremos considerar regiones de 100 mil bases. Entonces podríamos considerar las bases 1 a 100,000 como una región, 100,001 a 200,000 como una región, y así sucesivamente, hasta que el inicio de la región considerada es más allá de la última ubicación del SNP. Podemos lograr esto con un bucle de tiempo. La estrategia será mantener información sobre la región más densa encontrada hasta el momento (incluyendo su densidad así como la ubicación de inicio y finalización), y actualizar esta respuesta según sea necesario en el bucle. [8]

    II.11_26_PY_114_4_SNP_densidad_bucle

    En lo anterior, necesitábamos acceder a la posición del último SNP en el cromosoma (para que el código pudiera dejar de considerar regiones más allá del último SNP). En lugar de escribir ese código directamente en el método, decidimos que debería ser su propio método, y lo marcamos con un comentario “todo”. Entonces, también necesitamos agregar este método:

    II.11_27_PY_114_5_SNP_Last_SNP_POS

    En el código que imprime los resultados, podemos agregar la nueva llamada a .max_density (100000) para cada cromosoma, e imprimir la información relevante.

    II.11_28_PY_114_6_SNP_densidad_uso

    Llamemos a nuestro nuevo snps_ex_density.py (canalizando el resultado a través de la columna -t para ver más fácilmente el diseño de la columna separada por tabuladores):

    II.11_29_py_114_7_snp_densidad_out

    Nuevamente, ninguno de los métodos individuales o secciones de código son particularmente largos o complejos, pero juntos representan un programa de análisis bastante sofisticado.

    Resumen

    Quizás encuentres que estos ejemplos usando clases y objetos para la resolución de problemas sean elegantes, o quizás no. Algunos programadores piensan que este tipo de organización da como resultado un código demasiado detallado y complejo. Sin duda es fácil llegar a ser demasiado ambicioso con la idea de clases y objetos. Crear clases personalizadas para cada pequeña cosa corre el riesgo de confusión y molestias innecesarias. Al final, depende de cada programador decidir qué nivel de encapsulación es el adecuado para el proyecto; para la mayoría de las personas, una buena separación de conceptos mediante el uso de clases es una forma de arte que requiere práctica.

    ¿Cuándo deberías considerar crear una clase?

    • Cuando tienes muchos tipos diferentes de datos relacionados con el mismo concepto, y te gustaría mantenerlos organizados en objetos individuales como variables de instancia.
    • Cuando tienes muchas funciones diferentes relacionadas con el mismo concepto, y te gustaría mantenerlas organizadas en objetos individuales como métodos.
    • Cuando tienes un concepto que ahora es simple, pero sospechas que podría aumentar en complejidad en el futuro a medida que le añades. Al igual que las funciones, las clases permiten que el código se reutilice, y es fácil agregar nuevos métodos y variables de instancia a las clases cuando sea necesario.

    Herencia y Polimorfismo

    A pesar de esta discusión sobre los objetos, existen algunas características únicas del paradigma orientado a objetos que no hemos cubierto pero que a veces se consideran integrales al tema. En particular, la mayoría de los lenguajes orientados a objetos (incluido Python) admiten herencia y polimorfismo para objetos y clases.

    La herencia es la idea de que algunos tipos de clases pueden representarse como casos especiales de otros tipos de clases. Considere una clase que define una Secuencia, que podría tener variables de instancia para self.seq y self.id. Las secuencias podrían ser capaces de reportar su longitud y, por lo tanto, podrían tener un método .length bp (), devolviendo len (self.seq). También puede haber muchas otras operaciones que una Secuencia genérica podría soportar, como .get_id (). Ahora, supongamos que queríamos implementar una clase openReadingFrame; también debería tener un self.id y un self.seq y poder reportar su .length bp (). Debido a que un objeto de este tipo representaría un marco de lectura abierto, probablemente también debería tener un método .get_translation () que devuelva la traducción de aminoácidos de su self.seq. Al usar la herencia, podemos definir la clase OpenReadingFrame como un tipo de clase Sequence, ahorrándonos de tener que volver a implementar .longitud_bp () —solo necesitaríamos implementar el método .get_translation () específico de la clase y cualquier otro método sería heredado automáticamente de la clase Sequence.

    imagen

    El polimorfismo es la idea de que heredar tipos de clases no tiene que aceptar los métodos predeterminados heredados, y son libres de volver a implementar (o “anular”) métodos específicos incluso si sus clases “padre” o “hermano” ya los definen. Por ejemplo, podríamos considerar otra clase llamada AminoacidSequence que hereda de Sequence, así que también tendrá un .get_id () y .longitud_bp (); en este caso, sin embargo, el heredado .longitud_bp () estaría mal, porque len (self.seq ) sería tres veces demasiado corto. Entonces, una AminoacidSequence podría anular el método .length bp () para devolver 3*len (self.seq). La característica interesante del polimorfismo es que dado un objeto como Gene_a, ni siquiera necesitamos saber qué “tipo” de objeto Sequence es: ejecutar Gene_a.Length_BP () devolverá la respuesta correcta si es alguno de estos tres tipos de secuencia.

    II.11_31_py_114_9_polimorfismo

    Estas ideas son consideradas por muchos como los puntos definitorios del “diseño orientado a objetos”, y permiten a los programadores estructurar su código de manera jerárquica (vía herencia) al tiempo que permiten patrones interesantes de flexibilidad (vía polimorfismo). No los hemos cubierto en detalle aquí, ya que hacer un buen uso de ellos requiere una buena cantidad de práctica. Además, ¡la simple idea de encapsular datos y funciones en objetos proporciona bastantes beneficios en sí misma!

    Ejercicios

    1. Modifique el script snps_ex_density.py para generar, por cada región de 100.000pb de cada cromosoma, el porcentaje de SNP que son transiciones y el número de SNP en cada ventana. La salida debe ser un formato que se vea así:
      II.11_32_PY_114_10_SNP_DENS_EX_Salida En la sección sobre programación R (capítulo 37, “Ploting Data y ggplot2”), descubriremos formas fáciles de visualizar este tipo de salida.
    2. El módulo random (usado con import random) nos permite hacer elecciones aleatorias; por ejemplo, random.random () devuelve un float aleatorio entre 0.0 y 1.0. La función random.randint (a, b) devuelve un entero aleatorio entre a y b (inclusive); por ejemplo, random.randint (1, 4) podría devolver 1, 2, 3 o 4. También hay una función random.choice (); dada una lista, devuelve un solo elemento (al azar) de ella. Entonces, si bases = ["A”, “C”, “T”, “G"], entonces random.choice (bases) devolverá una sola cadena, ya sea “A”, “C”, “T” o “G”.

      Crea un programa llamado pop_sim.py. En este programa escribe una clase Bug; un objeto “bug” representará un organismo individual con un genoma, a partir del cual se puede calcular una aptitud. Por ejemplo, si a = Bug (), tal vez a tendrá un autogenoma como una lista de 100 bases de ADN aleatorias (por ejemplo, ["G”, “T”, “A”, “G”,... ; estos deben crearse en el constructor). Debe implementar un método .get_fitness () que devuelve un float calculado de alguna manera a partir de self.genome, por ejemplo el número de bases G o C, más 5 si el genoma contiene tres caracteres “A” en una fila. Los objetos bug también deben tener un método.mutate_random_base (), que hace que un elemento aleatorio de self.genome se establezca en un elemento aleatorio de ["A”, “C”, “G”, “T"]. Finalmente, implementar un método .set_base (), que establece un índice específico en el genoma a una base específica: a.set_base (3, “T”) debería establecer self.genome [3] en “T”.

      Pon a prueba tu programa creando una lista de 10 objetos Bug, y en un for-loop, haz que cada uno ejecute su método.mutate_random_base () e imprima su nueva aptitud.

    3. A continuación, crear una clase de Población. Los objetos de población tendrán una lista de objetos Bug (digamos, 50) llamados self.bug_list.

      Esta clase Population debe tener un método .create_descendencia (), que: 1) creará una lista new_pop, 2) por cada elemento oldbug de self.bug_list: a) crear un nuevo objeto Bug newbug, b) y establecer el genoma de newbug (una base a la vez) para que sea el mismo que el de oldbug, c) llame a newbug.mutate_random_base (), y d) agregue oldbug y newbug a new_pop. Por último, este método debería 3) establecer self.bug_pop en new_pop.

      La clase Population también tendrá un método .cull (); esto debería reducir self.bug_pop al 50% superior de los objetos bug por aptitud física. (Podría encontrar el ejercicio anterior discutiendo. __lt__ () y métodos similares útiles, ya que te permitirán ordenar self.bug_pop por fitness si se implementa correctamente.)

      Finalmente, implementar un método .get_mean_fitness (), que debería devolver la aptitud promedio de self.bug_pop.

      Para probar tu código, crea una instancia de un objeto p = Population (), y en un for-loop: 1) ejecuta p.create_descendencia (), 2) ejecuta p.cull () y 3) imprime p.get_mean_fitness (), permitiéndote ver el progreso evolutivo de tu simulación.

    4. Modificar el programa de simulación anterior para explorar su dinámica. Podría considerar agregar un método.get_best_individual () a la clase Population, por ejemplo, o implementar un esquema de “apareamiento” mediante el cual los genomas de descendencia son mezclas de dos genomas parentales. También podrías intentar ajustar el método.get_fitness (). Este tipo de simulación se conoce como algoritmo genético, especialmente cuando los individuos evolucionados representan soluciones potenciales a un problema computacional. [9]

    1. Otra metodología destacada para la programación es el paradigma “funcional”, donde las funciones son el foco principal y los datos suelen ser inmutables. Si bien Python también es compatible con la programación funcional, no nos centraremos en este tema. Por otro lado, el lenguaje de programación R enfatiza la programación funcional, por lo que exploraremos este paradigma con más detalle en capítulos posteriores.
    2. Esta definición no es precisa y, de hecho, tergiversa intencionalmente cómo se almacenan los objetos en la RAM. (En realidad, todos los objetos del mismo tipo comparten un solo conjunto de funciones/métodos). Pero esta definición nos va a servir bien conceptualmente.
    3. Aunque algunos han criticado la antropomorfización de objetos de esta manera, está perfectamente bien, siempre y cuando siempre digamos “¡por favor!”
    4. Este primer parámetro técnicamente no necesita ser nombrado self, pero es un estándar ampliamente aceptado.
    5. Uno podría preguntarse si un objeto Gene alguna vez necesitaría permitir que su secuencia cambiara en absoluto. Una posible razón sería si simuláramos mutaciones a lo largo del tiempo; un método como .mutar (0.05) podría pedirle a un gen que cambie aleatoriamente el 5% de sus bases.
    6. Este archivo se obtuvo del Proyecto 1000 Genomas Humanos en ftp://ftp.1000genomes.ebi.ac.uk/vol1..._07/trio/snps/, en el archivo CEU.Trio.2010_03.genotypes.vcf.gz. Después de descomprimir, muestreamos el 5% de los SNP al azar con awk '{if ($1 ~ “##” || rand () < 0.05) {print $0}}' CEU.trio.2010_03.genotypes.vcf > trio.sample.vcf; ver capítulo 10, “Filas y Columnas” para más detalles.
    7. Las pruebas unitarias suelen ser automatizadas; de hecho, Python incluye un módulo para construir pruebas unitarias llamado unittest.
    8. Esta estrategia no es la más rápida que podríamos haber ideado. Para determinar la región de mayor densidad, tenemos que considerar cada región, y el cálculo para cada región implica el bucle sobre todas las posiciones del SNP (la gran mayoría de las cuales se encuentran fuera de la región). Existen algoritmos más sofisticados que correrían mucho más rápido, pero están fuera del alcance de este libro. Sin embargo, ¡una solución lenta es mejor que ninguna solución!
    9. La idea de simular poblaciones “in silico” no sólo es bastante divertida, sino que también ha producido interesantes conocimientos sobre la dinámica poblacional. Para un ejemplo, véase Hinton y Nowlan, “Cómo el aprendizaje puede guiar la evolución”, Complex Systems 1 (1987): 495—502. La simulación de sistemas complejos mediante muestreo aleatorio se conoce comúnmente como método de Monte Carlo. Para un tratamiento más imaginario de simulaciones de sistemas naturales, véase Daniel Shiffman, La naturaleza del código (autor, 2012). Los ejemplos allí utilizan el lenguaje gráfico disponible en http://processing.org, aunque una versión de Python también está disponible en http://py.processing.org.

    This page titled 2.11: Objetos y Clases is shared under a CC BY-NC-SA license and was authored, remixed, and/or curated by Shawn T. O’Neil (OSU Press) .