Django中如何为functools.partialmethod设置可生效的自定义属性
问题根因
首先明确两个关键事实:
- 从语法层面,你完全可以给
functools.partialmethod的实例赋值自定义属性,Python不会拦截这类操作。但这些属性仅仅是存储在partialmethod实例的内部字典里,不会自动传递给任何通过该实例生成的可调用对象。 - 你遇到属性丢失报错的本质是:
partialmethod是标准的Python描述器。当你把它作为类属性挂载到模型类上后,不管是Django admin通过类访问方法,还是你自己用getattr取值,Python都会自动触发partialmethod的__get__方法,返回一个全新生成的偏函数对象。这个新生成的对象和你之前挂载admin_order_field、short_description的那个原始partialmethod实例是完全隔离的,自然就读不到任何自定义属性。
你之前在Django 2.2使用的django.utils.functional.curry之所以没有这个问题,是因为它的内部实现专门做了属性透传逻辑:会把挂载在curry实例上的所有自定义属性,自动拷贝到返回的可调用对象上,和原生partialmethod的行为差异明显。
适配方案
以下两种方案均可100%兼容原curry的行为,可根据实际需求选择:
方案1:动态生成闭包方法(最贴合Django开发习惯,零额外依赖)
这是Django官方自定义模型方法时的通用写法,完全避免使用偏函数类,直接在contribute_to_class里动态生成带固定参数的普通函数,直接把自定义属性绑在普通函数对象上,从根源上避免属性丢失。
改造后的contribute_to_class代码如下:
def contribute_to_class(self, cls, name): super().contribute_to_class(cls, name) if hasattr(cls, '_get_value_as_currency'): # 动态生成固定了field参数的展示方法 def get_currency_display(self): return cls._get_value_as_currency(self, field=self) # 直接将自定义属性绑定在普通函数对象上 get_currency_display.admin_order_field = self.name get_currency_display.short_description = self.verbose_name setattr(cls, f'get_{self.name}_as_currency', get_currency_display)
这种写法不存在闭包延迟绑定问题:每个字段的contribute_to_class调用都是独立的,动态生成的函数会正确捕获当前对应的模型类和字段实例。普通函数作为类属性时,通过类访问拿到的就是函数本身,所有挂载的属性都可以被Django admin、业务代码正常读取。
方案2:自定义带属性透传的partialmethod子类(兼容原有代码逻辑,改动量最小)
如果不想修改原来的偏函数逻辑,可以继承原生partialmethod,重写它的__get__方法,把挂载在partialmethod实例上的所有自定义属性自动拷贝到返回的可调用对象上,完全复刻老版Django curry的行为:
from functools import partialmethod class AttachablePartialMethod(partialmethod): def __get__(self, instance, owner=None): bound_callable = super().__get__(instance, owner) # 将当前partialmethod实例上的所有自定义属性透传到返回的可调用对象 for attr_name, attr_value in self.__dict__.items(): setattr(bound_callable, attr_name, attr_value) return bound_callable
之后只需要把原有代码里的partialmethod替换成这个自定义的AttachablePartialMethod,原有逻辑完全不需要改动,自定义属性就可以正常保留。
避坑提醒
不管使用哪种方案,都不要试图给原生partialmethod返回的偏函数对象直接加属性——因为每次通过描述器访问类上的partialmethod时,都会生成全新的偏函数实例,你手动添加的属性会随着每次访问被覆盖,根本无法长期保留。
内容的提问来源于stack exchange,提问作者DrMeers

