functools.partial 和 partialmethod 的困惑

75 天前
 julyclyde

我在写一个 telegram bot ,自己寨了一个小的 lib

class Client:
    def callAPI(self, method="getMe", **kwargs):
       调用 requests.post(....)
       
    getMe = functools.partialmethod(callAPI, "getMe")

这样是成功的,可以用 getMe=functools.partialmethod 的方法定义一个名为 getMe 但实际上偏函数调用 callAPI 的方法

但是另外几种写法都不对,我不明白为什么: 第一个:

def __getattr__(self, APIname):
        return functools.partialmethod(callAPI, APIname)

会导致错误 NameError: name 'callAPI' is not defined 我不明白为什么 getMe 直接赋值的时候可以找到 callAPI 方法,但是在__getattr__函数里就找不到 callAPI 方法,必须用 self.callAPI 的方式来找。是不是因为前者在 class 内,而后者在 method 内的,scope 不同的缘故?

第二个:

def __getattr__(self, APIname):
        return functools.partialmethod(self.callAPI, APIname)

c=Client(token="....") c.getMe()会发生 TypeError: 'partialmethod' object is not callable

但是 c.getMe.func() 就可以正常执行

我不明白,为什么直接用 partialmethod 赋值出来那个函数就是 callable 的,但这里用__getattr__返回的却不是 callable 的呢

最后找到正确写法是:

    def __getattr__(self, APIname):
        return functools.partial(self.callAPI, APIname)

但是该用 partialmethod 的地方用了 partial ,总感觉不正经

2682 次点击
所在节点    Python
11 条回复
killerirving
75 天前
1. “是不是因为前者在 class 内,而后者在 method 内的,scope 不同的缘故” 是的
2. 这种情况应该使用 partial 。partialmethod 是声明为 class descriptor 使用的,被读取时会调用__get__();而在函数中直接调用的 method 是__call__(),partialmethod class 中并没有定义该方法所以会有 not callable 的报错
julyclyde
75 天前
@killerirving 求教,为什么之前的写法,在 class 直属层直接使用 getMe = functools.partialmethod(callAPI, "getMe")是可以的呢?
按说如果 partialmethod 返回的那个对象需要再 dot func 才能调用,那这种写法下的 getMe 到底是个啥东西??为什么不需要 dot func 就可以用?
killerirving
75 天前
c.getMe.func()其实是个错误使用,func 是 partialmethod class 的成员变量,也就是 self.callAPI 这个参数
```
class partial:
"""New function with partial application of the given arguments
and keywords.
"""

__slots__ = "func", "args", "keywords", "__dict__", "__weakref__"

def __new__(cls, func, /, *args, **keywords):
if not callable(func):
raise TypeError("the first argument must be callable")

if hasattr(func, "func"):
args = func.args + args
keywords = {**func.keywords, **keywords}
func = func.func

self = super(partial, cls).__new__(cls)

self.func = func
self.args = args
self.keywords = keywords
return self
````
julyclyde
75 天前
@killerirving 我的疑问其实是:

我最开始的写法
```
class Client:
def callAPI(self, method="getMe", **kwargs):
调用 requests.post(....)

getMe = functools.partialmethod(callAPI, "getMe")
```
这里 getMe 是一个 partialmethod object ,是 callable 的

但是为什么我改成
```
def __getattr__(self, APIname):
return functools.partialmethod(callAPI, APIname)
```
然后__getattr__返回的对象就不是 callable 呢?

看起来这俩都是 partialmethod object 啊?
或者可能,前者( getMe 直接赋值 partialmethod )并不是一个 partialmethod object ??
keakon
75 天前
partial 实现很简单,它的 __call__() 方法将新老参数合并在一起调用原函数。
因此 c.getMe() -> c. __getattr__('getMe') -> functools.partial(self.callAPI, 'getMe') -> self.callAPI('getMe')

partialmethod 是一个没有定义 __call__() 方法的 descriptor ,而它的 __get__ 方法主要实现是调用 partial()。
因此 functools.partialmethod(self.callAPI, APIname) 返回的是一个不能调用的 partialmethod 对象。
而 getMe = functools.partialmethod(callAPI, "getMe") 是给 Client 类定义了一个叫 'getMe' 的 descriptor 。此时,c.getMe() -> functools.partialmethod(callAPI, "getMe").__get__(c, Client) -> Client.callAPI(c, "getMe")。
julyclyde
74 天前
@keakon 哦。因为我不懂什么叫 descriptor 所以看不懂你说的……
我滚回去复习文档去了
julyclyde
74 天前
@keakon 是不是可以理解为:
直接赋值那个,it doesn't HAVE TO be callable?
而__getattr__那个必须是 callable ,所以才暴露出来了 partialmethod 返回值不是 callable 这个问题?
keakon
74 天前
@julyclyde 你先看看 descriptor 的作用吧。简单来说,如果一个类( Client )的属性( getMe )是 descriptor ,那么在访问这个类的实例( c )的同名属性( getMe )时,访问的实际是这个 descriptor 的 __get__() 方法。
getMe = functools.partialmethod(callAPI, "getMe") 正是给类定义了一个 descriptor ,而它的 __get__() 方法里返回了一个 callable 。
而 return functools.partialmethod(self.callAPI, APIname) 这个实现虽然返了 descriptor ,但它不是类的属性,因此访问时并不会调用 __get__()。
julyclyde
74 天前
@keakon 谢谢
我回去重读一下这段

以前觉得这段文档不重要哈哈哈哈……现在发现了还是必须得掌握
NoOneNoBody
71 天前
尽量避免用 partialmethod 吧,刚出来时有人提过 issue ,当时没有确信解释,应是个 bug (结果不可预料),后续版本我就没关注,不知道修了没有

可以用取巧方法,如闭包+partial ,partial 还是稳定的
julyclyde
71 天前
@NoOneNoBody 为啥会不可预料呢?按说如果是用错了,应该稳定失败才对啊?

闭包听起来很邪教,外观看着像函数,但实际上是个有状态的对象,上次执行有可能对下次执行有影响,用起来不安心

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1077892

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX