Creating a top-level main() function

In Chapter 14, Configuration Files and Persistence, we suggested two application configuration design patterns:

  • A global property map: In the previous examples, we implemented the global property map with a Namespace object created by ArgumentParser.
  • Object construction: The idea behind object construction was to build the required object instances from the configuration parameters, effectively demoting the global property map to a local property map inside the main() function and not saving the properties.

What we showed you in the previous section was the use of a local Namespace object to collect all of the parameters. From this, we can build the necessary application objects that will do the real work of the application. The two design patterns aren't a dichotomy; they're complementary. We used Namespace to accumulate a consistent set of values and then built the various objects based on the values in that namespace.

This leads us to a design for a top-level function. Before looking at the implementation, we need to consider a proper name for this function. There are two ways to name the function:

  • Name it main(), because that's a common term for the starting point of the application as a whole; everyone expects this.
  • Don't name it main(), because main() is too vague to be meaningful in the long run and limits reuse. If we follow this path, we can make a meaningful top-level function with a name that's a verb_noun() phrase to describe the operation fairly. We can also add a line main = verb_noun that provides an alias of main().

Using the second, two-part implementation, lets us change the definition of main() through extension. We can add a new function and reassign the name main to the newer function. Old function names are left in place as part of a stable, growing API.

Here's a top-level application script that builds objects from a configuration Namespace object:

import ast 
import csv
import argparse

def simulate_blackjack(config: argparse.Namespace) -> None:
dealer_classes = {"Hit17": Hit17, "Stand17": Stand17}
dealer_rule = dealer_classes[config.dealer_rule]()
split_classes = {
"ReSplit": ReSplit, "NoReSplit": NoReSplit, "NoReSplitAces": NoReSplitAces
}
split_rule = split_classes[config.split_rule]()
try:
payout = ast.literal_eval(config.payout)
assert len(payout) == 2
except Exception as ex:
raise ValueError(f"Invalid payout {config.payout}") from ex
table = Table(
decks=config.decks,
limit=config.limit,
dealer=dealer_rule,
split=split_rule,
payout=payout,
)
player_classes = {"SomeStrategy": SomeStrategy, "AnotherStrategy": AnotherStrategy}
player_rule = player_classes[config.player_rule]()
betting_classes = {
"Flat": Flat, "Martingale": Martingale, "OneThreeTwoSix": OneThreeTwoSix
}
betting_rule = betting_classes[config.betting_rule]()
player = Player(
play=player_rule,
betting=betting_rule,
max_rounds=config.rounds,
init_stake=config.stake,
)
simulate = Simulate(table, player, config.samples)
with Path(config.outputfile).open("w", newline="") as target:
wtr = csv.writer(target)
wtr.writerows(simulate)

main = simulate_blackjack

The simulate_blackjack function depends on an externally supplied Namespace object with the configuration attributes. It's not named main() so that we can make future additions and changes. We can reassign main to any new function that replaces or extends this function.

This function builds the various objects—Table, Player, and Simulate—that are required. We configured these objects based on the supplied configuration parameters.

We've set up the object that does the real work. After the object construction, the actual work is a single, highlighted line: wtr.writerows(simulate). About 90 percent of the program's time will be spent here, generating samples and writing them to the required file.

A similar pattern holds for GUI applications. They enter a main loop to process GUI events. The pattern also holds for servers that enter a main loop to process requests.

We've depended on having a configuration object passed in as an argument. This follows from our testing strategy of minimizing dependencies. This top-level simulate_blackjack() function doesn't depend on the details of how the configuration was created. We can then use this function in an application script, as follows:

if __name__ == "__main__": 
    logging.config.dictConfig(yaml.load("logging.config")) 
    config5 = get_options_2(sys.argv[1:]) 
    simulate_blackjack(config5) 
    logging.shutdown() 

This represents a separation of concerns. The work of the application is separated into three separate parts:

  • The outermost level is defined by logging. We configured logging outside of all other application components to ensure that there are no conflicts between other top-level packages configuring logging. When we look at combining applications into larger composite processing, we need to be sure that several applications being combined doesn't result in conflicting logging configurations.
  • The inner level is defined by the application's configuration. We don't want conflicts among separate application components. We'd like to allow a single command-line API to evolve separately from our application implementations. We'd like to be able to embed our application processing into separate environments, perhaps defined by multiprocessing or a RESTful web server.
  • The final portion is the simulate_blackjack() function. This is separated from the logging and configuration issues. This allows a variety of techniques to be used to provide a configuration of parameters. Furthermore, when we look at combining this with other processing, the separation of logging and configuration will be helpful.
..................Content has been hidden....................

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