[Python-3000] PEP 31XX: A Type Hierarchy for Numbers (and other algebraic entities) (original) (raw)

Guido van Rossum guido at python.org
Mon Apr 30 00:46:58 CEST 2007


On 4/29/07, Jeffrey Yasskin <jyasskin at gmail.com> wrote:

On 4/28/07, Baptiste Carvello <baptiste13 at altern.org> wrote: > 2) In the PEP, the concepts are used inconsistently. Complex derives from Ring > because the set of complex numbers is a ring. Int derives from Complex because > integer are complex numbers (or, alternatively, the set of integers *is included > in* the set of complex numbers). The consistent way could be to make the Complex > class an instance of Ring, not a subclass.

Good point. In this structure, isinstance(3, Ring) really means that 3 is a member of some (unspecified) ring, not that 3 isa Ring,

To ask whether x is a Ring, you'd use issubclass(x, Ring). Now, in a different context (still in Python) you might define a class Ring whose instances are rings; but in the current draft of PEP 3141, Ring is a class whose subclasses are rings.

[BTW "isa" is not an English word, nor used anywhere in Python. If "isa" means "is a" you might as well write proper English; if it has additional connotations, they're likely lost to this crowd so you can't count on your readers knowing the distinction.]

but the ambiguity there is probably the root cause of the problem with mixed-mode operations.

(Where by mixed-mode operations you mean e.g. trying to add or multiply members of two different rings, right?)

We should also say that isinstance(3, Complex) means that 3 is a member of some subring of the complex numbers, which preserves the claim that Complex is a subtype of Ring.

Hm... it's beginning to look like binary operations in the mathematical sense just don't have an exact equivalent in OO type theory. (Or vice versa.) There really isn't a way to define an operation binop(a: T, b: T) -> T in such a way that it is clear what should happen if a and b are members of two different subtypes of T, named T1 and T2. Classic OO seems to indicate that this must be defined, and there are plenty of examples that seem to agree, e.g. when T1 and T2 are trivial subtypes of T (maybe each adding a different inessential method). OTOH we have the counter-example where T==Ring and T1 and T2 are two different, unrelated Rings, and the result may not be defined.

Hmm... Maybe the conclusion to draw from this is that we shouldn't make Ring a class? Maybe it ought to be a metaclass, so we could ask isinstance(Complex, Ring)?

Perhaps a similar line of reasoning migtht apply to PartiallyOrdered and TotallyOrdered.

Up to here, things make sense, but because of how ABCs work, we need issubclass(rational, Complex). I suppose that's true too, since isinstance(3.4, rational) means "3.4 is a member of the rational subring of the complex numbers", which implies that "3.4 is a member of some subring of the complex numbers."

There may be better names for these concepts. Perhaps suffixing every numeric ABC with "Element"? Do you have suggestions?

Maybe we should stop trying to capture radically different mathematical number systems using classes or types, and limit ourselves to capturing the systems one learns in high school: C, R, Q, Z, and (perhaps) N (really N0). The concrete types would be complex <: C, float<:R, Decimal<:R, int<:Z. NumPy would have many more. One could argue that float and Decimal are <:Q, but I'm not sure if that makes things better pragmatically; I guess I'm coming from the old Algol school where float was actually called real (and in retrospect I wish I'd called it that in Python). I'd rather reserve membership of Q for an infinite precision rational type (which many people have independently implemented).

Jason Orendorff points out that Haskell typeclasses capture the fact that complex is an instance of Ring. I think imitating them as much as possible would indeed imply making the numeric ABCs into metaclasses (in Haskell terminology, "kinds"). To tell if the arguments to a function were in the same total order, you'd check if they had any common superclasses that were themselves instances of TotallyOrdered. I don't know enough about how metaclasses are typically used to know how that would conflict.

The more I think about it, it sounds like the right thing to do. To take PartiallyOrdered (let's say PO for brevity) as an example, the Set class should specify PO as a metaclass. The PO metaclass could require that the class implement lt and le. If it found a class that didn't implement them, it could make the class abstract by adding the missing methods to its abstractmethods attribute. Or, if it found that the class implemented one but not the other, it could inject a default implementation of the other in terms of the one and eq.

This leaves us with the question of how to check whether an object is partially orderable. Though that may really be the wrong question -- perhaps you should ask whether two objects are partially orderable relative to each other. For that, you would first have to find the most derived common base class (if that is even always a defined operation(*)), and then check whether that class is an instance of PO. It seems easier to just try the comparison -- duck typing isn't dead yet! I don't think this is worth introducing a new inspection primitive ('ismetainstance(x, PO)').

The PO class may still be useful for introspection: at the meta-level, it may be useful occasionally to insist that or inquire whether a given class is PO. (Or TO, or a Ring, etc.)

Now, you could argue that Complex should also be a metaclass. While that may mathematically meaningful (for all I know there are people doing complex number theory using Complex[Z/n]), for Python's numeric classes I think it's better to make Complex a regular class representing all the usual complex numbers (i.e. a pair of Real numbers). I expect that the complex subclasses used in practice are all happy under mixed arithmetic using the usual definition of mixed arithmetic: convert both arguments to a common base class and compute the operation in that domain.

(*) consider classes AB derived from (B, A) and BA derived from (A, B). Would A or B be the most derived base class? Or would we have to skip both and continue the search with A's and B's base classes?

-- --Guido van Rossum (home page: http://www.python.org/~guido/)



More information about the Python-3000 mailing list