在这个问题中,我询问了Python中的函数组合运算符。 @Philip Tzou提供了以下代码,即可完成工作。
import functools
class Composable:
def __init__(self, func):
self.func = func
functools.update_wrapper(self, func)
def __matmul__(self, other):
return lambda *args, **kw: self.func(other.func(*args, **kw))
def __call__(self, *args, **kw):
return self.func(*args, **kw)
我添加了以下功能。
def __mul__(self, other):
return lambda *args, **kw: self.func(other.func(*args, **kw))
def __gt__(self, other):
return lambda *args, **kw: self.func(other.func(*args, **kw))
有了这些添加,就可以使用@,*和>作为运算符来编写函数。 例如,可以写print((add1 @ add2)(5), (add1 * add2)(5), (add1 > add2)(5))并得到# 8 8 8。 (PyCharm抱怨(add1 > add2)(5)不能调用布尔值。但是它仍然运行。)
一直以来,我一直想使用.作为函数组合运算符。 所以我加了
def __getattribute__(self, other):
return lambda *args, **kw: self.func(other.func(*args, **kw))
(请注意,这会导致update_wrapper犯规,可以将此问题删除。)
当我运行print((add1 . add2)(5))时,在运行时出现此错误:AttributeError: 'str' object has no attribute 'func'。 事实证明(显然)__getattribute__的参数在传递给__getattribute__之前已转换为字符串。
有没有办法解决这种转换? 还是我误诊了问题,还有其他方法可行吗?
__getattribute__中的other是属性的名称,它不是像>或@这样的二进制运算符。
如您所见,.被保留用于访问属性和方法。 但是您可以使用,它与原始运算符更相似,并且不会产生此问题。
@FrenchMasterSword实现该功能的神奇方法是什么?! 它不是有效的Python运算符。
好吧,它不是python运算符^^
@FrenchMasterSword那么...这个建议到底有多准确? 您是否建议OP编写自己的解释器版本以将该符号作为运算符处理?
您可能需要__getattr__,而不是__getattribute__。 仅当属性不存在时才调用前者,而对所有属性都调用后者。
@MartijnPieters仍然会得到字符串add2而不是Composable对象。
@jonrsharpe:是的,很好。 __getattr__只能处理属性名称。
相关:如何在python中乘法函数?
你没有想要的东西。 .表示符不是二进制运算符,它是一个主要运算符,只有值操作数(.的左侧)和一个标识符。标识符是字符串,不是生成对值的引用的成熟表达式。
在"属性引用"部分中:
An attribute reference is a primary followed by a period and a name:
attributeref ::= primary"." identifier
The primary must evaluate to an object of a type that supports attribute references, which most objects do. This object is then asked to produce the attribute whose name is the identifier.
因此,在编译时,Python将identifier解析为一个字符串值,而不是一个表达式(这是为运算符提供给运算符的结果)。 __getattribute__挂钩(以及其他任何属性访问挂钩)仅需要处理字符串。没有办法解决这个问题。动态属性访问函数getattr()严格要求name必须是字符串:
>>> getattr(object(), 42)
Traceback (most recent call last):
File"", line 1, in
TypeError: getattr(): attribute name must be string
如果要使用语法来组成两个对象,则只能使用二元运算符,因此需要两个操作数的表达式以及只有具有钩子的表达式(布尔值and和or运算符没有钩子,因为它们懒惰地求值,is和is not没有钩子,因为它们对对象标识(而不是对象值)进行操作。
我一直在努力寻找解决此限制的方法,但我想必须放弃。 @和*还不错。
我在这里问了一个相关的问题:stackoverflow.com/questions/54164382/
通过使用inspect模块,可以绕开在全局范围内专门定义可组合函数的限制。请注意,这与Pythonic差不多,并且使用inspect会使代码难于跟踪和调试。这个想法是使用inspect.stack()从调用上下文中获取名称空间,并在其中查找变量名称。
import functools
import inspect
class Composable:
def __init__(self, func):
self._func = func
functools.update_wrapper(self, func)
def __getattr__(self, othername):
stack = inspect.stack()[1][0]
other = stack.f_locals[othername]
return Composable(lambda *args, **kw: self._func(other._func(*args, **kw)))
def __call__(self, *args, **kw):
return self._func(*args, **kw)
请注意,如果您使用实际上名为func的函数编写函数,则将func更改为_func以防止冲突。另外,我将您的lambda包裹在Composable(...)中,以便它本身可以组成。
证明它在全局范围之外起作用:
def main():
@Composable
def add1(x):
return x + 1
@Composable
def add2(x):
return x + 2
print((add1.add2)(5))
main()
# 8
这给您带来了隐含的好处,即能够将函数作为参数传递,而不必担心在全局范围内将变量名解析为实际函数的名称。例:
@Composable
def inc(x):
return x + 1
def repeat(func, count):
result = func
for i in range(count-1):
result = result.func
return result
print(repeat(inc, 6)(5))
# 11
我实际上不愿意提供这个答案。但是您应该知道,在某些情况下,即使它是主要的,也可以使用点" ."表示法。此解决方案仅适用于可以从globals()访问的功能:
import functools
class Composable:
def __init__(self, func):
self.func = func
functools.update_wrapper(self, func)
def __getattr__(self, othername):
other = globals()[othername]
return lambda *args, **kw: self.func(other.func(*args, **kw))
def __call__(self, *args, **kw):
return self.func(*args, **kw)
去测试:
@Composable
def add1(x):
return x + 1
@Composable
def add2(x):
return x + 2
print((add1.add2)(5))
# 8
很奇怪。但是非常感谢您提供这些信息的麻烦。
这仅适用于在模块Composable中定义的全局变量。虽然您随后可以开始从堆栈中提取名称,但这仍然不能为您提供完整的表达式访问权限;这些将仅仅是语法错误或以错误的顺序进行解析。
@RussAbbott不知道您为什么认为这很奇怪?您有一个字符串,而globals()是将字符串映射到对象的模块名称空间的字典。所以是的,如果您在同一模块中将自己限制为全局变量,则可以通过这种方式将属性名称映射到全局变量。那是一条间接的道路,只是不要指望拥有与二元运算符的钩子一样的表达自由。
@Martijn Pieters。你是对的。"怪异"是错误的词。我对此的反应是,该解决方案仅在有限的情况下有效-正如您还指出的那样。