Extending the YAML representation

Sometimes, one of our classes has a tidy representation that is nicer than the default YAML dump of attribute values. For example, the default YAML for our Blackjack Card class definitions will include several derived values that we don't really need to preserve.

The yaml module includes a provision for adding a representer and constructor to a class definition. The representer is used to create a YAML representation, including a tag and value. The constructor is used to build a Python object from the given value. Here's yet another Card class hierarchy:

from enum import Enum
class Suit(str, Enum):
Clubs = "♣"
Diamonds = "♦"
Hearts = "♥"
Spades = "♠"

class Card:

def __init__(self, rank: str, suit: Suit,
hard: Optional[int]=None,
soft: Optional[
int]=None
) -> None:
self.rank = rank
self.suit = suit
self.hard = hard or int(rank)
self.soft = soft or int(rank)

def __str__(self) -> str:
return f"{self.rank!s}{self.suit.value!s}"

class AceCard(Card):

def __init__(self, rank: str, suit: Suit) -> None:
super().__init__(rank, suit, 1, 11)


class FaceCard(Card):

def __init__(self, rank: str, suit: Suit) -> None:
super().__init__(rank, suit, 10, 10)

We've used the superclass, Card, for number cards and defined two subclasses, AceCard and FaceCard, for aces and face cards. In previous examples, we made extensive use of a factory function to simplify the construction. The factory handled mapping from a rank of 1 to a class of AceCard, and from ranks of 11, 12, and 13 to a class of FaceCard. This was essential so that we could easily build a deck using a simple range(1,14) for the rank values.

When loading from YAML, the class will be fully spelled out via the YAML !! tags. The only missing information would be the hard and soft values associated with each subclass of the card. The hard and soft points have three relatively simple cases that can be handled through optional initialization parameters. Here's how it looks when we dump these objects into the YAML format using default serialization:

- !!python/object:Chapter_10.ch10_ex2.AceCard
hard: 1
rank: A
soft: 11
suit: !!python/object/apply:Chapter_10.ch10_ex2.Suit
- ♣
- !!python/object:Chapter_10.ch10_ex2.Card
hard: 2
rank: '2'
soft: 2
suit: !!python/object/apply:Chapter_10.ch10_ex2.Suit
- ♥
- !!python/object:Chapter_10.ch10_ex2.FaceCard
hard: 10
rank: K
soft: 10
suit: !!python/object/apply:Chapter_10.ch10_ex2.Suit
- ♦

These are correct, but perhaps a bit wordy for something as simple as a playing card. We can extend the yaml module to produce smaller and more focused output for these simple objects. Let's define representer and constructor for our Card subclasses. Here are the three functions and registrations:

def card_representer(dumper: Any, card: Card) -> str:
return dumper.represent_scalar(
"!Card", f"{card.rank!s}{card.suit.value!s}")


def acecard_representer(dumper: Any, card: Card) -> str:
return dumper.represent_scalar(
"!AceCard", f"{card.rank!s}{card.suit.value!s}")


def facecard_representer(dumper: Any, card: Card) -> str:
return dumper.represent_scalar(
"!FaceCard", f"{card.rank!s}{card.suit.value!s}")

yaml.add_representer(Card, card_representer)
yaml.add_representer(AceCard, acecard_representer)
yaml.add_representer(FaceCard, facecard_representer)

We've represented each Card instance as a short string. YAML includes a tag to show which class should be built from the string. All three classes use the same format string. This happens to match the __str__() method, leading to a potential optimization.

The other problem we need to solve is constructing Card instances from the parsed YAML document. For that, we need constructors. Here are three constructors and the registrations:

def card_constructor(loader: Any, node: Any) -> Card:
value = loader.construct_scalar(node)
rank, suit = value[:-1], value[-1]
return Card(rank, suit)


def acecard_constructor(loader: Any, node: Any) -> Card:
value = loader.construct_scalar(node)
rank, suit = value[:-1], value[-1]
return AceCard(rank, suit)


def facecard_constructor(loader: Any, node: Any) -> Card:
value = loader.construct_scalar(node)
rank, suit = value[:-1], value[-1]
return FaceCard(rank, suit)

yaml.add_constructor("!Card", card_constructor)
yaml.add_constructor("!AceCard", acecard_constructor)
yaml.add_constructor("!FaceCard", facecard_constructor)

As a scalar value is parsed, the tag will be used to locate a specific constructor. The constructor can then decompose the string and build the proper subclass of a Card instance. Here's a quick demo that dumps one card of each class:

deck = [AceCard("A", Suit.Clubs), Card("2", Suit.Hearts), FaceCard("K", Suit.Diamonds)]
text = yaml.dump(deck, allow_unicode=True)

The following is the output:

- !AceCard 'A♣'
- !Card '2♥'
- !FaceCard 'K♦'

This gives us short, elegant YAML representations of cards that can be used to reconstruct Python objects.

We can rebuild our three-card deck using the following statement:

yaml.load(text, Loader=yaml.Loader)

This will parse the representation, use the constructor functions, and build the expected objects. Because the constructor function ensures that proper initialization gets done, the internal attributes for the hard and soft values are properly rebuilt.

It's essential to use a specific Loader when adding new constructors to the yaml module. The default behavior is to ignore these additional constructor tags. When we want to use them, we need to provide a Loader that will handle extension tags.

Let's take a look at security and safe loading in the next section.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset