Introduction


Credit: Gustavo Niemeyer, Facundo Batista

Today, last weekend, next year. These terms sound so common. You have probably wondered, at least once, about how deeply our lives are involved in the very idea of time. The concept of time surrounds us, and, as a consequence, it's also present in the vast majority of software projects. Even very simple programs may have to deal with timestamps, delays, timeouts, speed gauges, calendars, and so on. As befits a general-purpose language that is proud to come with "batteries included," Python's standard library offers solid support for these application needs, and more support yet comes from third-party modules and packages.

Computing tasks involving money are another interesting topic that catches our attention because it's so closely related to our daily lives. Python 2.4 introduced support for decimal numbers (and you can retrofit that support into 2.3, see http://www.taniquetil.com.ar/facundo/bdvfiles/get_decimal.html), making Python a good option even for computations where you must avoid using binary floats, as ones involving money so often are.

This chapter covers exactly these two topics, money and time. According to the old saying, maybe we should claim the chapter is really about a single topic, since after all, as everybody knowstime is money!

The time Module

Python Standard Library's time module lets Python applications access a good portion of the time-related functionality offered by the platform Python is running on. Your platform's documentation for the equivalent functions in the C library will therefore be useful, and some peculiarities of different platforms will affect Python as well.

One of the most used functions from module time is the one that obtains the current timetime.time. This function's return value may be a little cryptic for the uninitiated: it's a floating-point number that corresponds to the number of seconds passed since a fixed instant called the epoch, which may change depending on your platform but is usually midnight of January 1, 1970.

To check which epoch your platform uses, try, at any Python interactive interpreter prompt:

>>> import time >>> print time.asctime(time.gmtime(0))

Notice we're passing 0 (meaning 0 seconds after the epoch) to the time.gmtime function. time.gmtime converts any timestamp (in seconds since the epoch) into a tuple that represents that precise instant of time in human terms, without applying any kind of time zone conversion (GMT stands for "Greenwich mean time", an old but colorful way to refer to what is now known as UTC, for "Coordinated Universal Time"). You can also pass a timestamp (in seconds since the epoch) to time.localtime, which applies the current local notion of time zone.

It's important to understand the difference, since, if you have a timestamp that is already offset to represent a local time, passing it to the time.localtime function will not yield the expected resultunless you're so lucky that your local time zone happens to coincide with the UTC time zone, of course!

Here is a way to unpack a tuple representing the current local time:

year, month, mday, hour, minute, second, wday, yday = time.localtime( )

While valid, this code is not elegant, and it would certainly not be practical to use it often. This kind of construct may be completely avoided, since the tuples returned by the time functions let you access their elements via meaningful attribute names. Obtaining the current month then becomes a simple and elegant expression:

  time.localtime( ).tm_mon

Note that we omitted passing any argument to localtime. When we call localtime, gmtime, or asctime without an argument, each of them conveniently defaults to using the current time.

Two very useful functions in module time are strftime, which lets you build a string from a time tuple, and strptime, which goes the other way, parsing a string and producing a time tuple. Each of these two functions accepts a format string that lets you specify exactly what you want in the resulting string (or, respectively, what you expect from the string you're parsing) in excruciating detail. For all the formatting specifications that you can use in the format strings you pass to these functions, see http://docs.python.org/lib/module-time.html.

One last important function in module time is the time.sleep function, which lets you introduce delays in Python programs. Even though this function's POSIX counterpart accepts only an integer parameter, the Python equivalent supports a float and allows sub-second delays to be achieved. For instance:

for i in range(10):     time.sleep(0.5)     print "Tick!"

This snippet will take about 5 seconds to execute, emitting Tick! approximately twice per second.

Time and Date Objects

While module time is quite useful, the Python Standard Library also includes the datetime module, which supplies types that provide better abstractions for the concepts of dates and timesnamely, the types time, date, and datetime. Constructing instances of those types is quite elegant:

  today = datetime.date.today( )   birthday = datetime.date(1977, 5, 4)      #May 4   currenttime = datetime.datetime.now( ).time( )   lunchtime = datetime.time(12, 00)   now = datetime.datetime.now( )   epoch = datetime.datetime(1970, 1, 1)   meeting = datetime.datetime(2005, 8, 3, 15, 30)

Further, as you'd expect, instances of these types offer comfortable information access and useful operations through their attributes and methods. The following statements create an instance of the date type, representing the current day, then obtain the same date in the next year, and finally print the result in a dotted format:

  today = datetime.date.today( )   next_year = today.replace(year=today.year+1).strftime("%Y.%m.%d")   print next_year

Notice how the year was incremented, using the replace method. Assigning to the attributes of date and time instances may sound tempting, but these instances are immutable (which is a good thing, because it means we can use the instances as members in a set, or keys in a dictionary!), so new instances must be created instead of changing existing ones.

Module datetime also provides basic support for time deltas (differences between instants of time; you can think of them as basically meaning durations in time), through the timedelta type. This type lets you change a given date by incrementing or decrementing the date by a given time slice, and it is also the result of taking the difference between times or dates.

>>> import datetime >>> NewYearsDay = datetime.date(2005, 01, 01) >>> NewYearsEve = datetime.date(2004, 12, 31) >>> oneday = NewYearsDay - NewYearsEve >>> print oneday 1 day, 0:00:00 >>>

A timedelta instance is internally represented by days, seconds, and microseconds, but you can construct timedelta instances by passing any of these arguments and also other multipliers, like minutes, hours and weeks. Support for other kinds of deltas, like months, and years, is not availableon purpose, since their meanings, and operation results, are debatable. (This feature is, however, offered by the third-party dateutil packagesee https://moin.conectiva.com.br/DateUtil.)

datetime can be described as a prudent or cautious design. The decision of not implementing doubtful tasks, and tasks that may need many different implementations in different systems, reflects the strategy used to develop all of the module. This way, the module offers good interfaces for most use cases, and, even more importantly, a strong and coherent base for third-party modules to build upon.

Another area where this cautious design strategy for datetime shows starkly is the module's time zone support. Even though datetime offers nice ways to query and set time zone information, they're not really useful without an external source to provide nonabstract subclasses of the tzinfo type. At least two third-party packages provide time zone support for datetime: dateutil, mentioned previously, and pyTZ, available at http://sourceforge.net/projects/pytz/.

Decimal

decimal is a Python Standard Library module, new in Python 2.4, which finally brings decimal arithmetic to Python. Thanks to decimal, we now have a decimal numeric data type, with bounded precision and floating point. Let's look at each of these three little phrases in more detail:


Decimal numeric data type

The number is not stored in binary, but rather, as a sequence of decimal digits.


With bounded precision

The number of digits each number stores is fixed. (It is a fixed parameter of each decimal number object, but different decimal number objects can be set to use different numbers of digits.)


Floating point

The decimal point does not have a fixed place. (To put it another way: while the number has a fixed amount of digits in total, it does not have a fixed amount of digits after the decimal point. If it did, it would be a fixed-point, rather than floating-point, numeric data type).

Such a data type has many uses (the big use case is as the basis for money computations), particularly because decimal.Decimal offers many other advantages over standard binary float. The main advantage is that all of the decimal numbers that the user can enter (which is to say, all the decimal numbers with a finite number of digits) can be represented exactly (in contrast, some of those numbers do not have an exact representation in binary floating point):

>>> import decimal >>> 1.1 1.1000000000000001 >>> 2.3 2.2999999999999998 >>> decimal.Decimal("1.1") Decimal("1.1") >>> decimal.Decimal("2.3") Decimal("2.3")

The exactness of the representation carries over into arithmetic. In binary floating point, for example:

>>> 0.1 + 0.1 + 0.1 - 0.3 5.5511151231257827e-17

Such differences are very close to zero, and yet they prevent reliable equality testing; moreover, even tiny differences can accumulate. For this reason, decimal should be preferred to binary floats in accounting applications that have strict equality requirements:

>>> d1 = decimal.Decimal("0.1") >>> d3 = decimal.Decimal("0.3") >>> d1 + d1 + d1 - d3 Decimal("0.0")

decimal.Decimal instances can be constructed from integers, strings, or tuples. To create a decimal.Decimal from a float, first convert the float to a string. This necessary step serves as an explicit reminder of the details of the conversion, including representation error. Decimal numbers include special values such as NaN (which stands for "not a number"), positive and negative Infinity, and -0. Once constructed, a decimal.Decimal object is immutable, just like any other number in Python.

The decimal module essentially implements the rules of arithmetic that are taught in school. Up to a given working precision, exact, unrounded results are given whenever possible:

>>> 0.9 / 10 0.089999999999999997 >>> decimal.Decimal("0.9") / decimal.Decimal(10) Decimal("0.09")

Where the number of digits in a result exceeds the working precision, the number is rounded according to the current rounding method. Several rounding methods are available; the default is round-half-even.

The decimal module incorporates the notion of significant digits, so that, for example, 1.30+1.20 is 2.50. The trailing zero is kept to indicate significance. This is the usual representation for monetary applications. For multiplication, the "schoolbook" approach uses all the figures in the multiplicands:

>>> decimal.Decimal("1.3") * decimal.Decimal("1.2") Decimal("1.56") >>> decimal.Decimal("1.30") * decimal.Decimal("1.20") Decimal("1.5600")

In addition to the standard numeric properties that decimal objects share with other built-in number types, such as float and int, decimal objects also have several specialized methods. Check the docs for all of the methods, with details and examples.

The decimal data type works within a context, where some configuration aspects are set. Each thread has its own current context (having a separate context per thread means that each thread may make changes without interfering with other threads); the current thread's current context is accessed or changed using functions getcontext and setcontext from the decimal module.

Unlike hardware-based binary floating point, the precision of the decimal module can be set by users (defaulting to 28 places). It can be set to be as large as needed for a given problem:

>>> decimal.getcontext( ).prec = 6            # set the precision to 6... >>> decimal.Decimal(1) / decimal.Decimal(7) Decimal("0.142857") >>> decimal.getcontext( ).prec = 60           # ...and to 60 digits >>> decimal.Decimal(1) / decimal.Decimal(7) Decimal("0.142857142857142857142857142857142857142857142857142857142857")

Not everything in decimal can be as simple and elementary as shown so far, of course. Essentially, decimal implements the standards for general decimal arithmetic which you can study in detail at http://www2.hursley.ibm.com/decimal/. In particular, this means that decimal supports the concept of signals. Signals represent abnormal conditions arising from computations (e.g., 1/0, 0/0, Infinity/Infinity). Depending on the needs of each specific application, signals may be ignored, considered as informational, or treated as exceptions. For each signal, there is a flag and a trap enabler. When a signal is encountered, its flag is incremented from zero, and then, if the trap enabler is set to one, an exception is raised. This gives programmers a great deal of power and flexibility in configuring decimal to meet their exact needs.

Given all of these advantages for decimal, why would someone want to stick with float? Indeed, is there any reason why Python (like just about every other widespread language, with Cobol and Rexx the two major exceptions that easily come to mind) originally adopted floating-point binary numbers as its default (or only) noninteger data type? Of coursemany reasons can be provided, and they're all spelled speed! Consider:

$ python -mtimeit -s'from decimal import Decimal as D' 'D("1.2")+D("3.4")' 10000 loops, best of 3: 191 usec per loop $ python -mtimeit -s'from decimal import Decimal as D' '1.2+3.4' 1000000 loops, best of 3: 0.339 usec per loop

This basically translates to: on this machine (an old Athlon 1.2 GHz PC running Linux), Python can perform almost 3 million sums per second on floats (using the PC's arithmetic hardware), but only a bit more than 5 thousand sums per second on Decimals (all done in software and with all the niceties shown previously).

Essentially, if your application must sum many tens of millions of noninteger numbers, you had better stick with float! When an average machine was a thousand times slower than it is today (and it wasn't all that long ago!), such limitations hit even applications doing relatively small amounts of computation, if the applications ran on reasonably cheap machines (again, we see time and money both playing a role!). Rexx and Cobol were born on mainframes that were not quite as fast as today's cheapest PCs but thousands of times more expensive. Purchasers of such mainframes could afford nice and friendly decimal arithmetic, but most other languages, born on more reasonably priced machines (or meant for computationally intensive tasks), just couldn't.

Fortunately, relatively few applications actually need to perform so much arithmetic on non-integers as to give any observable performance problems on today's typical machines. Thus, today, most applications can actually take advantage of decimal's many beneficial aspects, including applications that must continue to use Python 2.3, even though decimal is in the Python Standard Library only since version 2.4. To learn how you can easily integrate decimal into Python 2.3, see http://www.taniquetil.com.ar/facundo/bdvfiles/get_decimal.html.



Python Cookbook
Python Cookbook
ISBN: 0596007973
EAN: 2147483647
Year: 2004
Pages: 420

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net