Use inherited class method within __init__

2019-03-02 16:11发布

问题:

I have a parent class that is inherited by several children. I would like to initialize one of the children using the parent's @classmethod initializers. How can I do this? I tried:

class Point(object):
    def __init__(self,x,y):
        self.x = x
        self.y = y

    @classmethod
    def from_mag_angle(cls,mag,angle):
        x = mag*cos(angle)
        y = mag*sin(angle)
        return cls(x=x,y=y)


class PointOnUnitCircle(Point):
    def __init__(self,angle):
        Point.from_mag_angle(mag=1,angle=angle)


p1 = Point(1,2)
p2 = Point.from_mag_angle(2,pi/2)
p3 = PointOnUnitCircle(pi/4)
p3.x #fail

回答1:

If you try to write __init__ like that, your PointOnUnitCircle has a different interface to Point (as it takes angle rather than x, y) and therefore shouldn't really be a sub-class of it. How about something like:

class PointOnUnitCircle(Point):

    def __init__(self, x, y):
        if not self._on_unit_circle(x, y):
            raise ValueError('({}, {}) not on unit circle'.format(x, y))
        super(PointOnUnitCircle, self).__init__(x, y)

    @staticmethod
    def _on_unit_circle(x, y):
        """Whether the point x, y lies on the unit circle."""
        raise NotImplementedError

    @classmethod
    def from_angle(cls, angle):
        return cls.from_mag_angle(1, angle)

    @classmethod
    def from_mag_angle(cls, mag, angle):  
        # note that switching these parameters would allow a default mag=1
        if mag != 1:
            raise ValueError('magnitude must be 1 for unit circle')
        return super(PointOnUnitCircle, cls).from_mag_angle(1, angle)

This keeps the interface the same, adds logic for checking the inputs to the subclass (once you've written it!) and provides a new class method to easily construct a new PointOnUnitCircle from an angle. Rather than

p3 = PointOnUnitCircle(pi/4)

you have to write

p3 = PointOnUnitCircle.from_angle(pi/4)


回答2:

You can override the subclass's __new__ method to construct instances from the superclass's alternate constructor as shown below.

import math


class Point(object):

    def __init__(self, x, y):
        self.x = x
        self.y = y

    @classmethod
    def from_polar(cls, radius, angle):
        x = radius * math.cos(angle)
        y = radius * math.sin(angle)
        return cls(x, y)


class PointOnUnitCircle(Point):

    def __new__(cls, angle):
        point = Point.from_polar(1, angle)
        point.__class__ = cls
        return point

    def __init__(self, angle):
        pass

Note that in __new__, the line point = Point.from_polar(1, angle) cannot be replaced by point = super().from_polar(1, angle) because whereas Point sends itself as the first argument of the alternate constructor, super() sends the subclass PointOnUnitCircle to the alternate constructor, which circularly calls the subclass's __new__ that calls it, and so on until a RecursionError occurs. Also note that even though __init__ is empty in the subclass, without overriding __init__ in the subclass, the superclass's __init__ would automatically be called immediately after __new__, undoing the alternate constructor.

Alternatively, some object designs are simpler with composition than with inheritance. For example, you could replace the above PointOnUnitCircle class without overriding __new__ with the following class.

class UnitCircle:

    def __init__(self, angle):
        self.set_point(angle)

    def set_point(self, angle):
        self.point = Point.from_polar(1, angle)