Python魔法方法漫游指南:属性访问

2385 views, 2023/10/17 updated   Go to Comments

Python 提供了几种常见的访问控制的魔法方法,其作用有点类似其他语言中对属性的封装(定义私有属性,通过公有的 getter/setter 访问)。

更通俗一点,这类魔法方法可以接管“点号”运算。每当你访问类的某个属性时,就将调用这类方法。

此类方法有如下几个:

  • __getattr__:在实例中找不到属性时调用。
  • __getattribute__:无论是否找到属性均调用。
  • __setattr__:对属性赋值时调用。
  • __delattr__:删除属性时调用。

来看个例子:

class Foo:
    def __init__(self, a=10):
        self.a = a

    def __getattr__(self, name):
        return f'Invoke getattr. name: {name}'

f = Foo()

a = f.a
print(a)
# 输出:
# 10
b = f.b
print(b)
# 输出:
# Invoke getattr. name: b

当调用一个不存在的属性时,此方法可以给出警告,或者灵活处理 AttributeError 和返回值。

再看看 __getattribute__ 的行为:

class Foo:
    def __init__(self, a=10):
        self.a = a

    def __getattribute__(self, name):
        return f'Invoke getattr. name: {name}'

f = Foo()

a = f.a
print(a)
# 输出:
# Invoke getattr. name: a
b = f.b
print(b)
# 输出:
# Invoke getattr. name: b

不管调用的属性存在与否,__getattribute__ 均被调用。运用此方法你可以对属性的返回值做某种定制,比如单位换算、动态计算之类的骚操作。

类似的还有 __setattr__

class Foo:
    def __init__(self, a=10):
        self.a = a

    def __setattr__(self, name, value):
        # 注意千万不能 self.name = value + 1 赋值
        # 引发无限递归错误
        self.__dict__[name] = value + 1

f = Foo()
f.a = 20
f.b = 30
print(f.a, f.b)
# 输出:
# 21 31

实现了 __setattr__ 方法后,相当于所有的 self.name = value 都会变成 self.__setattr__('name', value)

注意方法中不能用 self.name = value 进行赋值,会引发无限递归错误,而是要用 self.__dict__[name] 直接修改命名空间中的属性。

文章末尾有对 __dict__ 的解释。

最后就是删除属性的 __delattr__ 了:

class Foo:
    def __init__(self, a=10):
        self.a = a

    def __delattr__(self, name):
        print(f'Invoke delattr. name: {name}')
        self.__dict__.pop(name)

f = Foo()
print(f.a)
# 输出:
# 10

del f.a
print(f.a)
# 输出:
# Invoke delattr. name: a
# AttributeError: 'Foo' object has no attribute 'a'

你可以想象将这些方法组合运用的强大了,强大到可以写出反直觉的花里胡哨的玩意儿出来。 Python 将灵活性交给你,而你的任务是谨慎使用这些能力,同时达到代码简洁之道。

命名空间

Python 的类具有一个特殊的字典叫 __dict__ ,它被称作命名空间,说白了就是一个存放对象所有属性的字典。

对属性的引用被解释器转换为对该字典的查找,比如 a.x 相当于 a.__dict__['x'] 。看下面的例子:

class Foo:
    def __init__(self):
        self.a = 10
        self.b = 20

foo = Foo()

print(foo.__dict__)
# {'a': 10, 'b': 20}

foo.__dict__['c'] = 30

print(foo.__dict__)
# {'a': 10, 'b': 20, 'c': 30}

print(foo.c)
# 30

可以看到在程序运行期间,你可以动态的向 __dict__ 中插入新的值,使得对象具有新的属性。

__setattr__ 中之所以不能用 self.name = xxx 是因为对属性的赋值会嵌套调用 __setattr__ ,从而导致无限递归。

而直接操作 __dict__ 则不会产生这个问题,其他几个属性访问的魔法方法也是类似的道理。


本系列文章开源发布于 Github,传送门:Python魔法方法漫游指南

看完文章想吐槽?欢迎留言告诉我!




本文作者: 杜赛
发布时间: 2021年07月15日 - 11:34
最后更新: 2023年10月17日 - 11:35
转载请保留原文链接及作者