如何在Python中通用地使用sin、cos、tan(包括用户定义类型)?

11

编辑:让我试着重新措辞和改善我的问题。旧版本附在底部。

我正在寻找一种以通用类型的方式表达和使用自由函数的方法。例如:

abs(x)  # maps to x.__abs__()
next(x) # maps to x.__next__() at least in Python 3
-x      # maps to x.__neg__()

在这些情况下,函数的设计允许用户使用自定义类型通过委托工作给非静态方法调用来定制其行为。这很好。它使我们能够编写不真正关心确切参数类型的函数,只要它们“感觉”像模拟某种概念的对象即可。
反例:无法轻松通用的函数:
math.exp  # only for reals
cmath.exp # takes complex numbers

假设我想编写一个通用函数,可以在一组类似数字的对象上应用exp函数。 我应该使用什么exp函数? 如何选择正确的exp函数?
def listexp(lst):
    return [math.exp(x) for x in lst]

显然,这种方法对于复数列表是行不通的,即使有一个适用于复数的exp函数(在cmath中)。而且,它也不能适用于任何用户定义的类似数字类型,该类型可能提供其自己的特殊exp函数。
因此,我正在寻找一种处理双方问题的方法 - 最好是不需要特别处理很多东西。作为某个通用函数的编写者,他不关心参数的确切类型,我希望使用特定于所涉及类型的正确数学函数,而无需明确处理此问题。作为用户定义类型的编写者,我希望公开特殊的数学函数,已增强以处理存储在这些对象中的其他数据(类似于复数的虚部)。
做到这一点的首选模式/协议/惯用法是什么?我还没有测试numpy。但我下载了它的源代码。据我所知,它为数组提供sin函数。不幸的是,我还没有在源代码中找到它的实现。但是看到他们如何成功选择适合数组当前存储的数字类型的正确sin函数将会很有趣。
在C ++中,我会依靠函数重载和ADL(参数相关查找)。由于C ++是静态类型的,因此这(名称查找,重载分辨率)完全在编译时处理。我想,我可以使用Python和Python提供的反射工具在运行时模拟这个过程。但我也知道,尝试将编码风格导入另一种语言可能是一个坏主意,并且在新语言中不太惯用。因此,如果您对方法有不同的想法,我会倾听。
我猜,在某个时候,我需要以可扩展的方式手动执行一些类型相关的调度。也许编写一个名为"tgmath"(类型通用数学)的模块,该模块支持实数和复数,并允许其他人注册其类型和特殊情况函数... 观点?Python大师对此有何看法?
谢谢!
编辑:显然,我不是唯一对通用函数和类型相关重载感兴趣的人。有PEP 3124,但自4年前起仍处于草案状态。

旧版本的问题:

我在Java和C++方面有很强的背景,最近才开始学习Python。 我想知道的是:我们如何扩展数学函数(至少它们的名称),使它们适用于其他用户定义的类型? 这些类型的函数是否提供任何可以利用的扩展点/钩子(类似于迭代器协议,其中next(obj)实际上委托给obj.__next__等)?

在C ++中,我只需使用新参数类型重载函数,并让编译器根据参数表达式的静态类型确定应该使用哪个函数。 但由于Python是一种非常动态的语言,因此不存在重载。 做这件事的首选Python方式是什么?

此外,在编写自定义函数时,我希望避免长链

if isinstance(arg,someClass):
    suchandsuch
elif ...

有哪些模式可以让代码看起来更漂亮、更符合Python的风格?

我想,我基本上是在尝试解决Python中缺乏函数重载的问题。至少在C++中,重载和参数相关的查找是良好C++风格的重要部分。

是否可能使

x = udt(something)  # object of user-defined type that represents a number
y = sin(x)          # how do I make this invoke custom type-specific code for sin?
t = abs(x)          # works because abs delegates to __abs__() which I defined.

工作?我知道我可以将sin作为类的非静态方法。但是这样一来,我就失去了通用性,因为对于其他类型的类似数字对象,它们使用sin(x)而不是x.sin()

添加一个__float__方法是不可接受的,因为我在对象中保留了附加信息,如“自动微分”的导数。

TIA

编辑:如果您想知道代码的样子,请查看this。在理想情况下,我希望能以类型通用的方式使用sin/cos/sqrt。我认为这些函数是对象接口的一部分,即使它们是“自由函数”。在__somefunction中,我没有使用math.__main__.限定函数。这只是因为我通过装饰器在自定义函数中手动回退到math.sin(等等)。但我认为这是一个丑陋的hack。


似乎内置的数学函数没有任何扩展点。例如,sin(x) 可以尝试回退到一个方法:x.__sin__() …叹气… - sellibitze
sin()不是内置函数,因此通用对象的数据模型支持__sin__方法并没有太多意义。在大多数情况下,如果在对象上调用math.sin()有意义,那么也应该能够调用float() - Wooble
Python 3.2有所不同:<built-in function sin> - sellibitze
奇怪,我的Python3.2显示:NameError: name 'sin' is not defined - Wooble
注意:我在Python中尝试的内容在Julia中变得轻而易举(而且速度更快)。谢谢,Julia!多重分派和即时编译胜利! - sellibitze
5个回答

5
你可以这样做,但它是反着来的。你需要在新类型中实现__float__(),然后sin()就可以使用你的类了。
换句话说,你不是让正弦函数适用于其他类型,而是使这些类型适用于正弦函数。
这样做更好,因为它强制实现一致性。如果你的对象没有明显的浮点数映射,那么对于该类型来说,sin()可能没有合理的解释。
[对于我之前错过的"__float__无法工作"部分感到抱歉;也许你是在回应这个问题?不管怎样,作为令人信服的证明,python有cmath库,可以为复数等添加sin()等...]

2
很不幸,这对我来说是不可接受的,因为我试图使用此自定义类型来自动跟踪导数(自动微分)。 - sellibitze
啊,抱歉。 (我曾经用Python编写过一些符号微分代码-http://acooke.org/cute/Differenti0.html,如果有帮助的话;我知道这不是同一件事...) - andrew cooke
谢谢提供链接。但我更喜欢使用自动微分。符号微分有点过头了,因为我不需要看到任何公式——只需要在离散点处评估函数和导数。;-) - sellibitze
1
我不明白为什么会有问题。__float__() 应该返回一个新的浮点数,而不是将您的对象转换为浮点数。这样 sin() 就不必破坏信息了。 - Clueless
1
@Clueless:在我所处的地方(静态类型语言),“转换”仅仅意味着创建一个不同类型的新对象——通常情况下,原始对象并没有被修改。损失在其他地方。我为这个用户定义的类型有自己的sin函数,它可以处理对象中可用的额外数据。 __float__建议和忽略复数的虚部一样毫无意义。不,我不能实现__float__的方式,使其意义明显,原因与编写float(some_complex_number)没有意义相同。 - sellibitze

4

如果你希望math.sin()的返回类型是你定义的类型,那么似乎没戏了。Python的math库基本上只是一个快速本地IEEE 754浮点数运算库的简单包装器。如果你想保持内部一致性和鸭子类型,至少可以在自己的代码中加入Python缺少的可扩展性层。

def sin(x):
    try:
        return x.__sin__()
    except AttributeError:
        return math.sin(x)

现在你可以导入这个sin函数,并无差别地在之前使用math.sin的所有位置使用它。虽然不像让math.sin自动适应你的duck-typing那么漂亮,但至少它可以在你的代码库中保持一致性。

有趣。你会如何在这里添加对复数的支持? - sellibitze
为了保持一致性,您可以让实现复数的类实现__sin __()。我认为这种多态性作为核心语言特性非常好,适用于最常见的操作,但是这些数学运算非常耗费性能,人们愿意明确指定类型。我认为作为数学库的作者,完全可以说“我们已经使所有内置函数与我们的导数类型配合使用,并且您需要导入和显式使用任何其他内容。” - Clueless

1

在一个模块中定义你自己的版本。这就是 cmath 用于复数和 numpy 用于数组的做法。


目前,这是我所做的。谢谢你提供numpy的参考资料。不过我想知道例如numpy的sin函数是如何委托工作的。它必须以某种方式找到正确的sin函数并将其逐元素地应用于数组。 - sellibitze
此外,对于一些操作(例如sqrt),decimal模块做了什么? - Devin Jeanpierre

0

0
理想情况下,您应该从本地Python类型派生您的用户定义的数字类型,这样数学函数就可以正常工作。当不可能时,也许您可以在对象上定义__int__()__float__()__complex__()__long__(),以便它知道如何将自己转换为数学函数可以处理的类型。
当这不可行时,例如如果您希望对存储x和y位移而不是角度的对象进行sin()运算,您需要提供自己的等效函数(通常作为类的方法)或一个函数,例如to_angle(),将对象的内部表示转换为Python所需的表示形式。
最后,您可以提供自己的math模块,用您自己的变体替换内置的数学函数,这样如果您想在表达式中允许对您的类进行数学运算而不进行任何语法更改,可以通过这种方式实现,尽管这很棘手并且可能会降低性能,因为您将在Python中进行大量预处理,然后再调用本地实现。

嗯,Python确实支持__int__()__float()__,而且数学函数也能正常工作... - Wooble
哎呀,谷歌在这方面让我失望了。我本以为记得是这样的,但找不到证据。我已经编辑了我的回答以反映这一点。 - kindall

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接