Creating properties

A property is a method function that appears (syntactically) to be a simple attribute. We can get, set, and delete property values with syntax identical to the syntax for attribute values. There's an important distinction, though. A property is actually a method and can process, rather than simply preserve, a reference to another object.

Besides the level of sophistication, one other difference between properties and attributes is that we can't attach new properties to an existing object easily, but we can add dynamic attributes to an object very easily. A property is not identical to a simple attribute in this one respect.

There are two ways to create properties. We can use the @property decorator, or we can use the property() function. The differences are purely syntactic. We'll focus on the decorator.

We'll now take a look at two basic design patterns for properties:

  • Eager calculation: In this design pattern, when we set a value via a property, other attributes are also computed.
  • Lazy calculation: In this design pattern, calculations are deferred until requested via a property.

In order to compare the preceding two approaches to properties, we'll split some common features of the Hand object into an abstract superclass, as follows:

class Hand:

def __init__(
self,
dealer_card: BlackJackCard,
*cards: BlackJackCard
) -> None:
self.dealer_card = dealer_card
self._cards = list(cards)

def __str__(self) -> str:
return ", ".join(map(str, self.card))

def __repr__(self) -> str:
return (
f"{self.__class__.__name__}"
f"({self.dealer_card!r}, "
f"{', '.join(map(repr, self.card))})"
)

In the preceding code, we defined the object initialization method, which actually does nothing. There are two string representation methods provided. This class is a wrapper around an internal list of cards, kept in an instance variable, _cards. We've used a leading _ on the instance variable as a reminder that this is an implementation detail that may change.

The __init__() is used to provide instance variable names and type hints for mypy. An attempt to use None as a default in this kind of abstract class definition will violate the type hints. The dealer_card attribute must be an instance of BlackJackCard. To allow this variable to have an initial value of None, the type hint would have to be Optional[BlackJackCard], and all references to this variable would also require a guarding if statement to be sure the value was not None.

The following is a subclass of Hand, where total is a lazy property that is computed only when needed:

class Hand_Lazy(Hand):

@property
def total(self) -> int:
delta_soft = max(c.soft - c.hard for c in self._cards)
hard_total = sum(c.hard for c in self._cards)
if hard_total + delta_soft <= 21:
return hard_total + delta_soft
return hard_total

@property
def card(self) -> List[BlackJackCard]:
return self._cards

@card.setter
def card(self, aCard: BlackJackCard) -> None:
self._cards.append(aCard)

@card.deleter
def card(self) -> None:
self._cards.pop(-1)

The Hand_Lazy class sets the dealer_card and the _cards instance variables. The total property is based on a method that computes the total only when requested. Additionally, we defined some other properties to update the collection of cards in the hand. The card property can get, set, or delete cards in the hand. We'll take a look at these properties in the setter and deleter properties section.

We can create a Hand_Lazy object. total appears to be a simple attribute:

>>> d = Deck() 
>>> h = Hand_Lazy(d.pop(), d.pop(), d.pop()) 
>>> h.total 
19 
>>> h.card = d.pop() 
>>> h.total 
29 

The total is computed lazily by rescanning the cards in the hand each time the total is requested. For the simple BlackJackCard instances, this is a relatively inexpensive computation. For other kinds of items, this can involve considerable overhead.

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

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