![Mastering Objectoriented Python](https://wfqqreader-1252317822.image.myqcloud.com/cover/948/36704948/b_36704948.jpg)
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.