mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 05:31:20 +00:00 
			
		
		
		
	Added more documentation on how mixed-mode arithmetic should be implemented. I
also noticed and fixed a bug in Rational's forward operators (they were claiming all instances of numbers.Rational instead of just the concrete types).
This commit is contained in:
		
							parent
							
								
									e973c61238
								
							
						
					
					
						commit
						b23dea6adb
					
				
					 3 changed files with 222 additions and 13 deletions
				
			
		|  | @ -99,3 +99,144 @@ The numeric tower | ||||||
|    3-argument form of :func:`pow`, and the bit-string operations: ``<<``, |    3-argument form of :func:`pow`, and the bit-string operations: ``<<``, | ||||||
|    ``>>``, ``&``, ``^``, ``|``, ``~``. Provides defaults for :func:`float`, |    ``>>``, ``&``, ``^``, ``|``, ``~``. Provides defaults for :func:`float`, | ||||||
|    :attr:`Rational.numerator`, and :attr:`Rational.denominator`. |    :attr:`Rational.numerator`, and :attr:`Rational.denominator`. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | Notes for type implementors | ||||||
|  | --------------------------- | ||||||
|  | 
 | ||||||
|  | Implementors should be careful to make equal numbers equal and hash | ||||||
|  | them to the same values. This may be subtle if there are two different | ||||||
|  | extensions of the real numbers. For example, :class:`rational.Rational` | ||||||
|  | implements :func:`hash` as follows:: | ||||||
|  | 
 | ||||||
|  |     def __hash__(self): | ||||||
|  |         if self.denominator == 1: | ||||||
|  |             # Get integers right. | ||||||
|  |             return hash(self.numerator) | ||||||
|  |         # Expensive check, but definitely correct. | ||||||
|  |         if self == float(self): | ||||||
|  |             return hash(float(self)) | ||||||
|  |         else: | ||||||
|  |             # Use tuple's hash to avoid a high collision rate on | ||||||
|  |             # simple fractions. | ||||||
|  |             return hash((self.numerator, self.denominator)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | Adding More Numeric ABCs | ||||||
|  | ~~~~~~~~~~~~~~~~~~~~~~~~ | ||||||
|  | 
 | ||||||
|  | There are, of course, more possible ABCs for numbers, and this would | ||||||
|  | be a poor hierarchy if it precluded the possibility of adding | ||||||
|  | those. You can add ``MyFoo`` between :class:`Complex` and | ||||||
|  | :class:`Real` with:: | ||||||
|  | 
 | ||||||
|  |     class MyFoo(Complex): ... | ||||||
|  |     MyFoo.register(Real) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | Implementing the arithmetic operations | ||||||
|  | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||||
|  | 
 | ||||||
|  | We want to implement the arithmetic operations so that mixed-mode | ||||||
|  | operations either call an implementation whose author knew about the | ||||||
|  | types of both arguments, or convert both to the nearest built in type | ||||||
|  | and do the operation there. For subtypes of :class:`Integral`, this | ||||||
|  | means that :meth:`__add__` and :meth:`__radd__` should be defined as:: | ||||||
|  | 
 | ||||||
|  |     class MyIntegral(Integral): | ||||||
|  | 
 | ||||||
|  |         def __add__(self, other): | ||||||
|  |             if isinstance(other, MyIntegral): | ||||||
|  |                 return do_my_adding_stuff(self, other) | ||||||
|  |             elif isinstance(other, OtherTypeIKnowAbout): | ||||||
|  |                 return do_my_other_adding_stuff(self, other) | ||||||
|  |             else: | ||||||
|  |                 return NotImplemented | ||||||
|  | 
 | ||||||
|  |         def __radd__(self, other): | ||||||
|  |             if isinstance(other, MyIntegral): | ||||||
|  |                 return do_my_adding_stuff(other, self) | ||||||
|  |             elif isinstance(other, OtherTypeIKnowAbout): | ||||||
|  |                 return do_my_other_adding_stuff(other, self) | ||||||
|  |             elif isinstance(other, Integral): | ||||||
|  |                 return int(other) + int(self) | ||||||
|  |             elif isinstance(other, Real): | ||||||
|  |                 return float(other) + float(self) | ||||||
|  |             elif isinstance(other, Complex): | ||||||
|  |                 return complex(other) + complex(self) | ||||||
|  |             else: | ||||||
|  |                 return NotImplemented | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | There are 5 different cases for a mixed-type operation on subclasses | ||||||
|  | of :class:`Complex`. I'll refer to all of the above code that doesn't | ||||||
|  | refer to ``MyIntegral`` and ``OtherTypeIKnowAbout`` as | ||||||
|  | "boilerplate". ``a`` will be an instance of ``A``, which is a subtype | ||||||
|  | of :class:`Complex` (``a : A <: Complex``), and ``b : B <: | ||||||
|  | Complex``. I'll consider ``a + b``: | ||||||
|  | 
 | ||||||
|  |     1. If ``A`` defines an :meth:`__add__` which accepts ``b``, all is | ||||||
|  |        well. | ||||||
|  |     2. If ``A`` falls back to the boilerplate code, and it were to | ||||||
|  |        return a value from :meth:`__add__`, we'd miss the possibility | ||||||
|  |        that ``B`` defines a more intelligent :meth:`__radd__`, so the | ||||||
|  |        boilerplate should return :const:`NotImplemented` from | ||||||
|  |        :meth:`__add__`. (Or ``A`` may not implement :meth:`__add__` at | ||||||
|  |        all.) | ||||||
|  |     3. Then ``B``'s :meth:`__radd__` gets a chance. If it accepts | ||||||
|  |        ``a``, all is well. | ||||||
|  |     4. If it falls back to the boilerplate, there are no more possible | ||||||
|  |        methods to try, so this is where the default implementation | ||||||
|  |        should live. | ||||||
|  |     5. If ``B <: A``, Python tries ``B.__radd__`` before | ||||||
|  |        ``A.__add__``. This is ok, because it was implemented with | ||||||
|  |        knowledge of ``A``, so it can handle those instances before | ||||||
|  |        delegating to :class:`Complex`. | ||||||
|  | 
 | ||||||
|  | If ``A<:Complex`` and ``B<:Real`` without sharing any other knowledge, | ||||||
|  | then the appropriate shared operation is the one involving the built | ||||||
|  | in :class:`complex`, and both :meth:`__radd__` s land there, so ``a+b | ||||||
|  | == b+a``. | ||||||
|  | 
 | ||||||
|  | Because most of the operations on any given type will be very similar, | ||||||
|  | it can be useful to define a helper function which generates the | ||||||
|  | forward and reverse instances of any given operator. For example, | ||||||
|  | :class:`rational.Rational` uses:: | ||||||
|  | 
 | ||||||
|  |     def _operator_fallbacks(monomorphic_operator, fallback_operator): | ||||||
|  |         def forward(a, b): | ||||||
|  |             if isinstance(b, (int, long, Rational)): | ||||||
|  |                 return monomorphic_operator(a, b) | ||||||
|  |             elif isinstance(b, float): | ||||||
|  |                 return fallback_operator(float(a), b) | ||||||
|  |             elif isinstance(b, complex): | ||||||
|  |                 return fallback_operator(complex(a), b) | ||||||
|  |             else: | ||||||
|  |                 return NotImplemented | ||||||
|  |         forward.__name__ = '__' + fallback_operator.__name__ + '__' | ||||||
|  |         forward.__doc__ = monomorphic_operator.__doc__ | ||||||
|  | 
 | ||||||
|  |         def reverse(b, a): | ||||||
|  |             if isinstance(a, RationalAbc): | ||||||
|  |                 # Includes ints. | ||||||
|  |                 return monomorphic_operator(a, b) | ||||||
|  |             elif isinstance(a, numbers.Real): | ||||||
|  |                 return fallback_operator(float(a), float(b)) | ||||||
|  |             elif isinstance(a, numbers.Complex): | ||||||
|  |                 return fallback_operator(complex(a), complex(b)) | ||||||
|  |             else: | ||||||
|  |                 return NotImplemented | ||||||
|  |         reverse.__name__ = '__r' + fallback_operator.__name__ + '__' | ||||||
|  |         reverse.__doc__ = monomorphic_operator.__doc__ | ||||||
|  | 
 | ||||||
|  |         return forward, reverse | ||||||
|  | 
 | ||||||
|  |     def _add(a, b): | ||||||
|  |         """a + b""" | ||||||
|  |         return Rational(a.numerator * b.denominator + | ||||||
|  |                         b.numerator * a.denominator, | ||||||
|  |                         a.denominator * b.denominator) | ||||||
|  | 
 | ||||||
|  |     __add__, __radd__ = _operator_fallbacks(_add, operator.add) | ||||||
|  | 
 | ||||||
|  |     # ... | ||||||
|  | @ -292,7 +292,13 @@ def denominator(self): | ||||||
| 
 | 
 | ||||||
|     # Concrete implementation of Real's conversion to float. |     # Concrete implementation of Real's conversion to float. | ||||||
|     def __float__(self): |     def __float__(self): | ||||||
|         """float(self) = self.numerator / self.denominator""" |         """float(self) = self.numerator / self.denominator | ||||||
|  | 
 | ||||||
|  |         It's important that this conversion use the integer's "true" | ||||||
|  |         division rather than casting one side to float before dividing | ||||||
|  |         so that ratios of huge integers convert without overflowing. | ||||||
|  | 
 | ||||||
|  |         """ | ||||||
|         return self.numerator / self.denominator |         return self.numerator / self.denominator | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -179,16 +179,6 @@ def __str__(self): | ||||||
|         else: |         else: | ||||||
|             return '%s/%s' % (self.numerator, self.denominator) |             return '%s/%s' % (self.numerator, self.denominator) | ||||||
| 
 | 
 | ||||||
|     """ XXX This section needs a lot more commentary |  | ||||||
| 
 |  | ||||||
|     * Explain the typical sequence of checks, calls, and fallbacks. |  | ||||||
|     * Explain the subtle reasons why this logic was needed. |  | ||||||
|     * It is not clear how common cases are handled (for example, how |  | ||||||
|       does the ratio of two huge integers get converted to a float |  | ||||||
|       without overflowing the long-->float conversion. |  | ||||||
| 
 |  | ||||||
|     """ |  | ||||||
| 
 |  | ||||||
|     def _operator_fallbacks(monomorphic_operator, fallback_operator): |     def _operator_fallbacks(monomorphic_operator, fallback_operator): | ||||||
|         """Generates forward and reverse operators given a purely-rational |         """Generates forward and reverse operators given a purely-rational | ||||||
|         operator and a function from the operator module. |         operator and a function from the operator module. | ||||||
|  | @ -196,10 +186,82 @@ def _operator_fallbacks(monomorphic_operator, fallback_operator): | ||||||
|         Use this like: |         Use this like: | ||||||
|         __op__, __rop__ = _operator_fallbacks(just_rational_op, operator.op) |         __op__, __rop__ = _operator_fallbacks(just_rational_op, operator.op) | ||||||
| 
 | 
 | ||||||
|  |         In general, we want to implement the arithmetic operations so | ||||||
|  |         that mixed-mode operations either call an implementation whose | ||||||
|  |         author knew about the types of both arguments, or convert both | ||||||
|  |         to the nearest built in type and do the operation there. In | ||||||
|  |         Rational, that means that we define __add__ and __radd__ as: | ||||||
|  | 
 | ||||||
|  |             def __add__(self, other): | ||||||
|  |                 if isinstance(other, (int, long, Rational)): | ||||||
|  |                     # Do the real operation. | ||||||
|  |                     return Rational(self.numerator * other.denominator + | ||||||
|  |                                     other.numerator * self.denominator, | ||||||
|  |                                     self.denominator * other.denominator) | ||||||
|  |                 # float and complex don't follow this protocol, and | ||||||
|  |                 # Rational knows about them, so special case them. | ||||||
|  |                 elif isinstance(other, float): | ||||||
|  |                     return float(self) + other | ||||||
|  |                 elif isinstance(other, complex): | ||||||
|  |                     return complex(self) + other | ||||||
|  |                 else: | ||||||
|  |                     # Let the other type take over. | ||||||
|  |                     return NotImplemented | ||||||
|  | 
 | ||||||
|  |             def __radd__(self, other): | ||||||
|  |                 # radd handles more types than add because there's | ||||||
|  |                 # nothing left to fall back to. | ||||||
|  |                 if isinstance(other, RationalAbc): | ||||||
|  |                     return Rational(self.numerator * other.denominator + | ||||||
|  |                                     other.numerator * self.denominator, | ||||||
|  |                                     self.denominator * other.denominator) | ||||||
|  |                 elif isinstance(other, Real): | ||||||
|  |                     return float(other) + float(self) | ||||||
|  |                 elif isinstance(other, Complex): | ||||||
|  |                     return complex(other) + complex(self) | ||||||
|  |                 else: | ||||||
|  |                     return NotImplemented | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         There are 5 different cases for a mixed-type addition on | ||||||
|  |         Rational. I'll refer to all of the above code that doesn't | ||||||
|  |         refer to Rational, float, or complex as "boilerplate". 'r' | ||||||
|  |         will be an instance of Rational, which is a subtype of | ||||||
|  |         RationalAbc (r : Rational <: RationalAbc), and b : B <: | ||||||
|  |         Complex. The first three involve 'r + b': | ||||||
|  | 
 | ||||||
|  |             1. If B <: Rational, int, float, or complex, we handle | ||||||
|  |                that specially, and all is well. | ||||||
|  |             2. If Rational falls back to the boilerplate code, and it | ||||||
|  |                were to return a value from __add__, we'd miss the | ||||||
|  |                possibility that B defines a more intelligent __radd__, | ||||||
|  |                so the boilerplate should return NotImplemented from | ||||||
|  |                __add__. In particular, we don't handle RationalAbc | ||||||
|  |                here, even though we could get an exact answer, in case | ||||||
|  |                the other type wants to do something special. | ||||||
|  |             3. If B <: Rational, Python tries B.__radd__ before | ||||||
|  |                Rational.__add__. This is ok, because it was | ||||||
|  |                implemented with knowledge of Rational, so it can | ||||||
|  |                handle those instances before delegating to Real or | ||||||
|  |                Complex. | ||||||
|  | 
 | ||||||
|  |         The next two situations describe 'b + r'. We assume that b | ||||||
|  |         didn't know about Rational in its implementation, and that it | ||||||
|  |         uses similar boilerplate code: | ||||||
|  | 
 | ||||||
|  |             4. If B <: RationalAbc, then __radd_ converts both to the | ||||||
|  |                builtin rational type (hey look, that's us) and | ||||||
|  |                proceeds. | ||||||
|  |             5. Otherwise, __radd__ tries to find the nearest common | ||||||
|  |                base ABC, and fall back to its builtin type. Since this | ||||||
|  |                class doesn't subclass a concrete type, there's no | ||||||
|  |                implementation to fall back to, so we need to try as | ||||||
|  |                hard as possible to return an actual value, or the user | ||||||
|  |                will get a TypeError. | ||||||
|  | 
 | ||||||
|         """ |         """ | ||||||
|         def forward(a, b): |         def forward(a, b): | ||||||
|             if isinstance(b, RationalAbc): |             if isinstance(b, (int, long, Rational)): | ||||||
|                 # Includes ints. |  | ||||||
|                 return monomorphic_operator(a, b) |                 return monomorphic_operator(a, b) | ||||||
|             elif isinstance(b, float): |             elif isinstance(b, float): | ||||||
|                 return fallback_operator(float(a), b) |                 return fallback_operator(float(a), b) | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Jeffrey Yasskin
						Jeffrey Yasskin