ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

python 特殊属性及方法详细解析

python 特殊属性及方法详细解析 前言「特殊属性」和「特殊方法」这两个词经常被混着用其实它们在语言参考里是两套不同的东西。特殊方法也叫魔术方法是那些会被语法隐式调用的、可调用的下划线方法比如__init__、__len__特殊属性是那些由解释器自动维护、通常不是可调用的数据成员比如__dict__、__class__、__module__、__qualname__。前者你写它是为了改变对象的行为后者你读它是为了反射introspection——看看这个对象到底是什么、从哪来、长什么样。初学者最常见的误解有三个。第一以为__doc__、__name__是普通属性随便就能改——本对象上有些能改有些不能类上的__dict__甚至是只读的映射代理。第二以为属性能不能拿到只看「类里有没有」忽略了实例字典优先这条查找顺序。第三以为__dict__一定存在——用了__slots__的实例就可能没有。本文按「属性住在哪里」这条线索把它们梳理清楚并配一张对照表区分属性和方法。一、先分清属性和方法的区别类别长什么样谁在维护典型代表你拿它干什么特殊属性数据成员多为只读或半只读解释器__dict__、__class__、__doc__、__module__、__qualname__反射、调试、序列化特殊方法可调用的方法你定义、解释器调用__init__、__repr__、__eq__、__enter__改变对象在语法下的行为普通属性普通数据成员你self.name存业务数据判断标准很简单能被语法自动触发的下划线成员是方法描述对象自身信息的是属性。当然边界不是绝对的——__init__也是属性它在类字典里只是它的值是个函数对象可以调用而已。理解反射的视角是类本身也是一个对象也有自己的__dict__。二、对象上的特殊属性# 适用于 Python 3.8class Config:配置对象的文档字符串。timeout 3def __init__(self, host):self.host hostcfg Config(localhost)print(cfg.__class__) # class __main__.Configprint(cfg.__dict__) # {host: localhost}——注意只有实例属性print(Config.__dict__[timeout]) # 3类属性在类字典里print(Config.__doc__) # 配置对象的文档字符串。几个必须记牢的点obj.__class__是这个实例所属的类type(obj)也能拿到但对象可以伪造__class__type()不会。obj.__dict__是实例的命名空间只装实例自己的属性看不到类属性。Config上的timeout不在实例字典里。Config.__dict__是映射代理mapping proxy提供类命名空间的只读视图你改不动它。__doc__来自类或函数/模块的第一条字符串字面量没写 docstring 时是None。三、实例属性与类属性的查找顺序语言参考把这条链路和描述符协议合起来看才完整。取obj.attr时1. 先在类型沿 MRO 向上里找同名成员若找到且它是数据描述符直接用它的结果实例字典被忽略2. 否则查实例命名空间 obj.__dict__3. 实例字典里也没有就用类型上找到的那个成员普通类属性或非数据描述符函数会被变成绑定到实例的方法对象4. 以上都找不到才调用类的 __getattr__如果定义了反直觉的地方在第 1 步数据描述符带__set__/__delete__的描述符比如property的优先级高于实例字典而普通属性包括函数属于非数据描述符会被实例字典盖住。所以「实例属性覆盖类属性」这条经验法则对数据描述符是无效的。# 适用于 Python 3.8class Demo:class_attr 我是类属性propertydef computed(self):return 我是数据描述符def method(self):return 我是方法d Demo()d.class_attr 我盖住了类属性 # 赋值只会写进实例字典不会改类字典print(d.class_attr) # 我盖住了类属性print(Demo.class_attr) # 我是类属性——类字典没变print(d.computed) # 我是数据描述符——property 优先级更高最后一行尤其重要即使你给d硬塞一个computed到实例字典里property作为数据描述符依然优先会抛AttributeError——这就是「实例属性覆盖类属性」这条经验法则的例外。四、类、函数、模块上的特殊属性同一套__name__/__qualname__/__module__/__doc__会出现在好几类对象上含义略有差异属性类type函数模块__name__类的名字函数名模块名__qualname__带外层作用域的限定名带外层作用域的限定名无模块用__name____module__定义该类的模块名定义该函数的模块名——__doc__类 docstring函数 docstring模块 docstring__dict__映射代理只读函数的命名空间模块的命名空间可写__qualname__qualified name限定名是 3.3 起引入的它带上了定义处的嵌套路径所以嵌在类或函数里的东西__name__只给最后一段__qualname__给完整路径。# 适用于 Python 3.8class Outer:class Inner:def method(self):passprint(Outer.Inner.__name__) # Innerprint(Outer.Inner.__qualname__) # Outer.Innerprint(Outer.Inner.method.__qualname__) # Outer.Inner.method__mro__是类上的另一个常用特殊属性它是方法解析顺序method resolution order的元组C.__mro__能让你一眼看出多重继承时到底先找谁。五、注解与 __slots__两个特殊的「声明式」属性__annotations__存的是对象上的注解通常是类型提示。语言参考明确提醒它是惰性求值的——访问这个属性本身可能执行任意代码、也可能抛异常从 3.14 起注解改为默认延迟求值。类、函数、模块都能有__annotations__但对没有注解的对象访问它行为与版本相关具体以官方文档为准。__slots__是一个纯声明式的类属性值可以是字符串、可迭代对象或字符串序列作用是显式声明实例会有哪些数据成员并阻止自动创建__dict__和__weakref__。它带来两个实际后果一是没有__dict__的实例不能挂新属性赋值会抛AttributeError二是数据成员固定下来之后内存占用和属性查找都可能更省。要注意的几个陷阱从没有__slots__的父类继承时子类实例照样有__dict____slots__用描述符实现所以不能靠类属性给 slot 变量设默认值给int、bytes、tuple这类变长内置类型定义非空__slots__会直接抛TypeError。__match_args__是 3.10 起配合结构化模式匹配用的类属性值是一个字符串元组让case MyClass(x, y)这种位置写法能映射到关键字。它的缺省等价于空元组。六、把反射用起来一个小例子# 适用于 Python 3.8class Invoice:一张发票。currency CNYdef __init__(self, amount):self.amount amountdef describe(obj):cls obj.__class__print(类名, cls.__name__)print(限定名, cls.__qualname__)print(定义于模块, cls.__module__)print(文档, cls.__doc__)print(实例属性, obj.__dict__)print(类属性键, sorted(k for k in cls.__dict__ if not k.startswith(__)))describe(Invoice(99))cls.__dict__里既能看到currency也能看到__init__——因为类的方法本质上就是存在类字典里的属性。这正是「属性和方法在底层是同一回事」的佐证区分它们的不是存储位置而是能不能被语法隐式调用。常见坑点❌ 以为obj.__dict__里能看到类属性。✅ 实例字典只装实例自己的属性类属性在type(obj).__dict__里要反射得查两处。❌ 给实例赋一个和数据描述符如property同名的属性指望能盖住它。✅ 数据描述符的优先级高于实例字典「实例属性覆盖类属性」在这类成员上不成立。❌ 认为所有实例都有__dict__。✅ 用了__slots__的类实例可能没有__dict__反射代码要先hasattr再访问。❌ 把type(obj).__dict__当普通字典去改。✅ 类的__dict__是只读的映射代理改它会报错要改类型属性用setattr并清楚自己在做什么。❌ 用__name__当对象的唯一标识。✅ 嵌套定义时__name__只有最后一段会重名限定名要用__qualname__3.3。❌ 给子类只写__slots__就以为省掉了__dict__。✅ 父类没有__slots__时子类实例依然有__dict__省内存的目标落空。❌ 直接用类属性给__slots__里的变量设默认值。✅__slots__是描述符实现的类属性会覆盖描述符赋值默认值拿不到默认值应写在__init__里。❌ 以为__annotations__一定是个现成的字典随手就读。✅ 它是惰性求值的访问可能触发代码执行甚至抛异常3.14 起注解默认延迟求值。总结属性住在哪关键性质__class__实例实例所属的类__dict__实例 / 类 / 函数 / 模块实例的可见写类的只读映射代理__name__类 / 函数 / 模块名字最后一段__qualname__类 / 函数带外层路径的限定名3.3__module__类 / 函数 / 方法定义处的模块名__doc__类 / 函数 / 模块文档字符串没有时为 None__mro__类方法解析顺序__slots__类声明式阻止创建__dict____annotations__类 / 函数 / 模块惰性求值3.14 起默认延迟__match_args__类配合模式匹配的位置参数3.10一句话区分特殊方法改变行为特殊属性暴露身份。写类时你关心前者做反射、调试、序列化时你关心后者。而把两者串起来的那条主线就是本节反复强调的查找顺序——实例字典先于非数据描述符的类属性却被数据描述符反超。
返回列表