mutually exclusive arguments to a constructor

Steven D'Aprano steve+comp.lang.python at pearwood.info
Fri Dec 30 17:13:15 EST 2011


On Fri, 30 Dec 2011 20:40:16 +0000, Adam Funk wrote:

> (Warning: this question obviously reflects the fact that I am more
> accustomed to using Java than Python.)
> 
> Suppose I'm creating a class that represents a bearing or azimuth,
> created either from a string of traditional bearing notation
> ("N24d30mE") or from a number indicating the angle in degrees as usually
> measured in trigonometry (65.5, measured counter-clockwise from the
> x-axis).  The class will have methods to return the same bearing in
> various formats.
> 
> In Java, I would write two constructors, one taking a single String
> argument and one taking a single Double argument.  But in Python, a
> class can have only one __init__ method, although it can have a lot of
> optional arguments with default values.  What's the correct way to deal
> with a situation like the one I've outlined above?

The most idiomatic way to do this would be with named constructor 
functions, or by testing the argument type in __init__. For example:

# Method 1
class Azimuth(object):
    def __init__(self, angle):
        # Initialise an azimuth object from an angle (float)
        self._angle = float(angle)
    @classmethod
    def from_bearing(cls, bearing):
        # Create an azimuth object from a bearing (string).
        angle = cls.bearing_to_angle(bearing)
        return cls(angle)
    @staticmethod
    def bearing_to_angle(bearing):
        # Convert a bearing (string) into a float.
        return 0.0  # whatever...

        
Note some features of this version:

* Normal methods receive the instance as first argument, "self".

* We use the classmethod and staticmethod decorators to create class 
  and static methods. Be warned that the meaning of these are NOT 
  the same as in Java!

* Class methods receive the class object as first argument, "cls". 
  Hence the name. Note that in Python, classes are objects too.

* We make from_bearing a class method, so we can call it from either
  the class itself:

      ang = Azimuth.from_bearing("25N14E")

  or from an existing instance:

      ang2 = ang.from_bearing("17N31W")

* Static methods don't receive either the class or the instance. They
  are equivalent to a top level function, except encapsulated inside
  a class.


# Method 2
class Azimuth(object):
    def __init__(self, arg):
        # Initialise an azimuth object from arg, either an angle (float)
        # or a bearing (string).
        if isinstance(arg, str):
            angle = bearing_to_angle(arg)
        else:
            angle = float(arg)
        self._angle = float(angle)

def bearing_to_angle(bearing):
    # Convert a bearing (string) into a float.
    return 0.0  # whatever...


Note that in this example, I've turned bearing_to_angle into a regular 
function outside of the class instead of a static method. Just because I 
can. This is probably slightly more idiomatic than the use of static 
methods.


Either method is acceptable, although the first is slightly more "pure" 
because it doesn't use isinstance. The second may fail if the user passes 
a string-like object which behaves identically to strings, but doesn't 
inherit from str. If you care about that, you should prefer the first way 
with an explicit from_bearing method.



-- 
Steven



More information about the Python-list mailing list