Mastering Objectoriented Python
上QQ阅读APP看书,第一时间看更新

Metaclass example 2 – self-reference

We'll look at an example that involves unit conversion. For example, units of length include meters, centimeters, inches, feet, and numerous other units. Managing unit conversions can be challenging. Superficially, we need a matrix of all possible conversion factors among all the various units. Feet to meters, feet to inches, feet to yards, meters to inches, meters to yards, and so on—every combination.

Practically, however, we can do better than this if we define a standard unit for length. We can convert any unit to the standard and the standard to any other unit. By doing this, we can easily perform any possible conversion as a two-step operation, eliminating the complex matrix of all possible conversions: feet to standard, inches to standard, yards to standard, meters to standard.

In the following example, we're not going to subclass float or numbers.Number in any way. Rather than binding the unit to the value, we'll allow each value to remain a simple number. This is an example of a Flyweight design pattern. The class doesn't define objects that contain the relevant value. The objects only contain the conversion factors.

The alternative (binding units to values) leads to rather complex dimensional analysis. While interesting, it's rather complex.

We'll define two classes: Unit and Standard_Unit. We can easily be sure that each Unit class has a reference to its appropriate Standard_Unit. How can we ensure that each Standard_Unit class has a reference to itself? Self-referencing within a class definition is impossible because the class hasn't been defined yet.

The following is our Unit class definition:

class Unit:
    """Full name for the unit."""
    factor= 1.0
    standard= None # Reference to the appropriate StandardUnit
    name= "" # Abbreviation of the unit's name.
    @classmethod
    def value( class_, value ):
        if value is None: return None
        return value/class_.factor
    @classmethod
    def convert( class_, value ):
        if value is None: return None
        return value*class_.factor

The intent is that Unit.value() will convert a value in the given unit to the standard unit. The Unit.convert() method will convert a standard value to the given unit.

This allows us to work with units, as shown in the following code snippet:

>>> m_f= FOOT.value(4)
>>> METER.convert(m_f)
1.2191999999999998

The values created are built-in float values. For temperatures, the value() and convert() methods need to be overridden, as a simple multiplication doesn't work.

For Standard_Unit, we'd like to do something as follows:

class INCH:
    standard= INCH

However, that won't work. INCH hasn't been defined within the body of INCH. The class doesn't exist until after the definition.

We could, as a fallback, do this:

class INCH:
    pass
INCH.standard= INCH

However, that's rather ugly.

We could define a decorator as follows:

@standard
class INCH:
    pass

This decorator function could tweak the class definition to add an attribute. We'll return to this in Chapter 8, Decorators and Mixins – Cross-cutting Aspects.

Instead, we'll define a metaclass that can insert a circular reference into the class definition, as follows:

class UnitMeta(type):
    def __new__(cls, name, bases, dict):
        new_class= super().__new__(cls, name, bases, dict)
        new_class.standard = new_class
        return new_class

This forces the class variable standard into the class definition.

For most units, SomeUnit.standard references TheStandardUnit class. In parallel with that we'll also have TheStandardUnit.standard referencing TheStandardUnit class, also. This consistent structure among the Unit and Standard_Unit subclasses can help with writing the documentation and automating the unit conversions.

The following is the Standard_Unit class:

class Standard_Unit( Unit, metaclass=UnitMeta ):
    pass

The unit conversion factor inherited from Unit is 1.0, so this class does nothing to the supplied values. It includes the special metaclass definition so that it will have a self-reference that clarifies that this class is the standard for this particular dimension of measurement.

As an optimization, we could override the value() and convert() methods to avoid the multiplication and division.

The following are some sample class definitions for units:

class INCH( Standard_Unit ):
    """Inches"""
    name= "in"

class FOOT( Unit ):
    """Feet"""
    name= "ft"
    standard= INCH
    factor= 1/12

class CENTIMETER( Unit ):
    """Centimeters"""
    name= "cm"
    standard= INCH
    factor= 2.54

class METER( Unit ):
    """Meters"""
    name= "m"
    standard= INCH
    factor= .0254

We defined INCH as the standard unit. The other units' definitions will convert to and from inches.

We've provided some documentation for each unit: the full name in the docstring and a short name in the name attribute. The conversion factor is automatically applied by the convert() and value() functions inherited from Unit.

These definitions allow the following kind of programming in our applications:

>>> x_std= INCH.value( 159.625 )
>>> FOOT.convert( x_std )
13.302083333333332
>>> METER.convert( x_std )
4.054475
>>> METER.factor
0.0254

We can set a particular measurement from a given value in inches and report that value in any other compatible unit.

What the metaclass does is allow us to make queries like this from the unit-definition classes:

>>> INCH.standard.__name__
'INCH'
>>> FOOT.standard.__name__
'INCH'

These kinds of references can allow us to track all the various units of a given dimension.