
咱先掰扯清楚:Python 明明没给 “自定义中间运算符”(就是像 +、* 那样夹在俩数中间的符号)的官方接口,咋就能写出 “10 | 乘 | 2”“'hello' | 包含 | 'e'” 这种骚代码?其实核心就靠俩双下方法 ——ror和or,再套个类封装下逻辑就行。这招虽然源自 Python 2 的老配方,但 Python 3 里改改照样能用,今天咱从原理到代码、从坑到面试题,一步步给你讲透,保证每个代码复制过去都能跑!
咱平时写代码,算个乘法得写mul(10,2),检查字符串里有没有某个字符得写'e' in 'hello'。但 “中间型运算符” 是啥?就是把操作符夹在两个操作数中间,比如10 * 2里的*、a + b里的+—— 这种写法更直观,尤其是处理数学逻辑或特定场景时,可读性直接拉满。
但 Python 默认只给了+、-、*这些内置 infix 运算符,没让咱自己定义新的(比如造个❌当乘法符)。那咋办?就得绕个弯子:用 Python 自带的 “位或运算符|” 当 “桥梁”,再通过类的__ror__和__or__方法,把a | op | b这种写法,偷偷改成op(a,b)的逻辑。
__ror__和__or__?要搞懂这个,得先回忆 Python 的 “运算符重载” 规则:当你写a | b时,Python 会按顺序干两件事:
a有没有__or__方法,如果有,就调用a.__or__(b),用这个结果当最终值;a没有__or__,或者a.__or__(b)返回NotImplemented(表示处理不了),就会反过来调用右边b的__ror__方法,也就是b.__ror__(a)。而咱要实现的a | op | b,本质是分两步走:
a | opa是普通对象(比如 int、str),它的__or__方法只能处理同样是 int/str 的对象,没法处理咱自定义的op对象,所以会返回NotImplemented;op的__ror__方法,把a当成参数传进去,比如op.__ror__(a);__ror__里不做实际计算,只把a存起来,返回一个 “中间对象”(可以理解成 “半成品”,等着和右边的b结合)。__or__方法;a和现在的b一起传给咱定义的逻辑(比如乘法、成员检查),最后返回结果。简单说:a | op | b → 先通过__ror__抓牢a → 再通过__or__抓牢b → 最后一起算结果。
咱先写个最基础的 “自定义运算符类”,再扩展到乘法、成员检查等场景,每个代码都能直接跑,咱一步一步来。
这个类是核心,负责实现__ror__和__or__,还要能存咱自定义的逻辑(比如乘法、包含检查)。代码里加了注释,每步干啥都写清楚:
class Infix:
def __init__(self, func):
# 初始化时,把自定义的逻辑(比如lambda)存起来
self.func = func
def __ror__(self, left):
# 第一步:左边的a过来时,存下a,返回self(自己当中间对象)
# 这里用lambda x: self.func(left, x)是为了等右边的b过来
return Infix(lambda x: self.func(left, x))
def __or__(self, right):
# 第二步:右边的b过来时,调用存好的逻辑,计算left和right的结果
return self.func(right)比如咱想写10 | mul | 2,结果等于10*2=20,咋弄?只需用上面的 Infix 类,把 “乘法逻辑” 用 lambda 传进去就行:
# 1. 定义“乘法运算符”:逻辑是x*y
mul = Infix(lambda x, y: x * y)
# 2. 测试:10 | mul | 2 → 等价于mul(10,2)
result1 = 10 | mul | 2
print("10 | mul | 2 =", result1) # 输出:10 | mul | 2 = 20
# 再试个复杂的:(2 | mul | 3) | mul | 4 → 2*3*4=24
result2 = (2 | mul | 3) | mul | 4
print("(2 | mul | 3) | mul | 4 =", result2) # 输出:24咱再搞几个实用的,比如 “检查 a 是否包含 b”(a | contains | b)、“减法”(a | sub | b)、“幂运算”(a | power | b),代码一样简单:
# 1. 成员检查:a是否包含b(比如字符串、列表)
contains = Infix(lambda x, y: y in x)
# 测试:'hello'里有没有'e'?[1,2,3]里有没有4?
print("'hello' | contains | 'e' =", 'hello' | contains | 'e') # True
print("[1,2,3] | contains | 4 =", [1,2,3] | contains | 4) # False
# 2. 减法:a - b
sub = Infix(lambda x, y: x - y)
print("100 | sub | 30 =", 100 | sub | 30) # 70
# 3. 幂运算:a的b次方(比如2^3=8)
power = Infix(lambda x, y: x ** y)
print("2 | power | 3 =", 2 | power | 3) # 8为了让你一目了然,咱把上面的场景整理成表格,包括用途、逻辑、代码和结果,直接抄就能用:
运算符名称 | 核心逻辑(lambda) | 示例代码 | 运行结果 | 适用场景 |
|---|---|---|---|---|
乘法(mul) |
|
| ||
成员检查(contains) |
|
| ||
减法(sub) |
|
| ||
幂运算(power) |
|
| ||
除法(div) |
|
| ||
取模(mod) |
|
|
咱写代码时肯定会掉坑,这里把最常见的 4 个问题列出来,告诉你咋错的、咋改:
__or__,忘了__or__,直接报错!错误代码:
class BadInfix:
def __init__(self, func):
self.func = func
# 只写了__or__,没写__ror__
def __or__(self, right):
return self.func(right)
mul = BadInfix(lambda x,y: x*y)
print(10 | mul | 2) # 报错!报错原因:计算10 | mul时,10(int)的__or__处理不了mul(BadInfix 对象),又没有mul.__ror__可以调用,所以 Python 扔出TypeError: unsupported operand type(s) for |: 'int' and 'BadInfix'。
解决办法:必须在 Infix 类里实现__ror__方法,存下左边的操作数。
错误代码:
# 把lambda x,y: x-y写成了lambda x,y: y-x(参数顺序反了)
sub = Infix(lambda x, y: y - x)
print(10 | sub | 3) # 你以为是10-3=7,实际输出3-10=-7!错误原因:咱的 Infix 类里,__ror__存的是左边的数(比如 10),__or__传的是右边的数(比如 3),最终调用的是func(left, right)(也就是func(10,3))。如果 lambda 里写y-x,就变成了3-10,结果自然反了。
解决办法:lambda 的参数顺序必须是(left, right),要和你想实现的逻辑一致(比如减法就是x-y,x 是 left,y 是 right)。
问题代码:
# 同时用mul和add,想算10*2 + 3 =23,但没加括号
add = Infix(lambda x,y: x+y)
print(10 | mul | 2 | add | 3) # 实际输出多少?实际结果:输出10 | (mul | 2 | add) | 3?不对!因为|是 “左结合” 运算符,Python 会从左往右算:
10 | mul → 得到中间对象 A(存了 10,逻辑是lambda x: 10*x);A | 2 → 调用10*2=20,得到中间对象 B(存了 20,逻辑是lambda x:20+x);B |3 → 20+3=23。哎?这次结果对了?但如果换个顺序呢? 再试一个:10 | add | 2 | mul | 3 → 按左结合算:
10 | add → 中间对象 A(存 10,逻辑lambda x:10+x);A |2 → 10+2=12,中间对象 B(存 12,逻辑lambda x:12*x);B |3 →123=36(也就是`(10+2)3=36,不是10+(2*3)=16`)。 结论:连续用自定义 infix 运算符时,|的左结合特性会影响结果。如果想改变优先级,必须加括号,比如10 | add | (2 | mul | 3),这样才是10+(2*3)=16。
__or__,用不了?问题代码:
# 自己写个类,没实现__or__和__ror__
class MyNum:
def __init__(self, num):
self.num = num
a = MyNum(10)
mul = Infix(lambda x,y: x.num * y.num)
print(a | mul | MyNum(2)) # 报错吗?实际结果:不报错!因为计算a | mul时,a(MyNum)没有__or__,所以会调用mul.__ror__(a),照样能存下a;后续和MyNum(2)结合时,lambda 里用x.num和y.num也能拿到值,最终输出 20。
原因:只要咱的 Infix 类实现了__ror__,不管左边是啥对象(哪怕是自定义的、没实现__or__的),都能触发__ror__,所以不用担心兼容性问题。
如果面试官问你 “Python 咋模拟自定义 infix 运算符”,别只说 “用ror和or”,得把原理、优缺点说透,下面是高频问题和标准答案:
__ror__和__or__?不用__add__或__sub__?答:主要是为了 “避免冲突”。+、-这些运算符是 Python 里最常用的,比如你写10 + add + 2,如果用__add__模拟,很容易和原生的10+2冲突;而|(位或)平时用得少(除了处理二进制、集合),冲突概率低。另外,__ror__的 “反向调用” 特性,刚好能解决 “左边对象处理不了右边运算符” 的问题,完美适配a | op | b的场景。
答:分场景:
a | dot | b表示向量点积)、数据处理(df | filter | condition表示过滤数据框),能让代码更直观,像 “自然语言” 一样;还有就是处理重复逻辑,比如频繁用contains(a,b),改成a | contains | b更简洁。a + b没必要改成a | add | b,纯属画蛇添足。__ror__和__or__,还有别的方法模拟 infix 吗?答:有,但不如这个方法好用:
infix模块(pip install infix),但依赖外部库,兼容性差;infix(lambda x,y: x*y)(10,2),但写法是infix(...),不是a op b,不够直观;(10, mul, 2),然后写个函数解析,但需要额外解析步骤,不如a | op | b直接。 所以__ror__ + __or__是 “无依赖、高兼容、最直观” 的方法。
答:主要有 3 个:
a | mul | 2是啥意思,得先理解背后的__ror__逻辑;|的优先级固定(比+、*低),如果混合用原生运算符,得加括号,比如10 | mul | 2 + 3会变成(10*2)+3,不是10*(2+3);❌当乘法符,用这个方法做不到,因为❌不是 Python 的合法运算符,只能用|当桥梁,所以本质是 “借壳”,不是 “真・自定义符号”。最后咱聊聊:既然这招这么酷,为啥平时很少见人用?
其实核心是 “可读性” 和 “实用性” 的平衡。比如你自己写脚本玩,怎么酷怎么来,用a | power | 3没问题;但如果是团队协作,得考虑别人能不能看懂 —— 毕竟代码是写给人看的,不是写给机器看的。
另外,Python 的设计哲学是 “简洁明了”,这种 “黑魔法” 虽然能实现酷炫的功能,但违背了 “显式优于隐式” 的原则(PEP 20)。所以我的建议是:可以学,用来拓展思路;可以玩,自己写脚本时用;但团队项目里,除非大家都认可,否则别随便用。
为了方便你测试,把所有核心代码汇总在这里,复制到 Python 文件里运行,就能看到所有效果:
# 核心Infix类
class Infix:
def __init__(self, func):
self.func = func
def __ror__(self, left):
return Infix(lambda x: self.func(left, x))
def __or__(self, right):
return self.func(right)
# 1. 乘法
mul = Infix(lambda x, y: x * y)
print("10 | mul | 2 =", 10 | mul | 2) # 20
# 2. 成员检查
contains = Infix(lambda x, y: y in x)
print("'hello' | contains | 'e' =", 'hello' | contains | 'e') # True
print("[1,2,3] | contains | 4 =", [1,2,3] | contains | 4) # False
# 3. 减法
sub = Infix(lambda x, y: x - y)
print("100 | sub | 30 =", 100 | sub | 30) # 70
# 4. 幂运算
power = Infix(lambda x, y: x ** y)
print("2 | power | 3 =", 2 | power | 3) # 8
# 5. 连续运算(加括号改变优先级)
add = Infix(lambda x, y: x + y)
print("10 | add | (2 | mul | 3) =", 10 | add | (2 | mul | 3)) # 16这篇文章从原理到代码、从坑到面试题都讲透了,每个代码都亲测能跑。如果你还想扩展其他场景(比如自定义矩阵乘法、字符串拼接),或者想知道怎么解决 “优先级问题”,随时跟我说,咱再接着深挖!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。