Categorías
Python

Programación orientada a objetos en Python vs Java

 

Tabla de Contenidos

  • alternativas a Clases de datos
  • Básico de Datos ClassesDefault ValuesType Métodos HintsAdding
  • Valores por defecto de
  • tipo, insinúa
  • de añadir métodos de
  • Más datos flexible ClassesAdvanced defecto ValuesYou necesita representación? Comparar los valores de las tarjetas
  • avanzada defecto
  • Usted

  • Representación de la necesidad?
  • comparar las clases Tarjetas
  • inmutable de datos
  • Herencia
  • Clases de optimizar los datos
  • Conclusión y lecturas adicionales
  • valores por defecto
  • Tipo Hints
  • de añadir métodos de
  • avanzada Valores por defecto de
  • usted necesita representación?
  • comparar las tarjetas

Una característica nueva y emocionante venir en Python 3.7 es la clase de datos. Una clase de datos es una clase normalmente contiene principalmente datos, aunque en realidad no hay ninguna restricción. Se ha creado usando el nuevo decorador @dataclass, de la siguiente manera:

from dataclasses import dataclass

@dataclass
class DataClassCard:
rank: str
suit: str

Nota: Este código , así como todos los demás ejemplos en este tutorial, sólo se trabajo en Python 3.7 y superiores. clase de datos

Un viene con funcionalidad básica ya ejecutados. Por ejemplo, se puede crear una instancia, imprimir y comparar instancias de la clase de datos directamente de la caja:

>>> queen_of_hearts = DataClassCard('Q', 'Hearts')
>>> queen_of_hearts.rank
'Q'
>>> queen_of_hearts
DataClassCard(rank='Q', suit='Hearts')
>>> queen_of_hearts == DataClassCard('Q', 'Hearts')
True

Compare eso con una clase regular. Una clase regular mínima sería algo como esto:

class RegularCard
def __init__(self, rank, suit):
self.rank = rank
self.suit = suit

Si bien esto no es mucho más código para escribir, ya se puede ver signos de dolor repetitivo: rango y palo ambos se repiten tres veces simplemente para inicializar un objeto. Por otra parte, si se intenta utilizar esta clase normal, se dará cuenta que la representación de los objetos no es muy descriptivo, y por alguna razón una reina de corazones, no es lo mismo que una reina de corazones:

>>> queen_of_hearts = RegularCard('Q', 'Hearts')
>>> queen_of_hearts.rank
'Q'
>>> queen_of_hearts
<__main__.RegularCard object at 0x7fb6eee35d30>
>>> queen_of_hearts == RegularCard('Q', 'Hearts')
False

Parece que los datos clases nos están ayudando a cabo entre bastidores. Por defecto, las clases de datos implementan un método .__ __ repr () para proporcionar una representación de cadena agradable y un método .__ eq __ () que se pueden hacer comparaciones de objetos básicos. Para la clase RegularCard imitar la clase de datos anterior, es necesario añadir estos métodos así:

class RegularCard
def __init__(self, rank, suit):
self.rank = rank
self.suit = suit

def __repr__(self):
return (f'{self.__class__.__name__}'
f'(rank={self.rank!r}, suit={self.suit!r})')

def __eq__(self, other):
if other.__class__ is not self.__class__:
return NotImplemented
return (self.rank, self.suit) == (other.rank, other.suit)

En este tutorial, aprenderá exactamente qué clases de datos proporcionan comodidad. Además de las representaciones agradables y comparaciones, verá:

  • ¿Cómo añadir valores por defecto a la clase campos de datos
  • Cómo clases de datos permiten la ordenación de los objetos
  • cómo representar datos inmutables
  • Cómo manejan las clases de datos herencia

pronto vamos a profundizar más en aquellas características de las clases de datos. Sin embargo, usted puede estar pensando que ya ha visto algo como esto antes. Bono

gratuito: Haga clic aquí para obtener acceso a un capítulo de trucos Python: El libro que te muestra las mejores prácticas de Python con ejemplos sencillos puede aplicar instantáneamente a escribir código más bonito + Pythonic.

Alternativas a Clases de datos

Para las estructuras de datos simples, probablemente ya han utilizado una tupla o un diccionario. Se podía representar a la reina de corazones tarjeta en cualquiera de las siguientes maneras:

>>> queen_of_hearts_tuple = ('Q', 'Hearts')
>>> queen_of_hearts_dict = {'rank': 'Q', 'suit': 'Hearts'}

Funciona. Sin embargo, pone una gran responsabilidad en usted como un programador:

  • Es necesario recordar que los queen_of_hearts _… variable representa una tarjeta.
  • Para la versión _tuple, es necesario recordar el orden de los atributos. Escribir ( ‘espadas’, ‘A’) se hace un lío de su programa, pero probablemente no le dará un mensaje de error fácilmente comprensible.
  • Si utiliza el _dict, usted debe asegurarse de que los nombres de los atributos son consistentes. Por ejemplo {el ‘valor’: ‘A’, ‘traje’: ‘picas’} no funcionará como se espera.

Además, el uso de estas estructuras no es ideal:

>>> queen_of_hearts_tuple[0] # No named access
'Q'
>>> queen_of_hearts_dict['suit'] # Would be nicer with .suit
'Hearts'

Una alternativa mejor es la namedtuple. Durante mucho tiempo se ha utilizado para crear pequeñas estructuras de datos legibles. Podemos, de hecho, recrear el ejemplo de la clase de datos anterior utilizando un namedtuple así:

from collections import namedtuple

NamedTupleCard = namedtuple('NamedTupleCard', ['rank', 'suit'])

Esta definición de NamedTupleCard daremos exacta la misma salida que nuestro ejemplo no DataClassCard:

>>> queen_of_hearts = NamedTupleCard('Q', 'Hearts')
>>> queen_of_hearts.rank
'Q'
>>> queen_of_hearts
NamedTupleCard(rank='Q', suit='Hearts')
>>> queen_of_hearts == NamedTupleCard('Q', 'Hearts')
True

Así que ¿por qué molestarse con las clases de datos? En primer lugar, las clases de datos vienen con muchas más características que usted ha visto hasta ahora. Al mismo tiempo, el namedtuple tiene algunas otras características que no son necesariamente deseable. Por diseño, un namedtuple es una tupla regular. Esto se puede ver en las comparaciones, por ejemplo:

>>> queen_of_hearts == ('Q', 'Hearts')
True

Si bien esto puede parecer una cosa buena, esta falta de conciencia sobre su propio tipo puede conducir a errores sutiles y difíciles de encontrar, especialmente ya que también comparará felizmente dos diferentes clases namedtuple:

>>> Person = namedtuple('Person', ['first_initial', 'last_name']
>>> ace_of_spades = NamedTupleCard('A', 'Spades')
>>> ace_of_spades == Person('A', 'Spades')
True

El namedtuple también viene con algunas restricciones. Por ejemplo, es difícil añadir valores por defecto para algunos de los campos en un namedtuple. Un namedtuple también es inmutable por naturaleza. Es decir, el valor de una namedtuple nunca puede cambiar. En algunas aplicaciones, esto es una característica impresionante, pero en otros contextos, sería bueno tener más flexibilidad: las clases

>>> card = NamedTupleCard('7', 'Diamonds')
>>> card.rank = '9'
AttributeError: can't set attribute

datos no sustituirán a todos los usos de namedtuple. Por ejemplo, si usted necesita su estructura de datos a comportarse como una tupla, entonces una tupla con nombre es una gran alternativa!

Otra alternativa, y una de las inspiraciones para las clases de datos, es el proyecto attrs. Con attrs instalados (PIP instalar attrs), se puede escribir una clase de tarjeta de la siguiente manera:

import attr

@attr.s
class AttrsCard:
rank = attr.ib()
suit = attr.ib()

Esto puede ser usado exactamente de la misma manera que los ejemplos DataClassCard y NamedTupleCard anterior. El proyecto attrs es grande y es compatible con algunas características que las clases de datos no lo hacen, incluidos los convertidores y validadores. Además, attrs ha sido de alrededor por un tiempo y se admite en Python 2.7, así como Python 3.4 y más. Sin embargo, como attrs no es una parte de la biblioteca estándar, se le añade una dependencia externa para sus proyectos. A través de las clases de datos, una funcionalidad similar estará disponible en todas partes.

Además de tupla, dict, namedtuple y attrs, hay muchos otros proyectos similares, incluyendo typing.NamedTuple, namedlist, attrdict, fontanero, y campos. Mientras que las clases de datos son una gran alternativa nueva, todavía hay casos de uso donde uno de los mayores variantes mejor encaja. Por ejemplo, si necesita compatibilidad con una API específica esperando tuplas o funcionalidad necesidad no admitidas en las clases de datos. Clases

de Datos Básicas

Volvamos a clases de datos. A modo de ejemplo, vamos a crear una clase de posición que representan las posiciones geográficas con un nombre así como la latitud y longitud:

from dataclasses import dataclass

@dataclass
class Position:
name: str
lon: float
lat: float

Lo que hace esta una clase de datos es el decorador @dataclass justo por encima de la definición de clase. Por debajo de la posición de clase: línea, simplemente enumera los campos que desee en su clase de datos. El: notación utilizada para los campos está utilizando una nueva función en Python 3.6 llamada anotaciones variables. pronto vamos a hablar más sobre esta notación y la razón por la que especifique los tipos de datos como str y el flotador.

Esas pocas líneas de código son todo lo que necesita. La nueva clase está listo para su uso:

>>> pos = Position('Oslo', 10.8, 59.9)
>>> print(pos)
Position(name='Oslo', lon=10.8, lat=59.9)
>>> pos.lat
59.9
>>> print(f'{pos.name} is at {pos.lat}°N, {pos.lon}°E')
Oslo is at 59.9°N, 10.8°E

También puede crear clases de datos de manera similar a cómo se crean las tuplas con nombre. La siguiente es (casi) equivalente a la definición de la Posición arriba: clase de datos

from dataclasses import make_dataclass

Position = make_dataclass('Position', ['name', 'lat', 'lon'])

A es una clase regular Python. Lo único que lo diferencia es que cuenta con métodos básicos de modelos de datos como .__ init __ (), repr .__ __ (), y .__ eq __ () implementado para usted.

Valores por defecto de

Es fácil de añadir valores predeterminados a los campos de la clase de datos:

from dataclasses import dataclass

@dataclass
class Position:
name: str
lon: float = 0.0
lat: float = 0.0

Esto funciona exactamente igual que si hubiera especificado los valores por defecto en la definición del método .__ init __ () de una clase regular:

>>> Position('Null Island')
Position(name='Null Island', lon=0.0, lat=0.0)
>>> Position('Greenwich', lat=51.8)
Position(name='Greenwich', lon=0.0, lat=51.8)
>>> Position('Vancouver', -123.1, 49.3)
Position(name='Vancouver', lon=-123.1, lat=49.3)

más tarde se aprenderá sobre default_factory, lo que le da una forma de proporcionar valores por defecto más complicado.

Tipo Hints

Hasta ahora, no hemos hecho un gran alboroto del hecho de que las clases de datos de la ayuda a escribir fuera de la caja. Es probable que haya notado que definimos los campos con un toque tipo: nombre: str dice que el nombre debe ser una cadena de texto (tipo str).

De hecho, la adición de una cierta clase de tipo de pista es obligatoria en la definición de los campos de la clase de datos. Sin una pizca tipo, el campo no será una parte de la clase de datos. Sin embargo, si no desea agregar tipos explícitos a su clase de datos, el uso typing.Any:

from dataclasses import dataclass
from typing import Any

@dataclass
class WithoutExplicitTypes:
name: Any
value: Any = 42

Si bien es necesario agregar toques tipo en alguna forma al utilizar clases de datos, estos tipos no se hacen cumplir en tiempo de ejecución. Los siguientes carreras código sin ningún problema:

>>> Position(3.14, 'pi day', 2018)
Position(name=3.14, lon='pi day', lat=2018)

Esta es la forma de escribir en Python normalmente funciona: Python es y será siempre un lenguaje de tipos dinámicos. Para atrapar en realidad errores de tipo, forma de las damas como Mypy se pueden ejecutar en su código fuente.

de añadir métodos de

Usted ya sabe que una clase de datos es sólo una clase regular. Eso significa que se pueden agregar libremente sus propios métodos para una clase de datos. A modo de ejemplo, vamos a calcular la distancia entre una posición y otra, a lo largo de la superficie de la Tierra. Una forma de hacer esto es mediante el uso de la fórmula haversine:

Puede agregar un método .distance_to () a su clase de datos al igual que se puede con las clases normales:

from dataclasses import dataclass
from math import asin, cos, radians, sin, sqrt

@dataclass
class Position:
name: str
lon: float = 0.0
lat: float = 0.0

def distance_to(self, other):
r = 6371 # Earth radius in kilometers
lam_1, lam_2 = radians(self.lon), radians(other.lon)
phi_1, phi_2 = radians(self.lat), radians(other.lat)
h = (sin((phi_2 - phi_1) / 2)**2
+ cos(phi_1) * cos(phi_2) * sin((lam_2 - lam_1) / 2)**2)
return 2 * r * asin(sqrt(h))

Funciona como era de esperar:

>>> oslo = Position('Oslo', 10.8, 59.9)
>>> vancouver = Position('Vancouver', -123.1, 49.3)
>>> oslo.distance_to(vancouver)
7181.7841229421165

Más clases de datos flexible

Hasta el momento, se han visto algunas de las características básicas de la clase de datos: te da algunos métodos de conveniencia, y todavía se puede añadir valores por defecto y otros métodos. Ahora usted aprenderá acerca de algunas de las características más avanzadas como parámetros al decorador @dataclass y la función de campo (). Juntos, te dan más control al crear una clase de datos.

Volvamos al ejemplo de la tarjeta de juego que vio al principio del tutorial y añadir una clase que contiene una baraja de cartas, mientras estamos en ello: la cubierta sencilla

from dataclasses import dataclass
from typing import List

@dataclass
class PlayingCard:
rank: str
suit: str

@dataclass
class Deck:
cards: List[PlayingCard]

A que contiene sólo dos cartas se pueden crear de esta manera:

>>> queen_of_hearts = PlayingCard('Q', 'Hearts')
>>> ace_of_spades = PlayingCard('A', 'Spades')
>>> two_cards = Deck([queen_of_hearts, ace_of_spades])
Deck(cards=[PlayingCard(rank='Q', suit='Hearts'),
PlayingCard(rank='A', suit='Spades')])

avanzada Valores por defecto de

decir que usted quiere dar un valor por defecto a la cubierta. Sería conveniente, por ejemplo, si la cubierta () crea un (francés) baraja de 52 cartas. En primer lugar, especifique los diferentes rangos y trajes. A continuación, añadir un make_french_deck función () que crea una lista de instancias de baraja:

RANKS = '2 3 4 5 6 7 8 9 10 J Q K A'.split()
SUITS = '♣ ♢ ♡ ♠'.split()

def make_french_deck():
return [PlayingCard(r, s) for s in SUITS for r in RANKS]

Para la diversión, los cuatro palos diferentes se especifican mediante sus símbolos Unicode.

Nota: anterior, usamos glifos Unicode, como ♠ directamente en el código fuente. Podríamos hacer esto porque los soportes de Python escribir código fuente en UTF-8 por omisión. Consulte esta página en la entrada Unicode para saber cómo introducir estos en su sistema. También puede introducir los caracteres Unicode para los trajes que usan los escapes de caracteres \ N con nombre (como \ n {} NEGRO spade) o \ u Unicode escapa (como \ u2660). comparaciones de tarjetas

Para simplificar más tarde, las filas y trajes también se enumeran en el orden habitual. La teoría

>>> make_french_deck()
[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), ...
PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')]

en, que podría ahora utilizar esta función para especificar un valor predeterminado para Deck.cards:

from dataclasses import dataclass
from typing import List

@dataclass
class Deck: # Will NOT work
cards: List[PlayingCard] = make_french_deck()

No haga esto! Esto introduce uno de los más anti-patrones comunes en Python: utilizando los argumentos por defecto mutables. El problema es que todas las instancias de la cubierta utilizarán el mismo objeto de lista como el valor predeterminado de la propiedad .cards. Esto significa que si, por ejemplo, una tarjeta se ha extraído de una cubierta, y luego desaparece de todas las otras instancias de la cubierta también. En realidad, las clases de datos tratan de evitar que usted haga esto, y el código de seguridad aumentarán una ValueError.

En cambio, las clases de datos utilizan algo llamado default_factory para manejar los valores por defecto mutables. Para uso default_factory (y muchos otros fresca ofrece clases de datos), es necesario utilizar el especificador de campo ():

from dataclasses import dataclass, field
from typing import List

@dataclass
class Deck:
cards: List[PlayingCard] = field(default_factory=make_french_deck)

El argumento para default_factory puede ser cualquier parámetro exigible cero. Ahora es fácil crear una baraja completa de cartas de juego:

>>> Deck()
Deck(cards=[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), ...
PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')])

el campo () especificador se usa para personalizar cada campo de una clase de datos de forma individual. Verá algunos otros ejemplos más adelante. Como referencia, estos son los parámetros de campo (): soportes predeterminado

  • : El valor por defecto del campo
  • default_factory: Función que devuelve el valor inicial del campo
  • de inicio: use campo en .__ método init () __? (El valor predeterminado es True.)
  • repr: Uso de campo en repr del objeto? (El valor predeterminado es True.)
  • comparar: Incluir el campo en las comparaciones? (El valor predeterminado es True.)
  • de hash: Incluir el campo en el cálculo de hash ()? (Por defecto es usar el mismo que para comparar.)
  • metadatos: Un mapeo con información sobre el campo

En el ejemplo de posición, que vio cómo agregar valores predeterminados simples escribiendo Lat: float = 0,0. Sin embargo, si también desea personalizar el campo, por ejemplo, para ocultar que en el repr, es necesario utilizar el parámetro por defecto: Lat: float = campo (por defecto = 0,0, repr = False). No se puede especificar tanto defecto y default_factory.

El parámetro de metadatos no es utilizado por las clases de datos a sí mismos, pero está disponible para usted (o paquetes de terceros) para adjuntar información a los campos. En el ejemplo de posición, se puede por ejemplo especificar que la latitud y la longitud se debe dar en grados:

from dataclasses import dataclass, field

@dataclass
class Position:
name: str
lon: float = field(default=0.0, metadata={'unit': 'degrees'})
lat: float = field(default=0.0, metadata={'unit': 'degrees'})

Los metadatos (y otra información sobre un campo) pueden ser recuperados u s ing el campo de función s () ( observar el plural s ): Necesidad Representación

>>> from dataclasses import fields
>>> fields(Position)
(Field(name='name',type=,...,metadata={}),
Field(name='lon',type=,...,metadata={'unit': 'degrees'}),
Field(name='lat',type=,...,metadata={'unit': 'degrees'}))
>>> lat_unit = fields(Position)[2].metadata['unit']
>>> lat_unit
'degrees'

usted?

Recordemos que podemos crear barajas de cartas de la nada:

>>> Deck()
Deck(cards=[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), ...
PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')])

Mientras que esta representación de una cubierta es explícita y legible, sino que también es muy detallado. He suprimido 48 de las 52 cartas de la baraja en la salida anterior. En una pantalla de 80 columnas, sólo tiene que imprimir la cubierta completa dura hasta 22 líneas! Añadamos una representación más concisa. En general, un objeto de Python tiene dos representaciones de cadena diferentes:

  • repr (obj) está definido por obj .__ __ repr () y debe devolver una representación amistoso con el desarrollador de obj. Si es posible, esto debe ser un código que puede recrear obj. Las clases de datos hacen esto.
  • str (obj) se define por obj .__ __ str () y debe devolver una representación fácil de usar de obj. Las clases de datos no implementan un método str .__ __ (), lo que Python recurrirá al método .__ __ repr ().

repr (obj) se define por obj .__ __ repr () y debe devolver una representación amigable para los desarrolladores de obj. Si es posible, esto debe ser un código que puede recrear obj. Las clases de datos hacen esto.

str (obj) se define por obj .__ __ str () y debe devolver una representación fácil de usar de obj. Las clases de datos no implementan un método str .__ __ (), lo que Python recurrirá al método .__ __ repr ().

apliquémoslo una representación fácil de usar de una baraja:

from dataclasses import dataclass

@dataclass
class PlayingCard:
rank: str
suit: str

def __str__(self):
return f'{self.suit}{self.rank}'

Las tarjetas ahora se ven mucho mejor, pero la cubierta es todavía tan prolijo como siempre:

>>> ace_of_spades = PlayingCard('A', '♠')
>>> ace_of_spades
PlayingCard(rank='A', suit='♠')
>>> print(ace_of_spades)
♠A
>>> print(Deck())
Deck(cards=[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), ...
PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')])

para mostrar que es posible añadir su .__ repr __ ( ) método, así, vamos a violar el principio de que debe devolver el código que se puede volver a crear un objeto. Aplicación en la práctica es mejor que la pureza después de todo. El siguiente código añade una representación más concisa de la cubierta:

from dataclasses import dataclass, field
from typing import List

@dataclass
class Deck:
cards: List[PlayingCard] = field(default_factory=make_french_deck)

def __repr__(self):
cards = ', '.join(f'{c!s}' for c in self.cards)
return f'{self.__class__.__name__}({cards})'

Nota especificador de la s en el c {s!} Cadena de formato!. Esto significa que explícitamente deseamos utilizar la representación str () de cada baraja. Con el nuevo .__ __ repr (), la representación de la cubierta es más fácil en los ojos:

>>> Deck()
Deck(♣2, ♣3, ♣4, ♣5, ♣6, ♣7, ♣8, ♣9, ♣10, ♣J, ♣Q, ♣K, ♣A,
♢2, ♢3, ♢4, ♢5, ♢6, ♢7, ♢8, ♢9, ♢10, ♢J, ♢Q, ♢K, ♢A,
♡2, ♡3, ♡4, ♡5, ♡6, ♡7, ♡8, ♡9, ♡10, ♡J, ♡Q, ♡K, ♡A,
♠2, ♠3, ♠4, ♠5, ♠6, ♠7, ♠8, ♠9, ♠10, ♠J, ♠Q, ♠K, ♠A)

comparar las tarjetas

En muchos juegos de cartas, tarjetas se comparan entre sí. Por ejemplo, en un típico truco de tomar juego, la carta más alta toma el truco. Ya que se implementa actualmente, la clase baraja no es compatible con este tipo de comparación:

>>> queen_of_hearts = PlayingCard('Q', '♡')
>>> ace_of_spades = PlayingCard('A', '♠')
>>> ace_of_spades > queen_of_hearts
TypeError: '>' not supported between instances of 'Card' and 'Card'

Esto es, sin embargo, (aparentemente) fáciles de corregir:

from dataclasses import dataclass

@dataclass(order=True)
class PlayingCard:
rank: str
suit: str

def __str__(self):
return f'{self.suit}{self.rank}'

El @dataclass decorador tiene dos formas. Hasta ahora ha visto la forma sencilla en la que se especifica @dataclass sin ningún paréntesis y parámetros. Sin embargo, también se puede dar parámetros para el decorador @dataclass () entre paréntesis. Los siguientes parámetros son compatibles:

  • init: Añadir .__ método init () __? (El valor predeterminado es True.)
  • repr: Añadir .__ método () repr __? (El valor predeterminado es True.)
  • EQ: Añadir .__ () método eq __? (El valor predeterminado es True.) Para
  • : Añadir formas de realizar pedidos? (El predeterminado es Falso.) Unsafe_hash
  • : Método de la Fuerza de la adición de un hash .__ __ ()? (El predeterminado es Falso.)
  • congelado: Si es True, la asignación de campos elevar una excepción. (El predeterminado es Falso.)

Ver la PEP original para obtener más información acerca de cada parámetro. Después fin ajuste = Verdadero, los casos de baraja se pueden comparar:

>>> queen_of_hearts = PlayingCard('Q', '♡')
>>> ace_of_spades = PlayingCard('A', '♠')
>>> ace_of_spades > queen_of_hearts
False

¿Cómo están las dos tarjetas en comparación embargo? No ha especificado cómo el pedido se debe hacer, y por alguna razón Python parece creer que una reina es mayor que un As …

Resulta que las clases de datos se comparan los objetos como si fueran tuplas de sus campos. En otras palabras, una reina es mayor que un As, porque ‘Q’ viene después de ‘A’ en el alfabeto:

>>> ('A', '♠') > ('Q', '♡')
False

que en realidad no trabajo para nosotros. En su lugar, es necesario definir algún tipo de índice de tipo que utiliza el orden de rangos y trajes. Algo como esto:

>>> RANKS = '2 3 4 5 6 7 8 9 10 J Q K A'.split()
>>> SUITS = '♣ ♢ ♡ ♠'.split()
>>> card = PlayingCard('Q', '♡')
>>> RANKS.index(card.rank) * len(SUITS) + SUITS.index(card.suit)
42

Para baraja utilizar este tipo de índice para las comparaciones, tenemos que añadir un campo .sort_index a la clase. Sin embargo, este campo debe calcularse a partir de los otros campos .rank y .suit automáticamente. Esto es exactamente lo que el método especial .__ post_init __ () es para. Se permite un procesamiento especial después de la .__ init __ regulares () método se llama:

from dataclasses import dataclass, field

RANKS = '2 3 4 5 6 7 8 9 10 J Q K A'.split()
SUITS = '♣ ♢ ♡ ♠'.split()

@dataclass(order=True)
class PlayingCard:
sort_index: int = field(init=False, repr=False)
rank: str
suit: str

def __post_init__(self):
self.sort_index = (RANKS.index(self.rank) * len(SUITS)
+ SUITS.index(self.suit))

def __str__(self):
return f'{self.suit}{self.rank}'

Tenga en cuenta que .sort_index se añade como el primer campo de la clase. De esa manera, la comparación se realiza en primer lugar mediante .sort_index y sólo si hay lazos son los otros campos utilizados. Uso de un campo (), también debe especificar que .sort_index no debe ser incluida como un parámetro en el método .__ init __ () (ya que se calcula a partir del .rank y campos .suit). Para evitar confundir al usuario acerca de este detalle de implementación, es probable que sea también una idea buena para eliminar .sort_index de la repr de la clase.

Por último, los ases son altos:

>>> queen_of_hearts = PlayingCard('Q', '♡')
>>> ace_of_spades = PlayingCard('A', '♠')
>>> ace_of_spades > queen_of_hearts
True

Ahora puede crear fácilmente una cubierta Ordenada:

>>> Deck(sorted(make_french_deck()))
Deck(♣2, ♢2, ♡2, ♠2, ♣3, ♢3, ♡3, ♠3, ♣4, ♢4, ♡4, ♠4, ♣5,
♢5, ♡5, ♠5, ♣6, ♢6, ♡6, ♠6, ♣7, ♢7, ♡7, ♠7, ♣8, ♢8,
♡8, ♠8, ♣9, ♢9, ♡9, ♠9, ♣10, ♢10, ♡10, ♠10, ♣J, ♢J, ♡J,
♠J, ♣Q, ♢Q, ♡Q, ♠Q, ♣K, ♢K, ♡K, ♠K, ♣A, ♢A, ♡A, ♠A)

O bien, si no se preocupan por la clasificación, así es como se dibuja una mano aleatoria de 10 tarjetas:

>>> from random import sample
>>> Deck(sample(make_french_deck(), k=10))
Deck(♢2, ♡A, ♢10, ♣2, ♢3, ♠3, ♢A, ♠8, ♠9, ♠2)

De por supuesto, no es necesario order = True para que … Clases

inmutable de datos

Una de las características definitorias de la namedtuple ya hemos visto anteriormente es que es inmutable. Es decir, el valor de sus campos nunca se puede cambiar. Para muchos tipos de clases de datos, esto es una gran idea! Para hacer una inmutable clase de datos, establecer congelado = True cuando se crea. Por ejemplo, la siguiente es una versión inmutable de la clase de posición que vimos antes:

from dataclasses import dataclass

@dataclass(frozen=True)
class Position:
name: str
lon: float = 0.0
lat: float = 0.0

En una clase de datos congelados, no se puede Asignar valores a los campos después de la creación:

>>> pos = Position('Oslo', 10.8, 59.9)
>>> pos.name
'Oslo'
>>> pos.name = 'Stockholm'
dataclasses.FrozenInstanceError: cannot assign to field 'name'

Ten en cuenta que si la clase de datos contiene mutable campos, los que todavía podría cambiar. Esto es cierto para todas las estructuras de datos anidadas en Python (ver el vídeo para más información):

from dataclasses import dataclass
from typing import List

@dataclass(frozen=True)
class ImmutableCard:
rank: str
suit: str

@dataclass(frozen=True)
class ImmutableDeck:
cards: List[PlayingCard]

A pesar de que tanto ImmutableCard y ImmutableDeck son inmutables, la lista no es la celebración de tarjetas. Por lo tanto, todavía se puede cambiar las cartas de la baraja:

>>> queen_of_hearts = ImmutableCard('Q', '♡')
>>> ace_of_spades = ImmutableCard('A', '♠')
>>> deck = ImmutableDeck([queen_of_hearts, ace_of_spades])
>>> deck
ImmutableDeck(cards=[ImmutableCard(rank='Q', suit='♡'), ImmutableCard(rank='A', suit='♠')])
>>> deck.cards[0] = ImmutableCard('7', '♢')
>>> deck
ImmutableDeck(cards=[ImmutableCard(rank='7', suit='♢'), ImmutableCard(rank='A', suit='♠')])

Para evitar esto, asegúrese de que todos los campos de un uso inmutable clase de datos de tipos inmutables (pero recuerde que los tipos no se hacen cumplir en tiempo de ejecución). El ImmutableDeck debe ser implementada utilizando una tupla en lugar de una lista.

Herencia

Usted puede subclase clases de datos con bastante libertad. A modo de ejemplo, vamos a ampliar nuestro ejemplo de posición con un campo de país y utilizarlo para grabar capitales:

from dataclasses import dataclass

@dataclass
class Position:
name: str
lon: float
lat: float

@dataclass
class Capital(Position):
country: str

En este sencillo ejemplo, todo funciona sin problemas:

>>> Capital('Oslo', 10.8, 59.9, 'Norway')
Capital(name='Oslo', lon=10.8, lat=59.9, country='Norway')

Se añade el campo de país de capital después de los tres campos originales en posición. Las cosas se ponen un poco más complicado si cualquier campo de la clase base tienen valores por defecto:

from dataclasses import dataclass

@dataclass
class Position:
name: str
lon: float = 0.0
lat: float = 0.0

@dataclass
class Capital(Position):
country: str # Does NOT work

Este código se bloqueará inmediatamente con un TypeError quejándose de que “no predeterminada argumento‘país’sigue argumento predeterminado.” El problema es que nuestro nuevo campo de país no tiene un valor por defecto, mientras que la lon y lat campos tienen valores por defecto. La clase de datos se trate de escribir un método .__ init __ () con la siguiente firma:

def __init__(name: str, lon: float = 0.0, lat: float = 0.0, country: str):
...

Sin embargo, esto no es válida en Python. Si un parámetro tiene un valor por defecto, todos los siguientes parámetros también deben tener un valor por defecto. En otras palabras, si un campo de una clase base tiene un valor predeterminado, entonces todos los nuevos campos añadidos en una subclase deben tener valores por defecto también.

Otra cosa a tener en cuenta es cómo los campos están ordenados en una subclase. A partir de la clase base, los campos están ordenados en el orden en el que se definen en primer lugar. Si un campo se redefine en una subclase, su orden no cambia. Por ejemplo, si se define la posición y la capital de la siguiente manera:

from dataclasses import dataclass

@dataclass
class Position:
name: str
lon: float = 0.0
lat: float = 0.0

@dataclass
class Capital(Position):
country: str = 'Unknown'
lat: float = 40.0

Entonces el orden de los campos en Capital seguirá siendo nombre, lon, lat, país. Sin embargo, el valor por defecto de lat será 40,0. Clases

>>> Capital('Madrid', country='Spain')
Capital(name='Madrid', lon=0.0, lat=40.0, country='Spain')

de optimizar los datos

Voy a terminar este tutorial con unas pocas palabras acerca de ranuras. Las ranuras se pueden utilizar para hacer las clases más rápido y usar menos memoria. Las clases de datos no tienen la sintaxis explícita para trabajar con ranuras, pero la forma normal de la creación de ranuras funciona para las clases de datos también. (Realmente son sólo clases regulares!)

from dataclasses import dataclass

@dataclass
class SimplePosition:
name: str
lon: float
lat: float

@dataclass
class SlotPosition:
__slots__ = ['name', 'lon', 'lat']
name: str
lon: float
lat: float

En esencia, las ranuras se definen utilizando .__ slots__ para listar las variables en una clase. Variables o atributos no presentes en .__ slots__ no pueden ser definidos. Por otra parte, una clase ranuras no puede tener valores por defecto.

El beneficio de la adición de tales restricciones es que ciertas optimizaciones pueden llevar a cabo. Por ejemplo, las clases de ranuras ocupan menos memoria que puede ser medido utilizando Pympler:

>>> from pympler import asizeof
>>> simple = SimplePosition('London', -0.1, 51.5)
>>> slot = SlotPosition('Madrid', -3.7, 40.4)
>>> asizeof.asizesof(simple, slot)
(440, 248)

Del mismo modo, las clases de máquinas tragaperras son normalmente más rápido que trabajar. El siguiente ejemplo mide la velocidad de acceso de atributo en una clase de ranuras de datos y una clase regular de datos utilizando timeit de la librería estándar.

>>> from timeit import timeit
>>> timeit('slot.name', setup="slot=SlotPosition('Oslo', 10.8, 59.9)", globals=globals())
0.05882283499886398
>>> timeit('simple.name', setup="simple=SimplePosition('Oslo', 10.8, 59.9)", globals=globals())
0.09207444800267695

En este ejemplo particular, la clase de ranura es aproximadamente 35% más rápido.

Conclusión y Otras clases de lectura

de datos son una de las nuevas características de Python 3.7. Con clases de datos, que no tiene que escribir código repetitivo para obtener inicialización adecuada, la representación y la comparación de sus objetos.

Usted ha visto cómo definir sus propias clases de datos, así como:

  • Cómo agregar valores predeterminados a los campos de la clase de datos
  • Cómo personalizar el orden de clase de datos de objetos
  • cómo trabajar con inmutable clases de datos
  • ¿Cómo funciona la herencia de clases de datos

Si desea sumergirse en todos los detalles de las clases de datos, echar un vistazo a PEP 557, así como las discusiones en el repositorio GitHub originales.

Además, de Raymond Hettinger PyCon 2018 Dataclasses de discusión: El generador de código para poner fin a todos los generadores de código es bien vale la pena ver.

Si aún no tienes Python 3.7, también hay datos de clases acondicionarlo para Python 3.6. Y ahora, seguir adelante y escribir menos código!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *