Currently, math.factorial only supports integers and not floats, whereas the "mathematical" version supports both integers and floats. I.e: ``` import math def better_factorial(n): return n * math.gamma(n) print(math.factorial(10) == better_factorial(10)) ``` This ends up in `True`, as that's correct. However, `math.factorial(math.pi)` (for example, or any float) Ends up in `ValueError: factorial() only accepts integral values`. unlike `better_factorial(math.pi)` which would end up in 7.188082728976031. My proposal is to make another function for floats, or even use the same math.factorial function and check inside it whether the given input is an integer or a float object. I would like to hear your review. Jonathan.
Is there a reason you defined it as n * math.gamma(n), instead of math.gamma(n+1)? Damian (he/him) On Thu, Sep 16, 2021 at 2:35 PM Jonatan <pybots.il@gmail.com> wrote:
Currently, math.factorial only supports integers and not floats, whereas the "mathematical" version supports both integers and floats. I.e: ``` import math
def better_factorial(n): return n * math.gamma(n)
print(math.factorial(10) == better_factorial(10)) ```
This ends up in `True`, as that's correct. However, `math.factorial(math.pi)` (for example, or any float) Ends up in `ValueError: factorial() only accepts integral values`. unlike `better_factorial(math.pi)` which would end up in 7.188082728976031.
My proposal is to make another function for floats, or even use the same math.factorial function and check inside it whether the given input is an integer or a float object.
I would like to hear your review. Jonathan. _______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/HID2JP... Code of Conduct: http://python.org/psf/codeofconduct/
On Thu, Sep 16, 2021 at 06:33:58PM -0000, Jonatan wrote:
Currently, math.factorial only supports integers and not floats, whereas the "mathematical" version supports both integers and floats.
That's questionable. (Warning: maths geek post follows.) Mathematicians typically only use the notation n! (factorial) for non-negative integer arguments. If the argument could be any real number, they typically use Γ(x) (gamma). It is true that n! = Γ(n+1) so from a pragmatic point of view, we could have the factorial function accept any float. The HP-48 calculator does that. But the more modern TI-Nspire calculator doesn't. However, the gamma function is not the only way to extrapolate factorial to all real numbers. There are at least two well-known others: * Gauss' pi function Π(x) = Γ(x+1) so that Π(n) = n! * and Hadamard's gamma function H(x) But there are also others that are not as well known: http://www.luschny.de/math/factorial/hadamard/HadamardsGammaFunctionMJ.html As the above link suggests, Euler's gamma function may be the most famous extrapolation of factorial, but it is perhaps not the best. Although not everyone agrees with that: https://math.stackexchange.com/questions/1537/why-is-eulers-gamma-function-t... By the way, it is unclear why Legendre introduced the notation Γ with the shift of 1 compared to factorial. That shift simplifies some formulae, but complicates many others. The Hungarian-Irish mathematician Cornelius Lanczos called it "void of any rationality". (I love it when mathematicians say what they're really thinking.) So from a *mathematical* perspective: 1. Having factorial accept any real number may be strange and confusing if calculating combinations and permutations, or other applications where non-integer values make no sense for factorial. 2. If you want to extrapolate factorial to non-integer values, you might not want to extrapolate it using Euler's Γ (gamma) function. And from a *programming* perspective: 1. What advantage do we have in writing `factorial(2.5)` if you want to calculate `gamma(3.5)`? 2. We lose the type checking that protects us from some classes of error in applications where extrapolating to floats makes no sense. Looking at your implementation:
def better_factorial(n): return n * math.gamma(n)
that is not going to work:
factorial(23) == better_factorial(23) False
The problem is that since floats only have about 17 significant figures or so, once you get to 23! the gamma function doesn't have enough precision to give an exact answer. Of course that's easily fixable. (Only call gamma if the input argument is a float.) In summary, I don't think that there is much real advantage in having the factorial function take float arguments and call gamma. And we certainly don't need a *second* factorial function that is exactly the same as gamma. Just use gamma directly. -- Steve
17.09.21 05:23, Steven D'Aprano пише:
factorial(23) == better_factorial(23) False
The problem is that since floats only have about 17 significant figures or so, once you get to 23! the gamma function doesn't have enough precision to give an exact answer. Of course that's easily fixable. (Only call gamma if the input argument is a float.)
It is not even easily fixable. First, the resulting function which uses different algorithms for integers and non-integers can be non-monotonic. Second, factorial() supports large arguments for which gamma() raises an overflow error. Third, it will have non-uniform computational complexity. The time of calculating gamma() is virtually constant, but the time of calculating factorial() grows with increasing argument.
On Fri, Sep 17, 2021 at 11:12:45AM +0300, Serhiy Storchaka wrote:
17.09.21 05:23, Steven D'Aprano пише:
factorial(23) == better_factorial(23) False
The problem is that since floats only have about 17 significant figures or so, once you get to 23! the gamma function doesn't have enough precision to give an exact answer. Of course that's easily fixable. (Only call gamma if the input argument is a float.)
It is not even easily fixable.
Yes, good point, the things you mention (monotonicity especially) would be an problem. But I don't think it would be a big problem unless the caller was mixing calls to gamma with int and float arguments. If you stick to one or the other, it wouldn't matter. Or if we had automatic simple type dispatch, we could define: def factorial(n: int) -> int: # current integer-only implementation def factorial(x: float) -> float: return x*gamma(x) and nobody would care that the two factorial functions had different performance and precision characteristics. -- Steve
Steven D'Aprano writes:
But I don't think it would be a big problem unless the caller was mixing calls to gamma with int and float arguments.
You mean `factorial` here, right? `gamma` coerces int to float before evaluating, doesn't it?
If you stick to one or the other, it wouldn't matter.
Users who are disciplined enough to stick to one or the other will use factorial when appropriate, and gamma when that's appropriate. The point of the proposal is to allow the less pedantic to not worry about the difference, and just use `factorial`. Horrifying thought to those of us for whom "pedantic" is a term of praise :-), "although practicality beats purity" :-P .
Or if we had automatic simple type dispatch, we could define:
def factorial(n: int) -> int: # current integer-only implementation
def factorial(x: float) -> float: return x*gamma(x)
and nobody would care that the two factorial functions had different performance and precision characteristics.
Except that it would still be the case that
factorial(23) == factorial(23.0) False
For me, that kills that idea. It's impractical! Other Steve
On Sat, Sep 18, 2021 at 11:24:40AM +0900, Stephen J. Turnbull wrote:
Steven D'Aprano writes:
But I don't think it would be a big problem unless the caller was mixing calls to gamma with int and float arguments.
You mean `factorial` here, right? `gamma` coerces int to float before evaluating, doesn't it?
Right, yes, sorry for the confusion.
If you stick to one or the other, it wouldn't matter.
Users who are disciplined enough to stick to one or the other will use factorial when appropriate, and gamma when that's appropriate. The point of the proposal is to allow the less pedantic to not worry about the difference, and just use `factorial`.
Indeed, and that's one of the problems with the proposal. The batteries in my HP-48GX are flat so I can't see what it does, but the HP-39G-II has a factorial function that computes the gamma function: 5.5! --> returns 287.885277815 but even for integer arguments, it always returns a float. (The calculator merely displays floats as if they were exact integers if they are small enough.) So for sufficiently large input, n! on the calculator is already going to be rounded to whatever float precision the calculator provides. 18! --> 6402373705730000 19! --> 1.21645100409E17
Or if we had automatic simple type dispatch, we could define: [...] and nobody would care that the two factorial functions had different performance and precision characteristics.
Except that it would still be the case that
factorial(23) == factorial(23.0) False
Sure, but if simple type dispatch (generic functions) was built into the language, people would be perfectly comfortable with the idea that two functions with the same name but accepting different types are different functions that might return different values. It only seems weird because we've forgotten the Python 2.x days: >>> 11.0/2 == 11/2 False I acknowledge that those who have not yet learned that floats are not real numbers and don't have infinite precision, and hence are surprised that sqrt(3)**2 != 3, will be surprised by this as well. But let's be honest, people who expect floating point maths to be identical to pure mathematics are surprised by all sorts of things. Python is not Scratch, our audience is not intended to be only the unsophisticated and unlearned newbie casual programmer. Anyway, I agree that trying to fit gamma into factorial would not be a great fit for the language as it stands. The benefit is just too little for the complexity it would add. -- Steve
Except that it would still be the case that
factorial(23) == factorial(23.0) False
Sure, but if simple type dispatch (generic functions) was built into the language, people would be perfectly comfortable with the idea that two functions with the same name but accepting different types are different functions that might return different values.
But factorial—to essentially everyone—means FACTORIAL. A function defined by simple recursion on integers. It DOES NOT mean "the actual factorial functions on the integers, but separately dispatch to (ONE OF) the proposed analytic continuations of the factorial function when given floating point arguments." Besides, what analytic continuation will we use for complex numbers?!... clearly a proposal not covering the complex plain is wrong! :-) The gamma function is already available. If you want the gamma function, just use that! -- Keeping medicines from the bloodstreams of the sick; food from the bellies of the hungry; books from the hands of the uneducated; technology from the underdeveloped; and putting advocates of freedom in prisons. Intellectual property is to the 21st century what the slave trade was to the 16th.
On Thu, 16 Sept 2021 at 19:34, Jonatan <pybots.il@gmail.com> wrote:
Currently, math.factorial only supports integers and not floats, whereas the "mathematical" version supports both integers and floats. I.e: ``` import math
def better_factorial(n): return n * math.gamma(n)
print(math.factorial(10) == better_factorial(10)) ```
This ends up in `True`, as that's correct. However, `math.factorial(math.pi)` (for example, or any float) Ends up in `ValueError: factorial() only accepts integral values`. unlike `better_factorial(math.pi)` which would end up in 7.188082728976031.
My proposal is to make another function for floats, or even use the same math.factorial function and check inside it whether the given input is an integer or a float object.
SymPy has a symbolic factorial function that does this:
import sympy as sym sym.factorial(10) 3628800 sym.factorial(0.5) 0.886226925452758
If you pass in exact rational input then you can compute the result to any desired precision:
sym.factorial(sym.Rational(1, 2)) factorial(1/2) sym.factorial(sym.Rational(1, 2)).evalf(60) 0.886226925452758013649083741670572591398774728061193564106904
Doing the same with integer values requires using evaluate=False to avoid the potentially slow exact integer calculation:
sym.factorial(20) 2432902008176640000 sym.factorial(20, evaluate=False) factorial(20) sym.factorial(20, evaluate=False).evalf(10) 2.432902008e+18 sym.factorial(10**30, evaluate=False).evalf(10) 6.223112323e+29565705518096748172348871081098
https://docs.sympy.org/latest/modules/functions/combinatorial.html#factorial The floating point calculations are computed by mpmath under the hood which can also be used directly: https://mpmath.org/doc/current/functions/gamma.html#factorial-fac -- Oscar
participants (8)
-
Damian Shaw
-
David Mertz, Ph.D.
-
Jonatan
-
Jonathan Sh.
-
Oscar Benjamin
-
Serhiy Storchaka
-
Stephen J. Turnbull
-
Steven D'Aprano