Configuration via SimpleNamespace

Using a types.SimpleNamespace object allows us to simply add attributes as needed; this is similar to using a class definition. When defining a class, all of the assignment statements are localized to the class. When creating a SimpleNamespace object, we'll need to explicitly qualify every name with the NameSpace object that we're populating. Ideally, we can create SimpleNamespace like the following code:

>>> import types 
>>> config = types.SimpleNamespace(  
...     param1="some value", 
...     param2=3.14, 
... ) 
>>> config 
namespace(param1='some value', param2=3.14) 

This works delightfully well if all of the configuration values are independent of each other. In our case, however, we have some complex dependencies among the configuration values. We can handle this in one of the following two ways:

  • Provide only the independent values and leave it to the application to build the dependent values
  • Build the values in the namespace incrementally

To create only the independent values, we might do something like this:

import types
config2c = types.SimpleNamespace( dealer_rule=Hit17(), split_rule=NoReSplitAces(), player_rule=SomeStrategy(), betting_rule=Flat(), outputfile=Path.cwd()/"data"/"ch14_simulation2c.dat", samples=100, ) config2c.table = Table(
decks=6,
limit=50,
dealer=config2c.dealer_rule,
split=config2c.split_rule,
payout=(3, 2),
)
config2c.player = Player(
play=config2c.player_rule,
betting=config2c.betting_rule,
max_rounds=100,
init_stake=50
)

Here, we created SimpleNamespace with the six independent values for the configuration. Then, we updated the configuration to add two more values that are dependent on four of the independent values.

The config2c object is nearly identical to the object that was created by evaluating Example4() in the preceding example. Note that the base class is different, but the set of attributes and their values are identical. Here's the alternative, where we build the configuration incrementally in a top-level script:

from types import SimpleNamespace

config2d = SimpleNamespace()
config2d.dealer_rule = Hit17()
config2d.split_rule = NoReSplitAces()
config2d.table = Table(
decks=6,
limit=50,
dealer=config2d.dealer_rule,
split=config2d.split_rule,
payout=(3, 2),
)
config2d.player_rule = SomeStrategy()
config2d.betting_rule = Flat()
config2d.player = Player(
play=config2d.player_rule,
betting=config2d.betting_rule,
max_rounds=100,
init_stake=50
)
config2d.outputfile = Path.cwd() / "data" / "ch14_simulation2d.dat"
config2d.samples = 100

The same simulate_c() function shown previously can be used for this configuration object.

Sadly, this suffers from the same problem as configuration using a top-level script. There's no handy way to provide default values to a configuration object.

An easy way to provide defaults is through a function that includes default parameter values.

We might want to have a factory function that we can import, which creates SimpleNamespace with the appropriate default values:

From simulation import  make_config 
config2 = make_config()

If we used something like the preceding code, then the config2 object would have the default values assigned by the factory function, make_config(). A user-supplied configuration only needs to provide overrides for the default values.

Our default-supplying make_config() function can use the following code:

def make_config(
dealer_rule: DealerRule = Hit17(),
split_rule: SplitRule = NoReSplitAces(),
decks: int = 6,
limit: int = 50,
payout: Tuple[int, int] = (3, 2),
player_rule: PlayerStrategy = SomeStrategy(),
betting_rule: BettingStrategy = Flat(),
base_name: str = "ch14_simulation2e.dat",
samples: int = 100,
) -> SimpleNamespace:
return SimpleNamespace(
dealer_rule=dealer_rule,
split_rule=split_rule,
table=Table(
decks=decks,
limit=limit,
dealer=dealer_rule,
split=split_rule,
payout=payout,
),
payer_rule=player_rule,
betting_rule=betting_rule,
player=Player(
play=player_rule,
betting=betting_rule,
max_rounds=100,
init_stake=50
),
outputfile=Path.cwd() / "data" / base_name,
samples=samples,
)

Here, the make_config() function will build a default configuration through a sequence of assignment statements. The derived configuration values, including the table attribute and the player attribute, are built from the original inputs.

An application can then set only the interesting override values, as follows:

config_b = make_config(dealer_rule=Stand17()) 
simulate_c(config_b) 

This seems to maintain considerable clarity by specifying only the override values.

All of the techniques from Chapter 2, The __init__() Method, apply to the definition of this kind of configuration factory function. We can build in a great deal of flexibility if we need to. This has the advantage of fitting nicely with the way that the argparse module parses command-line arguments. We'll expand on this in Chapter 18, Coping with the Command Line.

Let's explore how to use Python with exec() for configuration.

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

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