Skip to content

Commit 786cb5d

Browse files
author
georg.brandl
committed
Patch #1675423: PyComplex_AsCComplex() now tries to convert an object
to complex using its __complex__() method before falling back to the __float__() method. Therefore, the functions in the cmath module now can operate on objects that define a __complex__() method. (backport) git-svn-id: http://svn.python.org/projects/python/trunk@54421 6015fed2-1504-0410-9fe1-9d1591cc4771
1 parent 525c00c commit 786cb5d

5 files changed

Lines changed: 264 additions & 56 deletions

File tree

Doc/api/concrete.tex

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,9 @@ \subsection{Floating Point Objects \label{floatObjects}}
443443

444444
\begin{cfuncdesc}{double}{PyFloat_AsDouble}{PyObject *pyfloat}
445445
Return a C \ctype{double} representation of the contents of
446-
\var{pyfloat}.
446+
\var{pyfloat}. If \var{pyfloat} is not a Python floating point
447+
object but has a \method{__float__} method, this method will first
448+
be called to convert \var{pyfloat} into a float.
447449
\end{cfuncdesc}
448450

449451
\begin{cfuncdesc}{double}{PyFloat_AS_DOUBLE}{PyObject *pyfloat}
@@ -558,8 +560,11 @@ \subsubsection{Complex Numbers as Python Objects}
558560
\end{cfuncdesc}
559561

560562
\begin{cfuncdesc}{Py_complex}{PyComplex_AsCComplex}{PyObject *op}
561-
Return the \ctype{Py_complex} value of the complex number
562-
\var{op}.
563+
Return the \ctype{Py_complex} value of the complex number \var{op}.
564+
\versionchanged[If \var{op} is not a Python complex number object
565+
but has a \method{__complex__} method, this method
566+
will first be called to convert \var{op} to a Python
567+
complex number object]{2.6}
563568
\end{cfuncdesc}
564569

565570

Doc/lib/libcmath.tex

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@ \section{\module{cmath} ---
55
\modulesynopsis{Mathematical functions for complex numbers.}
66

77
This module is always available. It provides access to mathematical
8-
functions for complex numbers. The functions are:
8+
functions for complex numbers. The functions in this module accept
9+
integers, floating-point numbers or complex numbers as arguments.
10+
They will also accept any Python object that has either a
11+
\method{__complex__} or a \method{__float__} method: these methods are
12+
used to convert the object to a complex or floating-point number, respectively, and
13+
the function is then applied to the result of the conversion.
14+
15+
The functions are:
916

1017
\begin{funcdesc}{acos}{x}
1118
Return the arc cosine of \var{x}.

Lib/test/test_cmath.py

Lines changed: 195 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,196 @@
1-
#! /usr/bin/env python
2-
""" Simple test script for cmathmodule.c
3-
Roger E. Masse
4-
"""
1+
from test.test_support import run_unittest
2+
import unittest
53
import cmath, math
6-
from test.test_support import verbose, verify, TestFailed
7-
8-
verify(abs(cmath.log(10) - math.log(10)) < 1e-9)
9-
verify(abs(cmath.log(10,2) - math.log(10,2)) < 1e-9)
10-
try:
11-
cmath.log('a')
12-
except TypeError:
13-
pass
14-
else:
15-
raise TestFailed
16-
17-
try:
18-
cmath.log(10, 'a')
19-
except TypeError:
20-
pass
21-
else:
22-
raise TestFailed
23-
24-
25-
testdict = {'acos' : 1.0,
26-
'acosh' : 1.0,
27-
'asin' : 1.0,
28-
'asinh' : 1.0,
29-
'atan' : 0.2,
30-
'atanh' : 0.2,
31-
'cos' : 1.0,
32-
'cosh' : 1.0,
33-
'exp' : 1.0,
34-
'log' : 1.0,
35-
'log10' : 1.0,
36-
'sin' : 1.0,
37-
'sinh' : 1.0,
38-
'sqrt' : 1.0,
39-
'tan' : 1.0,
40-
'tanh' : 1.0}
41-
42-
for func in testdict.keys():
43-
f = getattr(cmath, func)
44-
r = f(testdict[func])
45-
if verbose:
46-
print 'Calling %s(%f) = %f' % (func, testdict[func], abs(r))
47-
48-
p = cmath.pi
49-
e = cmath.e
50-
if verbose:
51-
print 'PI = ', abs(p)
52-
print 'E = ', abs(e)
4+
5+
class CMathTests(unittest.TestCase):
6+
# list of all functions in cmath
7+
test_functions = [getattr(cmath, fname) for fname in [
8+
'acos', 'acosh', 'asin', 'asinh', 'atan', 'atanh',
9+
'cos', 'cosh', 'exp', 'log', 'log10', 'sin', 'sinh',
10+
'sqrt', 'tan', 'tanh']]
11+
# test first and second arguments independently for 2-argument log
12+
test_functions.append(lambda x : cmath.log(x, 1729. + 0j))
13+
test_functions.append(lambda x : cmath.log(14.-27j, x))
14+
15+
def cAssertAlmostEqual(self, a, b, rel_eps = 1e-10, abs_eps = 1e-100):
16+
"""Check that two complex numbers are almost equal."""
17+
# the two complex numbers are considered almost equal if
18+
# either the relative error is <= rel_eps or the absolute error
19+
# is tiny, <= abs_eps.
20+
if a == b == 0:
21+
return
22+
absolute_error = abs(a-b)
23+
relative_error = absolute_error/max(abs(a), abs(b))
24+
if relative_error > rel_eps and absolute_error > abs_eps:
25+
self.fail("%s and %s are not almost equal" % (a, b))
26+
27+
def test_constants(self):
28+
e_expected = 2.71828182845904523536
29+
pi_expected = 3.14159265358979323846
30+
self.assertAlmostEqual(cmath.pi, pi_expected, 9,
31+
"cmath.pi is %s; should be %s" % (cmath.pi, pi_expected))
32+
self.assertAlmostEqual(cmath.e, e_expected, 9,
33+
"cmath.e is %s; should be %s" % (cmath.e, e_expected))
34+
35+
def test_user_object(self):
36+
# Test automatic calling of __complex__ and __float__ by cmath
37+
# functions
38+
39+
# some random values to use as test values; we avoid values
40+
# for which any of the functions in cmath is undefined
41+
# (i.e. 0., 1., -1., 1j, -1j) or would cause overflow
42+
cx_arg = 4.419414439 + 1.497100113j
43+
flt_arg = -6.131677725
44+
45+
# a variety of non-complex numbers, used to check that
46+
# non-complex return values from __complex__ give an error
47+
non_complexes = ["not complex", 1, 5L, 2., None,
48+
object(), NotImplemented]
49+
50+
# Now we introduce a variety of classes whose instances might
51+
# end up being passed to the cmath functions
52+
53+
# usual case: new-style class implementing __complex__
54+
class MyComplex(object):
55+
def __init__(self, value):
56+
self.value = value
57+
def __complex__(self):
58+
return self.value
59+
60+
# old-style class implementing __complex__
61+
class MyComplexOS:
62+
def __init__(self, value):
63+
self.value = value
64+
def __complex__(self):
65+
return self.value
66+
67+
# classes for which __complex__ raises an exception
68+
class SomeException(Exception):
69+
pass
70+
class MyComplexException(object):
71+
def __complex__(self):
72+
raise SomeException
73+
class MyComplexExceptionOS:
74+
def __complex__(self):
75+
raise SomeException
76+
77+
# some classes not providing __float__ or __complex__
78+
class NeitherComplexNorFloat(object):
79+
pass
80+
class NeitherComplexNorFloatOS:
81+
pass
82+
class MyInt(object):
83+
def __int__(self): return 2
84+
def __long__(self): return 2L
85+
def __index__(self): return 2
86+
class MyIntOS:
87+
def __int__(self): return 2
88+
def __long__(self): return 2L
89+
def __index__(self): return 2
90+
91+
# other possible combinations of __float__ and __complex__
92+
# that should work
93+
class FloatAndComplex(object):
94+
def __float__(self):
95+
return flt_arg
96+
def __complex__(self):
97+
return cx_arg
98+
class FloatAndComplexOS:
99+
def __float__(self):
100+
return flt_arg
101+
def __complex__(self):
102+
return cx_arg
103+
class JustFloat(object):
104+
def __float__(self):
105+
return flt_arg
106+
class JustFloatOS:
107+
def __float__(self):
108+
return flt_arg
109+
110+
for f in self.test_functions:
111+
# usual usage
112+
self.cAssertAlmostEqual(f(MyComplex(cx_arg)), f(cx_arg))
113+
self.cAssertAlmostEqual(f(MyComplexOS(cx_arg)), f(cx_arg))
114+
# other combinations of __float__ and __complex__
115+
self.cAssertAlmostEqual(f(FloatAndComplex()), f(cx_arg))
116+
self.cAssertAlmostEqual(f(FloatAndComplexOS()), f(cx_arg))
117+
self.cAssertAlmostEqual(f(JustFloat()), f(flt_arg))
118+
self.cAssertAlmostEqual(f(JustFloatOS()), f(flt_arg))
119+
# TypeError should be raised for classes not providing
120+
# either __complex__ or __float__, even if they provide
121+
# __int__, __long__ or __index__. An old-style class
122+
# currently raises AttributeError instead of a TypeError;
123+
# this could be considered a bug.
124+
self.assertRaises(TypeError, f, NeitherComplexNorFloat())
125+
self.assertRaises(TypeError, f, MyInt())
126+
self.assertRaises(Exception, f, NeitherComplexNorFloatOS())
127+
self.assertRaises(Exception, f, MyIntOS())
128+
# non-complex return value from __complex__ -> TypeError
129+
for bad_complex in non_complexes:
130+
self.assertRaises(TypeError, f, MyComplex(bad_complex))
131+
self.assertRaises(TypeError, f, MyComplexOS(bad_complex))
132+
# exceptions in __complex__ should be propagated correctly
133+
self.assertRaises(SomeException, f, MyComplexException())
134+
self.assertRaises(SomeException, f, MyComplexExceptionOS())
135+
136+
def test_input_type(self):
137+
# ints and longs should be acceptable inputs to all cmath
138+
# functions, by virtue of providing a __float__ method
139+
for f in self.test_functions:
140+
for arg in [2, 2L, 2.]:
141+
self.cAssertAlmostEqual(f(arg), f(arg.__float__()))
142+
143+
# but strings should give a TypeError
144+
for f in self.test_functions:
145+
for arg in ["a", "long_string", "0", "1j", ""]:
146+
self.assertRaises(TypeError, f, arg)
147+
148+
def test_cmath_matches_math(self):
149+
# check that corresponding cmath and math functions are equal
150+
# for floats in the appropriate range
151+
152+
# test_values in (0, 1)
153+
test_values = [0.01, 0.1, 0.2, 0.5, 0.9, 0.99]
154+
155+
# test_values for functions defined on [-1., 1.]
156+
unit_interval = test_values + [-x for x in test_values] + \
157+
[0., 1., -1.]
158+
159+
# test_values for log, log10, sqrt
160+
positive = test_values + [1.] + [1./x for x in test_values]
161+
nonnegative = [0.] + positive
162+
163+
# test_values for functions defined on the whole real line
164+
real_line = [0.] + positive + [-x for x in positive]
165+
166+
test_functions = {
167+
'acos' : unit_interval,
168+
'asin' : unit_interval,
169+
'atan' : real_line,
170+
'cos' : real_line,
171+
'cosh' : real_line,
172+
'exp' : real_line,
173+
'log' : positive,
174+
'log10' : positive,
175+
'sin' : real_line,
176+
'sinh' : real_line,
177+
'sqrt' : nonnegative,
178+
'tan' : real_line,
179+
'tanh' : real_line}
180+
181+
for fn, values in test_functions.items():
182+
float_fn = getattr(math, fn)
183+
complex_fn = getattr(cmath, fn)
184+
for v in values:
185+
self.cAssertAlmostEqual(float_fn(v), complex_fn(v))
186+
187+
# test two-argument version of log with various bases
188+
for base in [0.5, 2., 10.]:
189+
for v in positive:
190+
self.cAssertAlmostEqual(cmath.log(v, base), math.log(v, base))
191+
192+
def test_main():
193+
run_unittest(CMathTests)
194+
195+
if __name__ == "__main__":
196+
test_main()

Misc/NEWS

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ What's New in Python 2.6 alpha 1?
1212
Core and builtins
1313
-----------------
1414

15+
- Patch #1675423: PyComplex_AsCComplex() now tries to convert an object
16+
to complex using its __complex__() method before falling back to the
17+
__float__() method. Therefore, the functions in the cmath module now
18+
can operate on objects that define a __complex__() method.
19+
1520
- Patch #1623563: allow __class__ assignment for classes with __slots__.
1621
The old and the new class are still required to have the same slot names.
1722

Objects/complexobject.c

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,12 +252,59 @@ Py_complex
252252
PyComplex_AsCComplex(PyObject *op)
253253
{
254254
Py_complex cv;
255+
PyObject *newop = NULL;
256+
static PyObject *complex_str = NULL;
257+
258+
assert(op);
259+
/* If op is already of type PyComplex_Type, return its value */
255260
if (PyComplex_Check(op)) {
256261
return ((PyComplexObject *)op)->cval;
257262
}
263+
/* If not, use op's __complex__ method, if it exists */
264+
265+
/* return -1 on failure */
266+
cv.real = -1.;
267+
cv.imag = 0.;
268+
269+
if (PyInstance_Check(op)) {
270+
/* this can go away in python 3000 */
271+
if (PyObject_HasAttrString(op, "__complex__")) {
272+
newop = PyObject_CallMethod(op, "__complex__", NULL);
273+
if (!newop)
274+
return cv;
275+
}
276+
/* else try __float__ */
277+
} else {
278+
PyObject *complexfunc;
279+
if (!complex_str) {
280+
if (!(complex_str = PyString_FromString("__complex__")))
281+
return cv;
282+
}
283+
complexfunc = _PyType_Lookup(op->ob_type, complex_str);
284+
/* complexfunc is a borrowed reference */
285+
if (complexfunc) {
286+
newop = PyObject_CallFunctionObjArgs(complexfunc, op, NULL);
287+
if (!newop)
288+
return cv;
289+
}
290+
}
291+
292+
if (newop) {
293+
if (!PyComplex_Check(newop)) {
294+
PyErr_SetString(PyExc_TypeError,
295+
"__complex__ should return a complex object");
296+
Py_DECREF(newop);
297+
return cv;
298+
}
299+
cv = ((PyComplexObject *)newop)->cval;
300+
Py_DECREF(newop);
301+
return cv;
302+
}
303+
/* If neither of the above works, interpret op as a float giving the
304+
real part of the result, and fill in the imaginary part as 0. */
258305
else {
306+
/* PyFloat_AsDouble will return -1 on failure */
259307
cv.real = PyFloat_AsDouble(op);
260-
cv.imag = 0.;
261308
return cv;
262309
}
263310
}

0 commit comments

Comments
 (0)