Anda di halaman 1dari 38

Mastering Object-oriented Python

Steven F. Lott

Chapter No. 1 "The __init__() Method"

In this package, you will find:


A Biography of the author of the book A preview chapter from the book, Chapter NO.1 "The __init__() Method" A synopsis of the books content Information on where to buy this book

About the Author


Steven F. Lott has been programming since the 70s, when computers were large, expensive, and rare. As a contract software developer and architect, he has worked on hundreds of projects from very small to very large. He's been using Python to solve business problems for over 10 years. Steven is currently a technomad who lives in various places on the east coast of the US. His technology blog is: http://slott-softwarearchitect.blogspot.com I owe deep gratitude to Floating Leaf for all her support and guidance.

For More Information: www.packtpub.com/mastering-object-oriented-python/book

Mastering Object-oriented Python


This book will introduce you to more advanced features of the Python programming language. The focus is on creating the highest quality Python programs possible. This often means creating programs that have the highest performance or are the most maintainable. This means exploring design alternatives and determining which design offers the best performance while still being a good fit with the problem that is being solved. Most of the book will look at a number of alternatives for a given design. Some will have better performance. Some will seem simpler or be a better solution for the problem domain. It's essential to locate the best algorithms and optimal data structures to create the most value with the least computer processing. Time is money, and programs that save time will create more value for their users. Python makes a number of internal features directly available to our application programs. This means that our programs can be very tightly integrated with existing Python features. We can leverage numerous Python features by ensuring that our OO designs integrate well. We'll often focus on a specific problem and examine several variant solutions to the problem. As we look at different algorithms and data structures, we'll see different memory and performance alternatives. It's an important OO design skill to work through alternate solutions in order to properly optimize the final application. One of the more important themes of this book is that there's no single best approach to any problem. There are a number of alternative approaches with different attributes. On programming style, the subject of style generates a surprising amount of interest. The astute reader will note that the examples do not meticulously conform to PEP-8 in every single particular detail of the name choice or punctuation. As we move towards achieving mastery over object-oriented Python, we'll spend a great deal of time reading Python code from a variety of sources. We'll observe wide variability even within the Python Standard Library modules. Rather than presenting examples that are all perfectly consistent, we've opted for some inconsistency, the lack of consistency will better confirm with code as seen in the various open source projects encountered in the wild.

For More Information: www.packtpub.com/mastering-object-oriented-python/book

What This Book Covers


We'll cover three advanced Python topics in a series of chapters that dig into the details. Some Preliminaries, covers some preliminary topics, such as unittest, doctest, docstrings, and some special method names.

Part 1, Pythonic Classes via Special Methods: This part looks more deeply at objectoriented programming techniques and how we can more tightly integrate the class definitions of our applications with Python's built-in features. It consists of nine chapters, which are as follows: Chapter 1, The _init_() Method, provides us with a detailed description and implementation of the _init_() method. We will look at different forms of initialization for simple objects. From this, we can look into more complex objects that involve collections and containers. Chapter 2, Integrating Seamlessly with Python Basic Special Methods, will explain in detail as to how we can expand a simple class definition to add special methods. We'll need to take a look at the default behavior inherited from the object so that we can understand what overrides are needed and when they're actually needed. Chapter 3, Attribute Access, Properties, and Descriptors, shows us how the default processing works in some detail. We need to decide where and when to override the default behavior. We will also explore descriptors and gain a much deeper understanding on how Python's internals work. Chapter 4, The ABCs of Consistent Design, looks at the abstract base classes in the collections.abc module in general. We'll look at the general concepts behind the various containers and collections that we might want to revise or extend. Similarly, we'll look at the concepts behind the numbers that we might want to implement. Chapter 5, Using Callables and Contexts, looks at several ways to create context managers using the tools in contextlib. We'll show you a number of variant designs for callable objects. This will show you why a stateful callable object is sometimes more useful than a simple function. We'll also take a look at how to use some of the existing Python context managers before we dive in and write our own context manager.

For More Information: www.packtpub.com/mastering-object-oriented-python/book

Chapter 6, Creating Containers and Collections, focuses on the basics of container classes. We'll review the variety of special methods that are involved in being a container and offering the various features that containers offer. We'll address extending built-in containers to add features. We'll also look at wrapping built-in containers and delegating methods through the wrapper to the underlying container. Chapter 7, Creating Numbers, covers these essential arithmetic operators: +, -, *, /, //, %, and **. We'll also take a look at these comparison operators: <, >, <=, >=, ==, and !=. We'll finish by summarizing some of the design considerations that go into extending or creating new numbers. Chapter 8, Decorators and Mixins Cross-cutting Aspects, covers simple function decorators, function decorators with arguments, class decorators, and method decorators.

Part 2, Persistence and Serialization: A persistent object has been serialized to a storage medium. Perhaps it's transformed to JSON and written to the filesystem. An ORM layer can store the object in a database. This part will take a look at the alternatives to handle persistence. This section contains five chapters, which are as follows: Chapter 9, Serializing and Saving JSON, YAML, Pickle, CSV, and XML, covers simple persistence using libraries focused on various data representations such as JSON, YAML, pickle, XML, and CSV. Chapter 10, Storing and Retrieving Objects via Shelve, explains basic database operations with Python modules, such as shelve (and dbm). Chapter 11, Storing and Retrieving Objects via SQLite, moves to the more complex world of SQL and the relational database. Because SQL features don't match object-oriented programming features well, we have an impedance mismatch problem. A common solution is to use ORM to allow us to persist a large domain of objects. Chapter 12, Transmitting and Sharing Objects, takes a look at the HTTP protocol, JSON, YAML, and XML representation to transmit an object. Chapter 13, Configuration Files and Persistence, covers various ways in which a Python application can work with a configuration file.

Part 3, Testing, Debugging, Deploying, and Maintaining: We'll show you how to gather data to support and debug high-performance programs. This will include information on creating the best possible documentation in order to reduce the confusion and complexity of the support. This section contains the final five chapters, which are as follows:

For More Information: www.packtpub.com/mastering-object-oriented-python/book

Chapter 14, The Logging and Warning Modules, takes a look at using the logging and warning modules to create audit information, as well as debug. We'll take a significant step beyond using the print() function. Chapter 15, Designing for Testability, covers designing for testability and how we use unittest and doctest. Chapter 16, Coping with the Command Line, takes a look at using the argparse module to parse options and arguments. We'll take this a step further and use the command design pattern to create program components that can be combined and expanded without resorting to writing shell scripts. Chapter 17, The Module and Package Design, covers module and package design. This is a higher-level set of considerations. We will take a look at related classes in a module and related modules in a package. Chapter 18, Quality and Documentation, covers how we can document our design to create trust that our software is correct and has been properly implemented.

For More Information: www.packtpub.com/mastering-object-oriented-python/book

The __init__() Method


The __init__() method is profound for two reasons. Initialization is the rst big step in an object's life; every object must be initialized properly to work properly. The second reason is that the argument values for __init__() can take on many forms. Because there are so many ways to provide argument values to __init__(), there is a vast array of use cases for object creation. We take a look at several of them. We want to maximize clarity, so we need to dene an initialization that properly characterizes the problem domain. Before we can get to the __init__() method, however, we need to take a look at the implicit class hierarchy in Python, glancing, briey, at the class named object. This will set the stage for comparing default behavior with the different kinds of behavior we want from our own classes. In this chapter, we take a look at different forms of initialization for simple objects (for example, playing cards). After this, we can take a look at more complex objects, such as hands that involve collections and players that involve strategies and states.

The implicit superclass object


Each Python class denition has an implicit superclass: object. It's a very simple class denition that does almost nothing. We can create instances of object, but we can't do much with them because many of the special methods simply raise exceptions. When we dene our own class, object is the superclass. The following is an example class denition that simply extends object with a new name:
class X: pass

For More Information: www.packtpub.com/mastering-object-oriented-python/book

The __init__() Method

The following are some interactions with our class:


>>> X.__class__ <class 'type'> >>> X.__class__.__base__ <class 'object'>

We can see that a class is an object of the class named type and that the base class for our new class is the class named object. As we look at each method, we also take a look at the default behavior inherited from object. In some cases, the superclass special method behavior will be exactly what we want. In other cases, we'll need to override the special method.

The base class object __init__() method


Fundamental to the life cycle of an object are its creation, initialization, and destruction. We'll defer creation and destruction to a later chapter on more advanced special methods and only focus on initialization for now. The superclass of all classes, object, has a default implementation of __init__() that amounts to pass. We aren't required to implement __init__(). If we don't implement it, then no instance variables will be created when the object is created. In some cases, this default behavior is acceptable. We can always add attributes to an object that's a subclass of the foundational base class, object. Consider the following class that requires two instance variables but doesn't initialize them:
class Rectangle: def area( self ): return self.length * self.width

The Rectangle class has a method that uses two attributes to return a value. The attributes have not been initialized anywhere. This is legal Python. It's a little strange to avoid specically setting attributes, but it's valid. The following is an interaction with the Rectangle class:
>>> r= Rectangle() >>> r.length, r.width = 13, 8 >>> r.area() 104

While this is legal, it's a potential source of deep confusion, which is a good reason to avoid it.
[ 26 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

Chapter 1

However, this kind of design grants exibility, so there could be times when we needn't set all of the attributes in the __init__() method. We walk a ne line here. An optional attribute is a kind of subclass that's not formally declared as a proper subclass. We're creating polymorphism in a way that could lead to confusing and inappropriate use of convoluted if statements. While uninitialized attributes may be useful, they could be the symptom of a bad design. The Zen of Python poem (import this) offers the following advice: "Explicit is better than implicit." An __init__() method should make the instance variables explicit.
Pretty Poor Polymorphism There's a ne line between exibility and foolishness. We may have stepped over the edge off exible into foolish as soon as we feel the need to write:
if 'x' in self.__dict__:

Or:
try: self.x except AttributeError:

It's time to reconsider the API and add a common method or attribute. Refactoring is better than adding if statements.

Implementing __init__() in a superclass


We initialize an object by implementing the __init__() method. When an object is created, Python rst creates an empty object and then calls the __init__() method for that new object. This method function generally creates the object's instance variables and performs any other one-time processing. The following are some example denitions of a Card class hierarchy. We'll dene a Card superclass and three subclasses that are variations of the basic theme of Card. We have two instance variables that have been set directly from argument values and two variables that have been calculated by an initialization method:
class Card: def __init__( self, rank, suit ): self.suit= suit self.rank= rank self.hard, self.soft = self._points() class NumberCard( Card ): [ 27 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

The __init__() Method def _points( self ): return int(self.rank), int(self.rank) class AceCard( Card ): def _points( self ): return 1, 11 class FaceCard( Card ): def _points( self ): return 10, 10

In this example, we factored the __init__() method into the superclass so that a common initialization in the superclass, Card, applies to all the three subclasses NumberCard, AceCard, and FaceCard. This shows a common polymorphic design. Each subclass provides a unique implementation of the _points() method. All the subclasses have identical signatures: they have the same methods and attributes. Objects of these three subclasses can be used interchangeably in an application. If we simply use characters for suits, we will be able to create Card instances as shown in the following code snippet:
cards = [ AceCard('A', ''), NumberCard('2',''), NumberCard('3',''), ]

We enumerated the class, rank, and suit for several cards in a list. In the long run, we need a much smarter factory function to build Card instances; enumerating all 52 cards this way is tedious and error prone. Before we get to the factory functions, we take a look at a number of other issues.

Using __init__() to create manifest constants


We can dene a class for the suits of our cards. In blackjack, the suits don't matter, and a simple character string could work. We use suit construction as an example of creating constant objects. In many cases, our application will have a small domain of objects that can be dened by a collection of constants. A small domain of static objects may be part of implementing a Strategy or State design pattern. In some cases, we may have a pool of constant objects created in an initialization or conguration le, or we might create constant objects based on command-line parameters. We'll return to the details of initialization design and startup design in Chapter 16, Coping with the Command Line.
[ 28 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

Chapter 1

Python has no simple formal mechanism for dening an object as immutable. We'll look at techniques to assure immutability in Chapter 3, Attribute Access, Properties, and Descriptors. In this example, it might make sense for the attributes of a suit to be immutable. The following is a class that we'll use to build four manifest constants:
class Suit: def __init__( self, name, symbol ): self.name= name self.symbol= symbol

The following is the domain of "constants" built around this class:


Club, Diamond, Heart, Spade = Suit('Club',''), Suit('Diamond',''), Suit('Heart',''), Suit('Spade','')

We can now create cards as shown in the following code snippet:


cards = [ AceCard('A', Spade), NumberCard('2', Spade), NumberCard('3', Spade), ]

For an example this small, this method isn't a huge improvement over single character suit codes. In more complex cases, there may be a short list of Strategy or State objects that can be created like this. This can make the Strategy or State design patterns work efciently by reusing objects from a small, static pool of constants. We do have to acknowledge that in Python these objects aren't technically constant; they are mutable. There may be some benet in doing the extra coding to make these objects truly immutable.
The irrelevance of immutability Immutability can become an attractive nuisance. It's sometimes justied by the mythical "malicious programmer" who modies the constant value in their application. As a design consideration, this is silly. This mythical, malicious programmer can't be stopped this way. There's no easy way to "idiot-proof" code in Python. The malicious programmer has access to the source and can tweak it just as easily as they can write code to modify a constant. It's better not to struggle too long to dene the classes of immutable objects. In Chapter 3, Attribute Access, Properties, and Descriptors, we'll show ways to implement immutability that provides suitable diagnostic information for a buggy program.

[ 29 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

The __init__() Method

Leveraging __init__() via a factory function


We can build a complete deck of cards via a factory function. This beats enumerating all 52 cards. In Python, we have two common approaches to factories as follows: We dene a function that creates objects of the required classes. We dene a class that has methods for creating objects. This is the full factory design pattern, as described in books on design patterns. In languages such as Java, a factory class hierarchy is required because the language doesn't support standalone functions.

In Python, a class isn't required. It's merely a good idea when there are related factories that are complex. One of the strengths of Python is that we're not forced to use a class hierarchy when a simple function might do just as well.
While this is a book about object-oriented programming, a function really is ne. It's common, idiomatic Python.

We can always rewrite a function to be a proper callable object if the need arises. From a callable object, we can refactor it into a class hierarchy for our factories. We'll look at callable objects in Chapter 5, Using Callables and Contexts. The advantage of class denitions in general is to achieve code reuse via inheritance. The function of a factory class is to wrap some target class hierarchy and the complexities of object construction. If we have a factory class, we can add subclasses to the factory class when extending the target class hierarchy. This gives us polymorphic factory classes; the different factory class denitions have the same method signatures and can be used interchangeably. This class-level polymorphism can be very helpful with statically compiled languages such as Java or C++. The compiler can resolve the details of the class and methods when generating code. If the alternative factory denitions don't actually reuse any code, then a class hierarchy won't be helpful in Python. We can simply use functions that have the same signatures. The following is a factory function for our various Card subclasses:
def card( rank, suit ): if rank == 1: return AceCard( 'A', suit ) elif 2 <= rank < 11: return NumberCard( str(rank), suit ) [ 30 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

Chapter 1 elif 11 <= rank < 14: name = { 11: 'J', 12: 'Q', 13: 'K' }[rank] return FaceCard( name, suit ) else: raise Exception( "Rank out of range" )

This function builds a Card class from a numeric rank number and a suit object. We can now build cards more simply. We've encapsulated the construction issues into a single factory function, allowing an application to be built without knowing precisely how the class hierarchy and polymorphic design works. The following is an example of how we can build a deck with this factory function:
deck = [card(rank, suit) for rank in range(1,14) for suit in (Club, Diamond, Heart, Spade)]

This enumerates all the ranks and suits to create a complete deck of 52 cards.

Faulty factory design and the vague else clause


Note the structure of the if statement in the card() function. We did not use a catch-all else clause to do any processing; we merely raised an exception. The use of a catch-all else clause is subject to a tiny scrap of debate. On the one hand, it can be argued that the condition that belongs on an else clause should never be left unstated because it may hide subtle design errors. On the other hand, some else clause conditions are truly obvious. It's important to avoid the vague else clause. Consider the following variant on this factory function denition:
def card2( rank, suit ): if rank == 1: return AceCard( 'A', suit ) elif 2 <= rank < 11: return NumberCard( str(rank), suit ) else: name = { 11: 'J', 12: 'Q', 13: 'K' }[rank] return FaceCard( name, suit )

The following is what will happen when we try to build a deck:


deck2 = [card2(rank, suit) for rank in range(13) for suit in (Club, Diamond, Heart, Spade)]

Does it work? What if the if conditions were more complex?


[ 31 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

The __init__() Method

Some programmers can understand this if statement at a glance. Others will struggle to determine if all of the cases are properly exclusive. For advanced Python programming, we should not leave it to the reader to deduce the conditions that apply to an else clause. Either the condition should be obvious to the newest of n00bz, or it should be explicit.
When to use catch-all else Rarely. Use it only when the condition is obvious. When in doubt, be explicit and use else to raise an exception. Avoid the vague else clause.

Simplicity and consistency using elif sequences


Our factory function, card(), is a mixture of two very common factory design patterns: An if-elif sequence A mapping

For the sake of simplicity, it's better to focus on just one of these techniques rather than on both. We can always replace a mapping with elif conditions. (Yes, always. The reverse is not true though; transforming elif conditions to a mapping can be challenging.) The following is a Card factory without the mapping:
def card3( rank, suit ): if rank == 1: return elif 2 <= rank < 11: elif rank == 11: return FaceCard( elif rank == 12: return FaceCard( elif rank == 13: return FaceCard( else: raise Exception( AceCard( 'A', suit ) return NumberCard( str(rank), suit ) 'J', suit ) 'Q', suit ) 'K', suit ) "Rank out of range" )

We rewrote the card() factory function. The mapping was transformed into additional elif clauses. This function has the advantage that it is more consistent than the previous version.
[ 32 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

Chapter 1

Simplicity using mapping and class objects


In some cases, we can use a mapping instead of a chain of elif conditions. It's possible to nd conditions that are so complex that a chain of elif conditions is the only sensible way to express them. For simple cases, however, a mapping often works better and can be easy to read. Since class is a rst-class object, we can easily map from the rank parameter to the class that must be constructed. The following is a Card factory that uses only a mapping:
def card4( rank, suit ): class_= {1: AceCard, 11: FaceCard, 12: FaceCard, 13: FaceCard}.get(rank, NumberCard) return class_( rank, suit )

We've mapped the rank object to a class. Then, we applied the class to the rank and suit values to build the nal Card instance. We can use a defaultdict class as well. However, it's no simpler for a trivial static mapping. It looks like the following code snippet:
defaultdict( lambda: NumberCard, {1: AceCard, 11: FaceCard, 12: FaceCard, 12: FaceCard} )

Note that the default of a defaultdict class must be a function of zero arguments. We've used a lambda construct to create the necessary function wrapper around a constant. This function, however, has a serious deciency. It lacks the translation from 1 to A and 13 to K that we had in previous versions. When we try to add that feature, we run into a problem. We need to change the mapping to provide both a Card subclass as well as the string version of the rank object. What can we do for this two-part mapping? There are four common solutions: We can do two parallel mappings. We don't suggest this, but we'll show it to emphasize what's undesirable about it. We can map to a two-tuple. This also has some disadvantages. We can map to a partial() function. The partial() function is a feature of the functools module. We can also consider modifying our class denition to t more readily with this kind of mapping. We'll look at this alternative in the next section on pushing __init__() into the subclass denitions.

We'll look at each of these with a concrete example.


[ 33 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

The __init__() Method

Two parallel mappings


The following is the essence of the two parallel mappings solution:
class_= {1: AceCard, 11: FaceCard, 12: FaceCard, 13: FaceCard }.get(rank, NumberCard) rank_str= {1:'A', 11:'J', 12:'Q', 13:'K'}.get(rank,str(rank)) return class_( rank_str, suit )

This is not desirable. It involves a repetition of the sequence of the mapping keys 1, 11, 12, and 13. Repetition is bad because parallel structures never seem to stay that way after the software has been updated.
Don't use parallel structures Two parallel structures should be replaced with tuples or some kind of proper collection.

Mapping to a tuple of values


The following is the essence of how mapping is done to a two-tuple:
class_, rank_str= { 1: (AceCard,'A'), 11: (FaceCard,'J'), 12: (FaceCard,'Q'), 13: (FaceCard,'K'), }.get(rank, (NumberCard, str(rank))) return class_( rank_str, suit )

This is reasonably pleasant. It's not much code to sort out the special cases of playing cards. We will see how it could be modied or expanded if we need to alter the Card class hierarchy to add additional subclasses of Card. It does feel odd to map a rank value to a class object and just one of the two arguments to that class initializer. It seems more sensible to map the rank to a simple class or function object without the clutter of providing some (but not all) of the arguments.

The partial function solution


Rather than map to a two-tuple of function and one of the arguments, we can create a partial() function. This is a function that already has some (but not all) of its arguments provided. We'll use the partial() function from the functools library to create a partial of a class with the rank argument.
[ 34 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

Chapter 1

The following is a mapping from rank to a partial() function that can be used for object construction:
from functools import partial part_class= { 1: partial(AceCard,'A'), 11: partial(FaceCard,'J'), 12: partial(FaceCard,'Q'), 13: partial(FaceCard,'K'), }.get(rank, partial(NumberCard, str(rank))) return part_class( suit )

The mapping associates a rank object with a partial() function that is assigned to part_class. This partial() function can then be applied to the suit object to create the nal object. The use of partial() functions is a common technique for functional programming. It works in this specic situation where we have a function instead of an object method. In general, however, partial() functions aren't helpful for most object-oriented programming. Rather than create partial() functions, we can simply update the methods of a class to accept the arguments in different combinations. A partial() function is similar to creating a uent interface for object construction.

Fluent APIs for factories


In some cases, we design a class where there's a dened order for method usage. Evaluating methods sequentially is very much like creating a partial() function. We might have x.a().b() in an object notation. We can think of it as x ( a, b ) . The x.a() function is a kind of partial() function that's waiting for b(). We can think of this as if it were x ( a )( b ) . The idea here is that Python offers us two alternatives for managing a state. We can either update an object or create a partial() function that is (in a way) stateful. Because of this equivalence, we can rewrite a partial() function into a uent factory object. We make the setting of the rank object a uent method that returns self. Setting the suit object will actually create the Card instance. The following is a uent Card factory class with two method functions that must be used in a specic order:
class CardFactory: def rank( self, rank ): self.class_, self.rank_str= { 1:(AceCard,'A'), [ 35 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

The __init__() Method 11:(FaceCard,'J'), 12:(FaceCard,'Q'), 13:(FaceCard,'K'), }.get(rank, (NumberCard, str(rank))) return self def suit( self, suit ): return self.class_( self.rank_str, suit )

The rank() method updates the state of the constructor, and the suit() method actually creates the nal Card object. This factory class can be used as follows:
card8 = CardFactory() deck8 = [card8.rank(r+1).suit(s) for r in range(13) for s in (Club, Diamond, Heart, Spade)]

First, we create a factory instance, then we use that instance to create Card instances. This doesn't materially change how __init__() itself works in the Card class hierarchy. It does, however, change the way that our client application creates objects.

Implementing __init__() in each subclass


As we look at the factory functions for creating Card objects, we see some alternative designs for the Card class. We might want to refactor the conversion of the rank number so that it is the responsibility of the Card class itself. This pushes the initialization down into each subclass. This often requires some common initialization of a superclass as well as subclassspecic initialization. We need to follow the Don't Repeat Yourself (DRY) principle to keep the code from getting cloned into each of the subclasses. The following is an example where the initialization is the responsibility of each subclass:
class Card: pass class NumberCard( Card ): def __init__( self, rank, suit ): self.suit= suit self.rank= str(rank) self.hard = self.soft = rank class AceCard( Card ): def __init__( self, rank, suit ): self.suit= suit [ 36 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

Chapter 1 self.rank= "A" self.hard, self.soft = 1, 11 class FaceCard( Card ): def __init__( self, rank, suit ): self.suit= suit self.rank= {11: 'J', 12: 'Q', 13: 'K' }[rank] self.hard = self.soft = 10

This is still clearly polymorphic. The lack of a truly common initialization, however, leads to some unpleasant redundancy. What's unpleasant here is the repeated initialization of suit. This must be pulled up into the superclass. We can have each __init__() subclass make an explicit reference to the superclass. This version of the Card class has an initializer at the superclass level that is used by each subclass, as shown in the following code snippet:
class Card: def __init__( self, rank, suit, hard, soft ): self.rank= rank self.suit= suit self.hard= hard self.soft= soft class NumberCard( Card ): def __init__( self, rank, suit ): super().__init__( str(rank), suit, rank, rank ) class AceCard( Card ): def __init__( self, rank, suit ): super().__init__( "A", suit, 1, 11 ) class FaceCard( Card ): def __init__( self, rank, suit ): super().__init__( {11: 'J', 12: 'Q', 13: 'K' }[rank], suit, 10, 10 )

We've provided __init__() at both the subclass and superclass level. This has the small advantage that it simplies our factory function, as shown in the following code snippet:
def card10( rank, suit ): if rank == 1: return AceCard( rank, suit ) elif 2 <= rank < 11: return NumberCard( rank, suit ) elif 11 <= rank < 14: return FaceCard( rank, suit ) else: raise Exception( "Rank out of range" )

[ 37 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

The __init__() Method

Simplifying a factory function should not be our focus. We can see from this variation that we've created rather complex __init__() methods for a relatively minor improvement in a factory function. This is a common trade-off.
Factory functions encapsulate complexity There's a trade-off that occurs between sophisticated __init__() methods and factory functions. It's often better to stick with more direct but less programmer-friendly __init__() methods and push the complexity into factory functions. A factory function works well if you wish to wrap and encapsulate the construction complexities.

Simple composite objects


A composite object can also be called a container. We'll look at a simple composite object: a deck of individual cards. This is a basic collection. Indeed, it's so basic that we can, without too much struggle, use a simple list as a deck.
list appropriate?

Before designing a new class, we need to ask this question: is using a simple

We can use random.shuffle() to shufe the deck and deck.pop() to deal cards into a player's Hand. Some programmers rush to dene new classes as if using a built-in class violates some object-oriented design principle. Avoiding a new class leaves us with something as shown in the following code snippet:
d= [card6(r+1,s) for r in range(13) for s in (Club, Diamond, Heart, Spade)] random.shuffle(d) hand= [ d.pop(), d.pop() ]

If it's that simple, why write a new class? The answer isn't perfectly clear. One advantage is that a class offer a simplied, implementation-free interface to the object. As we noted previously, when discussing factories, a class isn't a requirement in Python. In the preceding code, the deck only has two simple use cases and a class denition doesn't seem to simplify things very much. It does have the advantage of concealing the implementation's details. But the details are so trivial that exposing them seems to have little cost. We're focused primarily on the __init__() method in this chapter, so we'll look at some designs to create and initialize a collection.
[ 38 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

Chapter 1

To design a collection of objects, we have the following three general design strategies: Wrap: This design pattern is an existing collection denition. This might be an example of the Facade design pattern. Extend: This design pattern is an existing collection class. This is ordinary subclass denition. Invent: This is designed from scratch. We'll look at this in Chapter 6, Creating Containers and Collections.

These three concepts are central to object-oriented design. We must always make this choice when designing a class.

Wrapping a collection class


The following is a wrapper design that contains an internal collection:
class Deck: def __init__( self ): self._cards = [card6(r+1,s) for r in range(13) for s in (Club, Diamond, Heart, Spade)] random.shuffle( self._cards ) def pop( self ): return self._cards.pop()

We've dened Deck so that the internal collection is a list object. The pop() method of Deck simply delegates to the wrapped list object. We can then create a Hand instance with the following kind of code:
d= Deck() hand= [ d.pop(), d.pop() ]

Generally, a Facade design pattern or wrapper class contains methods that are simply delegated to the underlying implementation class. This delegation can become wordy. For a sophisticated collection, we may wind up delegating a large number of methods to the wrapped object.

Extending a collection class


An alternative to wrapping is to extend a built-in class. By doing this, we have the advantage of not having to reimplement the pop() method; we can simply inherit it. The pop() method has the advantage that it creates a class without writing too much code. In this example, extending the list class has the disadvantage that this provides many more functions than we truly need.
[ 39 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

The __init__() Method

The following is a denition of Deck that extends the built-in list:


class Deck2( list ): def __init__( self ): super().__init__( card6(r+1,s) for r in range(13) for s in (Club, Diamond, Heart, Spade) ) random.shuffle( self )

In some cases, our methods will have to explicitly use the superclass methods in order to have proper class behavior. We'll see other examples of this in the following sections. We leverage the superclass's __init__() method to populate our list object with an initial single deck of cards. Then we shufe the cards. The pop() method is simply inherited from list and works perfectly. Other methods inherited from the list also work.

More requirements and another design


In a casino, the cards are often dealt from a shoe that has half a dozen decks of cards all mingled together. This consideration makes it necessary for us to build our own version of Deck and not simply use an unadorned list object. Additionally, a casino shoe is not dealt fully. Instead, a marker card is inserted. Because of the marker, some cards are effectively set aside and not used for play. The following is Deck denition that contains multiple sets of 52-card decks:
class Deck3(list): def __init__(self, decks=1): super().__init__() for i in range(decks): self.extend( card6(r+1,s) for r in range(13) for s in (Club, Diamond, Heart, Spade) ) random.shuffle( self ) burn= random.randint(1,52) for i in range(burn): self.pop()

Here, we used the __init__() superclass to build an empty collection. Then, we used self.extend() to append multiple 52-card decks to the shoe. We could also use super().extend() since we did not provide an overriding implementation in this class.

[ 40 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

Chapter 1

We could also carry out the entire task via super().__init__() using a more deeply nested generator expression, as shown in the following code snippet:
( card6(r+1,s) for r in range(13) for s in (Club, Diamond, Heart, Spade) for d in range(decks) )

This class provides us with a collection of Card instances that we can use to emulate casino blackjack as dealt from a shoe. There's a peculiar ritual in a casino where they reveal the burned card. If we're going to design a card-counting player strategy, we might want to emulate this nuance too.

Complex composite objects


The following is an example of a blackjack Hand description that might be suitable for emulating play strategies:
class Hand: def __init__( self, dealer_card ): self.dealer_card= dealer_card self.cards= [] def hard_total(self ): return sum(c.hard for c in self.cards) def soft_total(self ): return sum(c.soft for c in self.cards)

In this example, we have an instance variable self.dealer_card based on a parameter of the __init__() method. The self.cards instance variable, however, is not based on any parameter. This kind of initialization creates an empty collection. To create an instance of Hand, we can use the following code:
d = Deck() h = Hand( d.pop() ) h.cards.append( d.pop() ) h.cards.append( d.pop() )

This has the disadvantage that a long-winded sequence of statements is used to build an instance of a Hand object. It can become difcult to serialize the Hand object and rebuild it with an initialization such as this one. Even if we were to create an explicit append() method in this class, it would still take multiple steps to initialize the collection.

[ 41 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

The __init__() Method

We could try to create a uent interface, but that doesn't really simplify things; it's merely a change in the syntax of the way that a Hand object is built. A uent interface still leads to multiple method evaluations. When we take a look at the serialization of objects in Part 2, Persistence and Serialization we'd like an interface that's a single class-level function, ideally the class constructor. We'll look at this in depth in Chapter 9, Serializing and Saving - JSON, YAML, Pickle, CSV, and XML. Note also that the hard total and soft total method functions shown here don't fully follow the rules of blackjack. We return to this issue in Chapter 2, Integrating Seamlessly with Python Basic Special Methods.

Complete composite object initialization


Ideally, the __init__() initializer method will create a complete instance of an object. This is a bit more complex when creating a complete instance of a container that contains an internal collection of other objects. It'll be helpful if we can build this composite in a single step. It's common to have both a method to incrementally accrete items as well as the initializer special method that can load all of the items in one step. For example, we might have a class such as the following code snippet:
class Hand2: def __init__( self, dealer_card, *cards ): self.dealer_card= dealer_card self.cards = list(cards) def hard_total(self ): return sum(c.hard for c in self.cards) def soft_total(self ): return sum(c.soft for c in self.cards)

This initialization sets all of the instance variables in a single step. The other methods are simply copies of the previous class denition. We can build a Hand2 object in two ways. This rst example loads one card at a time into a Hand2 object:
d = Deck() P = Hand2( d.pop() ) p.cards.append( d.pop() ) p.cards.append( d.pop() )

This second example uses the *cards parameter to load a sequence of Cards class in a single step:
d = Deck() h = Hand2( d.pop(), d.pop(), d.pop() ) [ 42 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

Chapter 1

For unit testing, it's often helpful to build a composite object in a single statement in this way. More importantly, some of the serialization techniques from the next part will benet from a way of building a composite object in a single, simple evaluation.

Stateless objects without __init__()


The following is an example of a degenerate class that doesn't need an __init__() method. It's a common design pattern for Strategy objects. A Strategy object is plugged into a Master object to implement an algorithm or decision. It may rely on data in the master object; the Strategy object may not have any data of its own. We often design strategy classes to follow the Flyweight design pattern: we avoid internal storage in the Strategy object. All values are provided to Strategy as method argument values. The Strategy object itself can be stateless. It's more a collection of method functions than anything else. In this case, we're providing the game play decisions for a Player instance. The following is an example of a (dumb) strategy to pick cards and decline the other bets:
class GameStrategy: def insurance( self, hand ): return False def split( self, hand ): return False def double( self, hand ): return False def hit( self, hand ): return sum(c.hard for c in hand.cards) <= 17

Each method requires the current Hand as an argument value. The decisions are based on the available information; that is, on the dealer's cards and the player's cards. We can build a single instance of this strategy for use by various Player instances as shown in the following code snippet:
dumb = GameStrategy()

We can imagine creating a family of related strategy classes, each one using different rules for the various decisions a player is offered in blackjack.

Some additional class denitions


As noted previously, a player has two strategies: one for betting and one for playing their hand. Each Player instance has a sequence of interactions with a larger simulation engine. We'll call the larger engine the Table class.
[ 43 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

The __init__() Method

The Table class requires the following sequence of events by the Player instances: The player must place an initial bet based on the betting strategy. The player will then receive a hand. If the hand is splittable, the player must decide to split or not based on the play strategy. This can create additional Hand instances. In some casinos, the additional hands are also splittable. For each Hand instance, the player must decide to hit, double, or stand based on the play strategy. The player will then receive payouts, and they must update their betting strategy based on their wins and losses.

From this, we can see that the Table class has a number of API methods to receive a bet, create a Hand object, offer a split, resolve each hand, and pay off the bets. This is a large object that tracks the state of play with a collection of Players. The following is the beginning of a Table class that handles the bets and cards:
class Table: def __init__( self ): self.deck = Deck() def place_bet( self, amount ): print( "Bet", amount ) def get_hand( self ): try: self.hand= Hand2( d.pop(), d.pop(), d.pop() ) self.hole_card= d.pop() except IndexError: # Out of cards: need to shuffle. self.deck= Deck() return self.get_hand() print( "Deal", self.hand ) return self.hand def can_insure( self, hand ): return hand.dealer_card.insure

The Table class is used by the Player class to accept a bet, create a Hand object, and determine if theinsurance bet is in play for this hand. Additional methods can be used by the Player class to get cards and determine the payout. The exception handling shown in get_hand() is not a precise model of casino play. This may lead to minor statistical inaccuracies. A more accurate simulation requires developing a deck that reshufes itself when empty instead of raising an exception.
[ 44 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

Chapter 1

In order to interact properly and simulate realistic play, the Player class needs a betting strategy. The betting strategy is a stateful object that determines the level of the initial bet. The various betting strategies generally change the bet based on whether the game was a win or a loss. Ideally, we'd like to have a family of betting strategy objects. Python has a module with decorators that allows us to create an abstract superclass. An informal approach to creating Strategy objects is to raise an exception for methods that must be implemented by a subclass. We've dened an abstract superclass as well as a specic subclass as follows to dene a at betting strategy:
class BettingStrategy: def bet( self ): raise NotImplementedError( "No bet method" ) def record_win( self ): pass def record_loss( self ): pass class Flat(BettingStrategy): def bet( self ): return 1

The superclass denes the methods with handy default values. The basic bet() method in the abstract superclass raises an exception. The subclass must override the bet() method. The other methods can be left to provide the default values. Given the game strategy in the previous section plus the betting strategy here, we can look at more complex __init__() techniques surrounding the Player class. We can make use of the abc module to formalize an abstract superclass denition. It would look like the following code snippet:
import abc class BettingStrategy2(metaclass=abc.ABCMeta): @abstractmethod def bet( self ): return 1 def record_win( self ): pass def record_loss( self ): pass

[ 45 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

The __init__() Method

This has the advantage that it makes the creation of an instance of BettingStrategy2, or any subclass that failed to implement bet(), impossible. If we try to create an instance of this class with an unimplemented abstract method, it will raise an exception instead of creating an object.
super().bet().

And yes, the abstract method has an implementation. It can be accessed via

Multi-strategy __init__()
We may have objects that are created from a variety of sources. For example, we might need to clone an object as part of creating a memento, or freeze an object so that it can be used as the key of a dictionary or placed into a set; this is the idea behind the set and frozenset built-in classes. There are several overall design patterns that have multiple ways to build an object. One design pattern is complex __init__() that is called multi-strategy initialization. Also, there are multiple class-level (static) constructor methods. These are incompatible approaches. They have radically different interfaces.
Avoid clone methods A clone method that unnecessarily duplicates an object is rarely needed in Python. Using cloning may be an indication of failure to understand the object-oriented design principles available in Python. A clone method encapsulates the knowledge of object creation in the wrong place. The source object that's being cloned cannot know about the structure of the target object that was built from the clone. However, the reverse (targets having knowledge about a source) is acceptable if the source provides a reasonably well-encapsulated interface.

The examples we have shown here are effectively cloning because they're so simple. We'll expand on them in the next chapter. However, to show ways in which these fundamental techniques are used to do more than trivial cloning, we'll look at turning a mutable Hand object into a frozen, immutable Hand object. The following is an example of a Hand object that can be built in either of the two ways:
class Hand3: def __init__( self, *args, **kw ): if len(args) == 1 and isinstance(args[0],Hand3): # Clone an existing hand; often a bad idea [ 46 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

Chapter 1 other= args[0] self.dealer_card= other.dealer_card self.cards= other.cards else: # Build a fresh, new hand. dealer_card, *cards = args self.dealer_card= dealer_card self.cards= list(cards)

In the rst case, a Hand3 instance has been built from an existing Hand3 object. In the second case, a Hand3 object has been built from individual Card instances. This parallels the way a frozenset object can be built from individual items or an existing set object. We look more at creating immutable objects in the next chapter. Creating a new Hand from an existing Hand allows us to create a memento of a Hand object using a construct like the following code snippet:
h = Hand( deck.pop(), deck.pop(), deck.pop() ) memento= Hand( h )

We saved the Hand object in the memento variable. This can be used to compare the nal with the original hand that was dealt, or we can freeze it for use in a set or mapping too.

More complex initialization alternatives


In order to write a multi-strategy initialization, we're often forced to give up on specic named parameters. This design has the advantage that it is exible, but the disadvantage that it has opaque, meaningless parameter names. It requires a great deal of documentation explaining the variant use cases. We can expand our initialization to also split a Hand object. The result of splitting a Hand object is simply another constructor. The following code snippet shows how the splitting of a Hand object might look:
class Hand4: def __init__( self, *args, **kw ): if len(args) == 1 and isinstance(args[0],Hand4): # Clone an existing handl often a bad idea other= args[0] self.dealer_card= other.dealer_card self.cards= other.cards elif len(args) == 2 and isinstance(args[0],Hand4) and 'split' in kw: # Split an existing hand other, card= args [ 47 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

The __init__() Method self.dealer_card= other.dealer_card self.cards= [other.cards[kw['split']], card] elif len(args) == 3: # Build a fresh, new hand. dealer_card, *cards = args self.dealer_card= dealer_card self.cards= list(cards) else: raise TypeError( "Invalid constructor args={0!r} kw={1!r}".format(args, kw) ) def __str__( self ): return ", ".join( map(str, self.cards) )

This design involves getting extra cards to build proper, split hands. When we create one Hand4 object from another Hand4 object, we provide a split keyword argument that uses the index of the Card class from the original Hand4 object. The following code snippet shows how we'd use this to split a hand:
d = Deck() h = Hand4( d.pop(), d.pop(), d.pop() ) s1 = Hand4( h, d.pop(), split=0 ) s2 = Hand4( h, d.pop(), split=1 )

We created an initial h instance of Hand4 and split it into two other Hand4 instances, s1 and s2, and dealt an additional Card class into each. The rules of blackjack only allow this when the initial hand has two cards of equal rank. While this __init__() method is rather complex, it has the advantage that it can parallel the way in which fronzenset is created from an existing set. The disadvantage is that it needs a large docstring to explain all these variations.

Initializing static methods


When we have multiple ways to create an object, it's sometimes more clear to use static methods to create and return instances rather than complex __init__() methods. It's also possible to use class methods as alternate initializers, but there's little tangible advantage to receiving the class as an argument to the method. In the case of freezing or splitting a Hand object, we might want to create two new static methods to freeze or split a Hand object. Using static methods as surrogate constructors is a tiny syntax change in construction, but it has huge advantages when organizing the code.

[ 48 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

Chapter 1

The following is a version of Hand with static methods that can be used to build new instances of Hand from an existing Hand instance:
class Hand5: def __init__( self, dealer_card, *cards ): self.dealer_card= dealer_card self.cards = list(cards) @staticmethod def freeze( other ): hand= Hand5( other.dealer_card, *other.cards ) return hand @staticmethod def split( other, card0, card1 ): hand0= Hand5( other.dealer_card, other.cards[0], card0 ) hand1= Hand5( other.dealer_card, other.cards[1], card1 ) return hand0, hand1 def __str__( self ): return ", ".join( map(str, self.cards) )

One method freezes or creates a memento version. The other method splits a Hand5 instance to create two new child instances of Hand5. This is considerably more readable and preserves the use of the parameter names to explain the interface. The following code snippet shows how we can split a Hand5 instance with this version of the class:
d = Deck() h = Hand5( d.pop(), d.pop(), d.pop() ) s1, s2 = Hand5.split( h, d.pop(), d.pop() )

We created an initial h instance of Hand5, split it into two other hands, s1 and s2, and dealt an additional Card class into each. The split() static method is much simpler than the equivalent functionality implemented via __init__(). However, it doesn't follow the pattern of creating a fronzenset object from an existing set object.

Yet more __init__() techniques


We'll take a look at a few other, more advanced __init__() techniques. These aren't quite so universally useful as the techniques in the previous sections. The following is a denition for the Player class that uses two strategy objects and a table object. This shows an unpleasant-looking __init__() method:
class Player: [ 49 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

The __init__() Method def __init__( self, table, bet_strategy, game_strategy ): self.bet_strategy = bet_strategy self.game_strategy = game_strategy self.table= table def game( self ): self.table.place_bet( self.bet_strategy.bet() ) self.hand= self.table.get_hand() if self.table.can_insure( self.hand ): if self.game_strategy.insurance( self.hand ): self.table.insure( self.bet_strategy.bet() ) # Yet more... Elided for now

The __init__() method for Player seems to do little more than bookkeeping. We're simply transferring named parameters to same-named instance variables. If we have numerous parameters, simply transferring the parameters into the internal variables will amount to a lot of redundant-looking code. We can use this Player class (and related objects) as follows:
table = Table() flat_bet = Flat() dumb = GameStrategy() p = Player( table, flat_bet, dumb ) p.game()

We can provide a very short and very exible initialization by simply transferring keyword argument values directly into the internal instance variables. The following is a way to build a Player class using keyword argument values:
class Player2: def __init__( self, **kw ): """Must provide table, bet_strategy, game_strategy.""" self.__dict__.update( kw ) def game( self ): self.table.place_bet( self.bet_strategy.bet() ) self.hand= self.table.get_hand() if self.table.can_insure( self.hand ): if self.game_strategy.insurance( self.hand ): self.table.insure( self.bet_strategy.bet() ) # etc.

This sacrices a great deal of readability for succinctness. It crosses over into a realm of potential obscurity.

[ 50 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

Chapter 1

Since the __init__() method is reduced to one line, it removes a certain level of "wordiness" from the method. This wordiness, however, is transferred to each individual object constructor expression. We have to add the keywords to the object initialization expression since we're no longer using positional parameters, as shown in the following code snippet:
p2 = Player2( table=table, bet_strategy=flat_bet, game_strategy=dumb )

Why do this? It does have a potential advantage. A class dened like this is quite open to extension. We can, with only a few specic worries, supply additional keyword parameters to a constructor. The following is the expected use case:
>>> p1= Player2( table=table, bet_strategy=flat_bet, game_ strategy=dumb) >>> p1.game()

The following is a bonus use case:


>>> p2= Player2( table=table, bet_strategy=flat_bet, game_ strategy=dumb, log_name="Flat/Dumb" ) >>> p2.game()

We've added a log_name attribute without touching the class denition. This can be used, perhaps, as part of a larger statistical analysis. The Player2.log_name attribute can be used to annotate logs or other collected data. We are limited in what we can add; we can only add parameters that fail to conict with the names already in use within the class. Some knowledge of the class implementation is required to create a subclass that doesn't abuse the set of keywords already in use. Since the **kw parameter provides little information, we need to read carefully. In most cases, we'd rather trust the class to work than review the implementation details. This kind of keyword-based initialization can be done in a superclass denition to make it slightly simpler for the superclass to implement subclasses. We can avoiding writing an additional __init__() method in each subclass when the unique feature of the subclass involves simple new instance variables. The disadvantage of this is that we have obscure instance variables that aren't formally documented via a subclass denition. If it's only one small variable, an entire subclass might be too much programming overhead to add a single variable to a class. However, one small variable often leads to a second and a third. Before long, we'll realize that a subclass would have been smarter than an extremely exible superclass.
[ 51 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

The __init__() Method

We can (and should) hybridize this with a mixed positional and keyword implementation as shown in the following code snippet:
class Player3( Player ): def __init__( self, table, bet_strategy, game_strategy, **extras ): self.bet_strategy = bet_strategy self.game_strategy = game_strategy self.table= table self.__dict__.update( extras )

This is more sensible than a completely open denition. We've made the required parameters positional parameters. We've left any nonrequired parameters as keywords. This claries the use of any extra keyword arguments given to the __ init__() method. This kind of exible, keyword-based initialization depends on whether we have relatively transparent class denitions. This openness to change requires some care to avoid debugging name clashes because the keyword parameter names are open-ended.

Initialization with type validation


Type validation is rarely a sensible requirement. In a way, this might be a failure to fully understand Python. The notional objective is to validate that all of the arguments are of a proper type. The issue with trying to do this is that the denition of proper is often far too narrow to be truly useful. This is different from validating that objects meet other criteria. Numeric range checking, for example, may be essential to prevent innite loops. What can create problems is trying to do something like the following in an __init__() method:
class ValidPlayer: def __init__( self, table, bet_strategy, game_strategy ): assert isinstance( table, Table ) assert isinstance( bet_strategy, BettingStrategy ) assert isinstance( game_strategy, GameStrategy ) self.bet_strategy = bet_strategy self.game_strategy = game_strategy self.table= table

The isinstance() method checks circumvent Python's normal duck typing.


[ 52 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

Chapter 1

We write a casino game simulation in order to experiment with endless variations on GameStrategy. These are so simple (merely four methods) that there's little real benet from inheritance from the superclass. We could dene the classes independently, lacking an overall superclass. The initialization error-checking shown in this example would force us to create subclasses merely to pass the error check. No usable code is inherited from the abstract superclass. One of the biggest duck typing issues surrounds numeric types. Different numeric types will work in different contexts. Attempts to validate the types of arguments may prevent a perfectly sensible numeric type from working properly. When attempting validation, we have the following two choices in Python: We write validation so that a relatively narrow collection of types is permitted, and someday the code will break because a new type that would have worked sensibly is prohibited We eschew validation so that a broad collection of types is permitted, and someday the code will break because a type that would not work sensibly was used

Note that both are essentially the same. The code could perhaps break someday. It either breaks because a type was prevented from being used even though it's sensible or a type that's not really sensible was used.
Just allow it Generally, it's considered better Python style to simply permit any type of data to be used. We'll return to this in Chapter 4, The ABCs of Consistent Design.

The question is this: why restrict potential future use cases? And the usual answer is that there's no good reason to restrict potential future use cases. Rather than prevent a sensible, but possibly unforeseen, use case, we can provide documentation, testing, and debug logging to help other programmers understand any restrictions on the types that can be processed. We have to provide the documentation, logging, and test cases anyway, so there's minimal additional work involved. The following is an example docstring that provides the expectations of the class:
class Player: def __init__( self, table, bet_strategy, game_strategy ): [ 53 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

The __init__() Method """Creates a new player associated with a table, and configured with proper betting and play strategies :param table: an instance of :class:`Table` :param bet_strategy: an instance of :class:`BettingStrategy` :param game_strategy: an instance of :class:`GameStrategy` """ self.bet_strategy = bet_strategy self.game_strategy = game_strategy self.table= table

The programmer using this class has been warned about what the type restrictions are. The use of other types is permitted. If the type isn't compatible with the expected type, then things will break. Ideally, we'll use too like unittest or doctest to uncover the breakage.

Initialization, encapsulation, and privacy


The general Python policy regarding privacy can be summed up as follows: we're all adults here. Object-oriented design makes an explicit distinction between interface and implementation. This is a consequence of the idea of encapsulation. A class encapsulates a data structure, an algorithm, an external interface, or something meaningful. The idea is to have the capsule separate the class-based interface from the implementation details. However, no programming language reects every design nuance. Python, typically, doesn't implement all design considerations as explicit code. One aspect of a class design that is not fully carried into code is the distinction between the private (implementation) and public (interface) methods or attributes of an object. The notion of privacy in languages that support it (C++ or Java are two examples) is already quite complex. These languages include settings such as private, protected, and public as well as "not specied", which is a kind of semiprivate. The private keyword is often used incorrectly, making subclass denition needlessly difcult. Python's notion of privacy is simple, as follows: It's all essentially public. The source code is available. We're all adults. Nothing can be truly hidden.

[ 54 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

Chapter 1

Conventionally, we'll treat some names in a way that's less public. They're generally implementation details that are subject to change without notice, but there's no formal notion of private.

Names that begin with _ are honored as less public by some parts of Python. The help() function generally ignores these methods. Tools such as Sphinx can conceal these names from documentation. Python's internal names begin (and end) with __. This is how Python internals are kept from colliding with application features above the internals. The collection of these internal names is fully dened by the language reference. Further, there's no benet to trying to use __ to attempt to create a "super private" attribute or method in our code. All that happens is that we create a potential future problem if a release of Python ever starts using a name we chose for internal purposes. Also, we're likely to run afoul of the internal name mangling that is applied to these names. The rules for the visibility of Python names are as follows: Most names are public. Names that start with _ are somewhat less public. Use them for implementation details that are truly subject to change. Names that begin and end with __ are internal to Python. We never make these up; we use the names dened by the language reference.

Generally, the Python approach is to register the intent of a method (or attribute) using documentation and a well-chosen name. Often, the interface methods will have elaborate documentation, possibly including doctest examples, while the implementation methods will have more abbreviated documentation and may not have doctest examples. For programmers new to Python, it's sometimes surprising that privacy is not more widely used. For programmers experienced in Python, it's surprising how many brain calories get burned sorting out private and public declarations that aren't really very helpful because the intent is obvious from the method names and the documentation.

Summary
In this chapter, we have reviewed the various design alternatives of the __init__() method. In the next chapter, we will take a look at the special methods, along with a few advanced ones as well.

[ 55 ]

For More Information: www.packtpub.com/mastering-object-oriented-python/book

Where to buy this book


You can buy Mastering Object-oriented Python from the Packt Publishing website: http://www.packtpub.com/mastering-object-oriented-python/book.
Free shipping to the US, UK, Europe and selected Asian countries. For more information, please read our shipping policy.

Alternatively, you can buy the book from Amazon, BN.com, Computer Manuals and most internet book retailers.

www.PacktPub.com

For More Information: www.packtpub.com/mastering-object-oriented-python/book

Anda mungkin juga menyukai