python复习–描述符相关–get / set / delete

你看到的这些:

	__dict__
	__get__
	__init__

统称 魔术方法(dunder methods)

本质:Python在背后偷偷调用的普通函数

一. __dict__是什么

dict 就是对象用来存属性的“字典”

1.1 普通对象的__dict__

	class A:
	    pass
	
	a = A()
	a.x = 10
	a.y = 20

	print(a.__dict__)

输出:

{'x': 10, 'y': 20}

示例:

	a.x = 10

等价于:

	a.__dict__['x'] = 10

1.2 类对象也有__dict__

	class A:
    """示例类"""
    z = 100

    def foo(self):
        pass

print(A.__dict__)

你会看到:

{'__module__': '__main__',
 'z': 100, 
 'foo': <function A.foo at 0x7f8b8c0a9e50>, 
 '__dict__': <attribute '__dict__' of 'A' objects>, 
 '__weakref__': <attribute '__weakref__' of 'A' objects>, 
 '__doc__': '示例类'}
  1. module’: ‘main
    表示 类 A 定义所在的模块
    当前代码在主程序中运行,因此是 main
    正常且必然存在

  2. ‘z’: 100
    属于 类属性(Class Attribute)
    属于类本身,所有实例共享
    这是你写的代码产生的结果

  3. ‘foo’: <function A.foo at 0x7f8b8c0a9e50>
    类中定义的 方法
    在类的 dict 中,方法本质是 函数对象
    当通过实例访问时,才会被“绑定”为方法

    A.foo        # 函数
    A().foo()    # 绑定方法
    

    地址 0x7f8b8c0a9e50 是函数在内存中的位置,每次运行可能不同

  4. dict’: <attribute ‘dict’ of ‘A’ objects>
    用于存放 实例对象的属性
    当你创建实例时:

    a = A()
    a.x = 10
    print(a.__dict__)  # {'x': 10}
    

    类本身会自动拥有该属性

  5. weakref’: <attribute ‘weakref’ of ‘A’ objects>
    用于支持 弱引用(weak reference)
    主要用于内存管理和垃圾回收
    普通开发中很少直接使用
    Python 自动添加

  6. doc: ‘示例类’
    类的文档字符串(docstring)
    来自类定义第一行的三引号字符串

    若没有添加注释,这里会显示’doc’: None

二. __get__是什么

__get__不是“给你用的函数”,
而是:当对象作为「类属性」被访问时,
Python 自动调用的一个“加工钩子”。
注:例,A类被实例化后 a=A(); a.xxx 语法中,xxx 永远叫“属性”,语法层面统一叫:属性访问

2.1 一个普通对象,没有__get__

class A:
    z = 100      # 类属性(普通 int)

a = A()        #  先实例化
print(a.z)     #  再访问

访问:

a.z

Python 找到 A.dict[‘z’] == 100
100 是普通对象
没有 get 方法
直接返回 100

普通对象(int / str / list)
没有 get
不会被加工
直接返回

2.2 function ,有__get__

class A:
    def f(self):
        pass
a = A()        #  实例化
print(A.f) 
print(a.f) 
  1. f 到底存在哪
    f 只存在一份,存放在类 A 里
    A.f 和 a.f 访问的是“同一个函数对象”

    class A:
        def f(self):
            pass
    

    类创建完成后:

    A.__dict__['f']  --->  function 对象
    

    只有这一份,存放在类 A 里,不在实例里

  2. 那 A.f 是什么?

    class A:
        def f(self):
            pass
    a = A()        #  实例化
    print(A.f) 
    

    行为是:

    function.__get__(f, None, A)
    

    因为:

    f 是 function
    function 实现了 get
    当通过类访问时:

    A.f  --->  原始 function(未绑定)
    

    你可以理解为:

    “还没绑定 self 的函数”

    你可以验证:

    class A:
        def f(self):
            pass
    a = A()        #  实例化
    print(A.f) 
    
    print(type(A.f))   # <class 'function'>
    
  3. 那 a.f 是什么?

    class A:
        def f(self):
            pass
    a = A()        #  实例化
    print(a.f) 
    

    行为是:

    function.__get__(f, a, A)
    

    返回的是:

    <bound method A.f of a>
    

    这是一个 method 对象
    它里面已经偷偷绑定了 self = a

    你可以验证:

    class A:
        def f(self):
            pass
    a = A()        #  实例化
    print(a.f) 
    print(type(a.f))   # <class 'method'>
    

    结论(非常重要):
    方法不是新定义的东西
    它是 function 被 get 加工后的结果

  4. 用一张表帮你彻底区分

    表达式从哪里找 f返回的是什么
    A.fA.__dict__[‘f’]function(未绑定)
    a.fA.__dict__[‘f’]method(已绑定 self)

    f 是类属性;
    A.f 是“未绑定函数”;
    a.f 是“绑定了实例的 method”。

    方法不是存在于实例里的东西,
    而是类里的函数在“被访问时”动态绑定出来的。

  5. 为什么 Python 要这样设计
    因为这样可以:

    A.f(a)     #  手动传 self
    a.f()      #  自动绑定 self
    

    这两句 完全等价:

    A.f(a) == a.f()
    

    因为 Python 想做到:

    函数写在类里
    用的时候自动拿到实例
    但你不需要手动传 self

    get 就是完成这件事的“隐藏机关”。

    否则你每天都要写:

    A.f(a)
    

2.3. 写一个“带 get 的东西

例子

class MyDescriptor:
    def __get__(self, obj, objtype=None):
        print("属性被访问了")
        return 123


class A:
    x = MyDescriptor()


a = A()
print(a.x)

输出:

属性被访问了
123

你现在亲眼看到 get 被调用了
这段代码证明了三件事
1.x 在类里
2.x 实现了 get
3.所以 a.x 一定会触发 get

三. 访问属性时,Python 做了什么

你写:

	class A:
    z = 100

    def f(self):
        return "normal method"

    @staticmethod
    def s():
        return "static method"
a = A()
print(a.z)
print(a.f)
print(a.s)

Python 内部做的是:

def object_getattr(obj, name):
    # 1.先从类中找(注意:不是实例)
    cls = type(obj)

    # —— 数据描述符优先 ——
    if name in cls.__dict__:
        attr = cls.__dict__[name]
        if has_data_descriptor(attr):
            return attr.__get__(obj, cls)

    # 2.再从实例 __dict__ 中找
    if name in obj.__dict__:
        return obj.__dict__[name]

    # —— 非数据描述符 ——
    if name in cls.__dict__:
        attr = cls.__dict__[name]
        if has_non_data_descriptor(attr):
            return attr.__get__(obj, cls)
        else:
            return attr

    # 3. 还找不到就报错
    raise AttributeError(name)

这就是 Python 访问 a.z 时“真正干的事”

执行 a.z 时:

  1. 第一步:看类

    A.__dict__['z'] == 100
    

    100 有没有 get
    不是描述符
    跳过第 1 步

  2. 第二步:实例

    a.__dict__  # {}
    

    没有 z

  3. 第三步:类(非描述符)

    return 100
    

所以 a.z == 100

执行 a.f 时:

  1. 第一步:类

    A.__dict__['f']  # <function f>
    

    function 有没有 get
    但它是非数据描述符
    第 1 步(数据描述符)不处理

  2. 第二步:实例

    a.__dict__  # {}
    

    没有 f

  3. 第三步:类(非数据描述符)

    attr = A.__dict__['f']
    return attr.__get__(a, A)
    

    等价于:

    bound_method = MethodType(A.f, a)
    

    所以:

    type(a.f)  # method
    

执行 a.s 时:

  1. 第一步:类

    A.__dict__['s']  # staticmethod 对象
    

    staticmethod 有没有 get
    但它是 非数据描述符
    第 1 步跳过

  2. 第二步:实例

    a.__dict__  # {}
    

    没有 s

  3. 第三步:类(非数据描述符)

    attr = A.__dict__['s']
    return attr.__get__(a, A)
    

    而 staticmethod.get 做的是:

    return 原始函数
    

    所以:

    type(a.s)  # function
    

把“对照结果”汇成一张表

访问命中哪一步返回结果类型
a.z第 3 步100int
a.f第 3 步 + get绑定方法method
a.f第 3 步 + get原始函数function

四. 可以这样理解整个流程(非常重要)

a.f
 ↓
找 A.__dict__['f']
 ↓
看有没有 __get__
 ↓
有 → 调 __get__(a, A)
 ↓
返回结果

五. 总结

dict:对象存属性的地方
get:属性被访问时自动执行的钩子
方法 / staticmethod 的区别,本质就是 get 行为不同

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐