如何通过Python类本身访问类属性名以提升可读性与类型提示?
需求背景
假设有以下类定义:
class MyClass: attribute1: str attribute2: int class MySecondClass: another_attribute1: str another_attribute2: int
想直接通过类拿到属性名的字符串(比如MyClass.attribute1.fieldname返回'attribute1'),避免硬编码字符串带来的重构失效、无类型提示等问题,下面是两个典型场景:
场景1:跨实例字段更新
要写个方法用MySecondClass实例更新MyClass实例,返回布尔值表示是否完成更新。原本用字典存字段映射循环处理的代码如下:
my_class_instance: MyClass my_second_class_instance: MySecondClass field_map = { 'attribute1': 'another_attribute1', 'attribute2': 'another_attribute2' } updated = False for field1, field2 in field_map.items(): current_value = getattr(my_class_instance, field1) new_value = getattr(my_second_class_instance, field2) if current_value != new_value: setattr(my_class_instance, field1, new_value) updated = True return updated
问题:吃不到IDE的类型提示和自动重构福利。要是把MyClass的attribute1重命名,field_map里的字符串不会自动跟着变,直接报错。逐个字段判断的话,字段多了代码又会臃肿不堪。
场景2:Django/DRF中的硬编码坑
Django模型定义:
class MyModel(models.Model): field1 = models.FloatField() field2 = models.IntegerField()
对应的DRF序列化器:
class MyModelSerializer(serializers.ModelSerializer): class Meta: model = MyModel fields = ['field1', 'field2']
问题:fields是字符串数组,要是把field1改名叫my_client_changed_the_requirements,序列化器因为fields没更新直接失效。要是能写成fields = [MyModel.field1.fieldname, MyModel.field2.fieldname],好处很明显:
- 重命名字段时IDE自动同步更新
fields数组 - 编写时IDE能直接提示所有可用字段
Django ORM的过滤操作也有同样的硬编码问题。
现有实现方案
1. 普通类:自定义描述器
对于只定义类型注解的类,可以用自定义描述器给每个属性绑定名称:
class FieldName: def __set_name__(self, owner, name): self.fieldname = name # 把当前实例绑定到类的对应属性上 setattr(owner, name, self) # 修改类定义 class MyClass: attribute1 = FieldName() attribute2 = FieldName() class MySecondClass: another_attribute1 = FieldName() another_attribute2 = FieldName()
这样就能通过MyClass.attribute1.fieldname拿到'attribute1',字段映射可以写成:
field_map = { MyClass.attribute1.fieldname: MySecondClass.another_attribute1.fieldname, MyClass.attribute2.fieldname: MySecondClass.another_attribute2.fieldname }
要保留类型注解的话,可以扩展描述器:
class TypedFieldName(FieldName): def __init__(self, type_): self.type = type_ class MyClass: attribute1 = TypedFieldName(str) attribute2 = TypedFieldName(int)
2. Django专属:直接用字段的name属性
Django模型字段本身就自带name属性,直接用就行:
class MyModelSerializer(serializers.ModelSerializer): class Meta: model = MyModel fields = [MyModel.field1.name, MyModel.field2.name]
ORM过滤也可以这么写:
MyModel.objects.filter(**{MyModel.field1.name: 10.0})
批量获取字段名的话,还能通过MyModel._meta.get_fields()遍历拿到所有字段的name。
3. 通用方案:用inspect模块批量获取
对于任意类,用inspect模块可以批量获取属性名,适合批量处理场景:
import inspect def get_field_names(cls): # 过滤掉方法,只保留属性 return [name for name, member in inspect.getmembers(cls) if not inspect.isfunction(member) and not inspect.ismethod(member)]
给Python加原生支持的可能性
Python目前没有原生语法支持直接通过类属性获取其名称,要加的话得提PEP提案。但考虑到Python的动态特性,原生支持需要兼顾兼容性和语义一致性,短期内落地概率不高。更实际的是用第三方库,比如pydantic的模型可以通过__fields__拿到字段名:
from pydantic import BaseModel class MyClass(BaseModel): attribute1: str attribute2: int # 获取单个字段名 MyClass.__fields__['attribute1'].name # 返回'attribute1'
内容的提问来源于stack exchange,提问作者Job

