# Python中的魔法属性

# 魔法属性

在Python中,所有以 __​ 双下划线包起来的方法,都统称为 Magic Method​,例如类的初始化方法 __init__()​ ,实例对象创造方法 __new__()​等。

魔法属性和方法是Python内置的一些属性和方法,有着特殊的含义。命名时前后加上两个下划线,在执行系统特定操作时,会自动调用。

image

# 常见的魔法属性

# __doc__

描述信息


# __doc__
class Foo:
    """ 描述类信息,这是用于测试的类 """
  
    def func(self):
        pass

  
# ipython 
In [2]: Foo.__doc__
Out[2]: ' 描述类信息,这是用于测试的类 '
  

# __module__​ 和 __class__

  • __module__​​​​ 表示当前操作的对象在那个模块

    • 入口文件是__main__​模块,其他都和文件名一致
  • __class__​​​​ 表示当前操作的对象的类是什么

    • 实例属性需要通过实例对象访问,类属性需要通过类进行访问,同时实例对象也能访问类属性
    • 每个实例都有一个__class__​​​属性,指向创建实例对象的类对象。
    • 实例对象访问属性的规则是先访问实例属性,然后再根据实例对象的 __class__​​​ 来访问类属性。如果都没有找到则报错。
    • 实例对象无法修改对象属性,实例对象.类属性 = xxx 并没有修改到其类属性,而是在实例对象中创建了一个与类属性同名的实例属性。

# __module__、__class__
# oop.py
class Student(object):
  
    def __init__(self, name):
        self.name = name
      
      
# main.py
from oop import Student

s = Student()
print(s.__module__)  # 输出 oop 即:输出模块
print(s.__class__)   # 输出 <class 'oop.Student'> 即:输出类

# __init__​ * 、*​__new__

__init__()​​ 初始化方法 和 __new__()​​,通过类创建对象时,自动触发执行。__new__​​ 是用来创建类并返回这个类的实例,而 __init__​​ 只是将传入的参数来初始化该实例。

new 魔术方法刚执行时,实例对象还未诞生,因此它不可能以实例方法的形式存在,只能以类方法的形式存在。

  • Python中调用构造器创建对象属于两阶段构造过程

  • 首先执行__new__​​方法会返回当前对象的一个实例,同时获得保存对象所需的内存空间

  • __init__​​在创建完对象后调用,对当前对象的一些实例初始化,无返回值

  • __new__​​方法的返回值是创建好的Python对象(的引用),而__init__​​方法的第一个参数就是这个对象(的引用),所以在__init__​​中可以完成对对象的初始化操作。__new__​​是类方法,它的第一个参数是类,__init__​​是对象方法,它的第一个参数是对象。

  • 可以这样理解

    • __new__​​方法返回一个空对象,就类似一张白纸;
    • __init__​​获取__new__​​方法中产生的白纸​​在上面画不同的图案。

# __del__

当对象在内存中被释放时,自动触发执行。

注:此方法一般无须定义,因为Python是一门高级语言,有 ​内存管理、垃圾回收机制​,程序员在使用时无需关心内存的分配和释放,因为此工作都是交给Python解释器来执行,所以,__del__​ 的调用是由解释器在进行垃圾回收时自动触发执行的。

# __del__
class Foo:
    def __del__(self):
        print('__del__() called')

      
# ipython 测验
In [29]: f = Foo()

In [30]: del f
__del__() called

#__call__

让类的实例的行为表现的像函数一样,你可以调用它们,将一个函数当做一个参数传到另外一个函数中等等。这是一个非常强大的特性,其让Python编程更加舒适甜美。​对象后面加括号,触发执行​。

注:__init__​ 方法的执行是由创建对象触发的,即:对象 = 类名()​ ;而对于 __call__​ 方法的执行是由对象后加括号触发的,即:对象()​ 或者 类()()

__call__​ 在那些 ​类的实例经常改变状态的时候会非常有效​。调用这个实例是一种改变这个对象状态的直接和优雅的做法。用一个实例来表达最好不过了:

# __call__
class Rect(object)
    """
    调用实例对象来改变矩形的位置
    """

    def __init__(self, x, y):
        # x, y代表矩形坐标
        self.x, self.y = x, y

    def __call__(self, x, y):      
        # 改变实体的位置
        self.x, self.y = x, y


# ipython 测验
In [33]: r = Rect(10, 10)

In [34]: r.x, r.y
Out[34]: (10, 10)

In [35]: r(0, 0)

In [36]: r.x, r.y
Out[36]: (0, 0)

In [37]: r(100, 100)

In [38]: r.x, r.y
Out[38]: (100, 100)

#__str__

如果一个类中定义了__str__方法,那么在打印 对象 时,默认输出该方法的返回值。

In [65]: # __str__
    ...: class Foo(object):
    ...:     pass
    ...:

In [66]: f = Foo()

In [67]: print(f)
<__main__.Foo object at 0x00000210E2715608>

In [68]: class Foo(object):
    ...:
    ...:     def __str__(self):
    ...:         return '< Custom Foo object str >'
    ...:

In [69]: f = Foo()

In [70]: print(f)
< Custom Foo object str >

# __repr__

__repr__​用于返回一个对象的“官方”字符串表示形式。当我们在交互式环境中打印一个对象时,会自动调用其 repr 方法来显示该对象的字符串表示形式。

注:将repr的返回值传递给eval函数,应该得到一个跟原来相同的Python对象。

# 容器

# __getitem__​ **、**​__setitem__​ **、**​__delitem__

用于索引操作,如字典。以上分别表示获取、设置、删除数据。

用于切片操作,如列表。

# 字典示例

# __getitem__、__setitem__、__delitem__
class MyDict(object):

    def __init__(self):
        self.my_dict = dict()

    def __getitem__(self, key):
        print('__getitem__() ', key)
        return self.my_dict.get(key, None)

    def __setitem__(self, key, value):
        print('__setitem__() ', key, value)
        self.my_dict.update(key=value)

    def __delitem__(self, key):
        print('__delitem__() ', key)
        del self.my_dict[key]


# ipython 测验      
In [33]: mdict = MyDict()

In [34]: print(mdict['name'])
__getitem__()  name
None

In [35]: # 新增

In [36]: mdict['name'] = 'hui'
__setitem__()  name hui

In [37]: mdict['age'] = 21
__setitem__()  age 21

In [38]: mdict['name']
__getitem__()  name
Out[38]: 'hui'

In [39]: mdict['age']
__getitem__()  age
Out[39]: 21

In [40]: # 更新

In [41]: mdict['name'] = 'jack'
__setitem__()  name jack

In [42]: mdict['name']
__getitem__()  name
Out[42]: 'jack'

In [43]: # 删除

In [44]: del mdict['age']
__delitem__()  age

In [45]: print(mdict['age'])
__getitem__()  age
None

# 列表示例

# 切片操作
class MyList(object):

    def __init__(self):
        self.mlist = list()

    def __getitem__(self, index):
        print('__getitem__() called')
        print(index)
        if isinstance(index, slice):
            return self.mlist[index]

    def __setitem__(self, index, value):
        print('__getitem__() called')
        print(index, value)
        if isinstance(index, slice):
            self.mlist[index] = value

    def __delitem__(self, index):
        print('__delitem__() called')
        if isinstance(index, slice):
            del self.mlist[index]
   
  
# ipython 
In [70]: mlist = MyList()

In [71]: mlist[0]
__getitem__() called
0

In [72]: mlist[0:-1]
__getitem__() called
slice(0, -1, None)
Out[72]: []

In [73]: mlist[:] = [1,2,3]
__getitem__() called
slice(None, None, None) [1, 2, 3]

In [74]: mlist[:]
__getitem__() called
slice(None, None, None)
Out[74]: [1, 2, 3]

In [75]: mlist[0:2]
__getitem__() called
slice(0, 2, None)
Out[75]: [1, 2]

In [76]: mlist[::-1]
__getitem__() called
slice(None, None, -1)
Out[76]: [3, 2, 1]

In [77]: mlist[0]
__getitem__() called
0

In [78]: mlist[0:1]
__getitem__() called
slice(0, 1, None)
Out[78]: [1]

In [79]: del mlist[0:1]
__delitem__() called

In [80]: mlist[:]
__getitem__() called
slice(None, None, None)
Out[80]: [2, 3]

注意: 当进行 mlist[0]​​ 操作的时候传递并不是一个 slice​​ 对象,不是一个 int​​ 类型的数字,所以不能把索引为 0​​ 的值取出来,改成 mlist[0, 1]​​ 或者在 __getitem__()​​ 的方法中新增数字判断,大家可以尝试一下。

#__missing__

__missing__​魔术方法是在 Python 字典中的一个特殊方法。当我们在一个字典中访问一个不存在的键时,Python 会调用这个方法,并且将不存在的键作为参数传递给它。

通过重写这个方法,我们可以定义自己的逻辑来处理不存在的键,而不是默认的抛出 KeyError 异常。下面是一个简单的例子:

class MyDict(dict):
    def __missing__(self, key):
        return f'Key {key} does not exist.'

d = MyDict(a=1, b=2)
print(d['c'])  # 输出: Key c does not exist.

需要注意的是,__missing__​ 方法仅在使用 []​ 操作符(而不是 get()​ 等方法)访问字典时才会被调用。

同时,这也是python标准库中defaultdict的实现原理

class defaultdict(dict):
    def __init__(self, default_factory=None, **kwargs):
        if (default_factory is not None and
                not callable(default_factory)):
            raise TypeError('first argument must be callable')
        dict.__init__(self, **kwargs)
        self.default_factory = default_factory

    def __missing__(self, key):
        if self.default_factory is None:
            raise KeyError(key)
        self[key] = value = self.default_factory()
        return value

# __iter__​ 和 __next__

  • 在 Python 中一切皆对象,对象的抽象就是类,而对象的集合就是容器。

  • 所有的容器都是可迭代的(iterable)。

    • 迭代和枚举不完全一样。迭代可以想象成是你去买苹果,卖家并不告诉你他有多少库存。这样,每次你都需要告诉卖家,你要一个苹果,然后卖家采取行为:要么给你拿一个苹果;要么告诉你,苹果已经卖完了。你并不需要知道,卖家在仓库是怎么摆放苹果的。
  • 把一个类作为一个迭代器使用需要在类中实现两个方法 __iter__​​与 __next__​​。

    • 迭代器(iterator)提供了一个 next 的方法。
    • 而可迭代对象,通过 iter() 函数返回一个迭代器,再通过 next() 函数实现遍历。for in​​语句将这个过程隐式化。
    • 除了for​​能接收可迭代对象,list、set、tuple​​等也能接收。
    class Fib(object):
    
        def __init__(self, num):
            self.num = num
            self.a, self.b = 0, 1
            self.idx = 0
    
        def __iter__(self):
            return self
    
        def __next__(self):
            if self.idx < self.num:
                self.a, self.b = self.b, self.a + self.b
                self.idx += 1
                return self.a
            raise StopIteration()
    
  • 可以通过isinstance​的方法来判断一个对象是否可迭代

    In [1]: from collections import Iterable
    
    In [2]: isinstance([], Iterable)
    Out[2]: True
    

# 上下文管理器

# __enter__​ **、**​__exit__

with​ 声明是从 Python2.5​ 开始引进的关键词。你应该遇过这样子的代码:

with open('foo.txt') as bar:
    # do something with bar
    pass

with​ 声明的代码段中,我们可以做一些对象的开始操作和退出操作,还能对异常进行处理。这需要实现两个魔术方法: enterexit

__enter__(self):

定义了当使用 with​ 语句的时候,会话管理器在块被初始创建时要产生的行为。请注意,__enter__​ 的返回值与 with​ 语句的目标或者 as​ 后的名字绑定。

__exit__(self, exception_type, exception_value, traceback):

定义了当一个代码块被执行或者终止后,会话管理器应该做什么。它可以被用来处理异常、执行清理工作或做一些代码块执行完毕之后的日常工作。如果代码块执行成功,exception_type,exception_value,和traceback​ 将会为 None​ 。否则,你可以选择处理这个异常或者是直接交给用户处理。如果你想处理这个异常的话,请确保__exit__​ 在所有语句结束之后返回 True​。如果你想让异常被会话管理器处理的话,那么就让其产生该异常。

# 属性描述符

# 概述

在 Python 中,属性描述符是一种定义了 __get__​,__set__​ 或 __delete__​ 方法的对象,它可以被用于控制对 Python 类中的属性的访问。当一个对象被用作属性描述符时,Python 解释器会调用其 __get__​、__set__​ 和 __delete__​ 方法来获取、设置或删除对应的属性值。

属性描述符通常被用于实现高级属性访问机制,例如类型检查、惰性计算等。例如,你可以使用属性描述符来限制属性的值必须是特定类型的实例,或者只有在访问属性时才计算属性的值。

另外,属性描述符还可以用于实现一些特殊的方法,例如 property​、classmethod​、staticmethod​ 等。这些方法都是属性描述符的实例,它们定义了 __get__​ 方法来实现特殊的行为。例如,property​ 类定义了一个 __get__​ 方法,它可以让一个方法像属性一样访问。当你通过属性名称访问这个属性时,property​ 类会自动调用这个方法来获取属性的值。

总之,属性描述符是 Python 中一种强大的机制,它可以被用于实现高级属性访问和一些特殊的方法。对于 Python 中一些高级的特性,例如属性、类方法和静态方法,属性描述符起着至关重要的作用。

# __get__​、__set__​、__delete__

__get__​ 方法用于描述属性的获取行为,当我们访问一个属性时,Python 会先调用该属性的 __get__​ 方法,该方法需要返回一个值作为属性的值。

__set__​ 方法用于描述属性的设置行为,当我们给一个属性赋值时,Python 会调用该属性的 __set__​ 方法,该方法需要接收一个参数,即赋给属性的新值。

__delete__​ 方法用于描述属性的删除行为,当我们使用 del​ 关键字删除一个属性时,Python 会调用该属性的 __delete__​ 方法,该方法不需要接收参数。

我们可以利用这些方法,自定义一个描述符,通过在类中定义描述符实例,来控制属性的访问、修改和删除行为,从而实现属性的各种特殊用途,比如类型检查、限制访问、懒惰计算等等。

# 示例

以下是一个自定义描述符的示例,该描述符用于限制属性只能是正整数:

class PositiveIntegerDescriptor:
    def __init__(self):
        self.value = None

    def __get__(self, instance, owner):
        return self.value

    def __set__(self, instance, value):
        if isinstance(value, int) and value > 0:
            self.value = value
        else:
            raise ValueError('Value must be a positive integer.')

    def __delete__(self, instance):
        self.value = None

class MyClass:
    x = PositiveIntegerDescriptor()

在上面的代码中,PositiveIntegerDescriptor​ 是一个描述符类,它定义了 __get__​、__set__​ 和 __delete__​ 三个方法。MyClass​ 是一个普通的类,其中定义了一个属性 x​,它是 PositiveIntegerDescriptor​ 的一个实例。

测试一下这个类:

>>> obj = MyClass()
>>> obj.x = 1
>>> obj.x
1
>>> obj.x = -1
Traceback (most recent call last):
  ...
ValueError: Value must be a positive integer.
>>> obj.x = '1'
Traceback (most recent call last):
  ...
ValueError: Value must be a positive integer.
>>> del obj.x
>>> obj.x is None
True

可以看到,当我们给 x​ 赋值时,会自动调用 PositiveIntegerDescriptor​ 实例的 __set__​ 方法,如果值不是正整数就会抛出异常。当我们访问 x​ 属性时,会自动调用 PositiveIntegerDescriptor​ 实例的 __get__​ 方法,返回当前属性的值。当我们使用 del​ 删除 x​ 属性时,会自动调用 PositiveIntegerDescriptor​ 实例的 __delete__​ 方法,将属性值设置为 None​。

# 对象展示

# __name__​和__qulname__

__qualname__​ 是一个 Python 的内置属性,用于返回一个函数或类的“限定名称”。限定名称包括所有层级的父级名称,以表示该函数或类的完整名称。

通常情况下,该属性与 __name__​ 类似,但在类中,__qualname__​ 还会包括其父类的名称。在嵌套函数中,该属性也会包括外部函数的名称。

下面是一个示例,展示了如何使用 __qualname__​:

class MyClass:
    def my_method(self):
        pass

print(MyClass.my_method.__name__)       # my_method
print(MyClass.my_method.__qualname__)   # MyClass.my_method

# 类相关

# __subclasshook__

__subclasshook__​是Python中一个特殊的类方法,用于在实现抽象基类时进行类型检查。这个方法可以被用来在类继承树中自动地寻找子类,并且决定这个子类是否可以被认为是一个合法的实例。

具体来说,当我们定义了一个抽象基类时,我们通常会使用isinstance()​或issubclass()​函数来检查某个类是否是合法的子类。但是这样的方法可能存在一些缺陷,比如如果一个类直接继承了抽象基类的子类而不是直接继承抽象基类本身,那么使用这些函数就无法正确地判断这个类是否是合法的子类。为了解决这个问题,我们可以使用__subclasshook__​方法来检查子类是否实现了抽象基类的所有方法,从而决定这个子类是否是一个合法的实例。

有如下示例:

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def speak(self):
        pass

    @classmethod
    def __subclasshook__(cls, other):
        if cls is Animal:
            if any('speak' in B.__dict__ for B in other.__mro__):
                return True
        return NotImplemented

class Dog:
    def speak(self):
        print("Woof!")

class Cat:
    pass

dog = Dog()
cat = Cat()

print(issubclass(Dog, Animal)) # True
print(issubclass(Cat, Animal)) # False
print(isinstance(Dog, Animal)) # False
print(isinstance(Cat, Animal)) # False
print(isinstance(dog, Animal)) # True
print(isinstance(cat, Animal)) # False

当使用 issubclass(cls, class_or_tuple)​ 函数判断一个类 cls​ 是否是另一个类或者其子类时,Python 会按照以下的方式进行判断:

  1. 如果 class_or_tuple​ 是一个类对象,则 Python 会检查 cls​ 是否直接或间接地继承自 class_or_tuple​,如果是则返回 True​,否则返回 False​。
  2. 如果 class_or_tuple​ 是一个元组,则 Python 会检查其中每一个元素是否都是类对象,如果不是则抛出 TypeError​ 异常。然后 Python 会遍历 class_or_tuple​ 中的每一个类对象,检查 cls​ 是否直接或间接地继承自其中的任意一个类,如果是则返回 True​,否则返回 False​。

当使用 isinstance(obj, class_or_tuple)​ 函数判断一个对象 obj​ 是否是某一个类或者其子类的实例时,Python 会按照以下的方式进行判断:

  1. 如果 class_or_tuple​ 是一个类对象,则 Python 会检查 obj​ 是否是该类或者其子类的实例,如果是则返回 True​,否则返回 False​。
  2. 如果 class_or_tuple​ 是一个元组,则 Python 会检查其中每一个元素是否都是类对象,如果不是则抛出 TypeError​ 异常。然后 Python 会遍历 class_or_tuple​ 中的每一个类对象,检查 obj​ 是否是该类或者其子类的实例,如果是则返回 True​,否则返回 False​。

__subclasshook__​ 方法在 issubclass(cls, class_or_tuple)​ 函数和 isinstance(obj, class_or_tuple)​ 函数中都会被调用,来协助确定一个类是否是另一个类或者其子类的实例。

issubclass(cls, class_or_tuple)​ 函数调用时,Python 会在 class_or_tuple​ 中的每一个类对象中查找是否有 __subclasshook__​ 方法,如果有则调用该方法,并传入一个参数 cls​,如果该方法返回一个非 None​ 的值,则该值将作为 issubclass​ 函数的返回值;否则,Python 会继续在 class_or_tuple​ 中的下一个类对象中查找 __subclasshook__​ 方法,直到找到一个返回非 None​ 值的方法或者查找完所有的类对象。

isinstance(obj, class_or_tuple)​ 函数调用时,Python 会在 obj​ 的类型和 class_or_tuple​ 中的每一个类对象中查找是否有 __subclasshook__​ 方法,如果有则调用该方法,并传入一个参数 obj.__class__​,如果该方法返回一个非 None​ 的值,则该值将作为 isinstance​ 函数的返回值;否则,Python 会继续在 obj 的类型和 class_or_tuple 中的其它类对象中查找是否有 __subclasshook__​方法,如果最终都没有找到,则返回 False。

因此,​__subclasshook__​方法可以用于动态地控制类的继承关系,从而影响 isinstance 和 issubclass 函数的行为。例如,如果一个类想要在 isinstance 或 issubclass 函数调用时,被判定为另一个类的子类,可以在该类中定义 ​__subclasshook__​方法来实现这个功能。

# __dict__​、__slot__

  • 每个类和实例都有一个dict属性,其包含了当前类的类属性或者当前实例的实例属性。

    class Sample:
        name = 'rocky'
    
    Sample.__dict__
    # mappingproxy({'__module__': '__main__', 'name': 'rocky', '__dict__': <attribute '__dict__' of 'Sample' objects>, '__weakref__': <attribute '__weakref__' of 'Sample' objects>, '__doc__': None})
    
    
    
  • 类的dict和实例的dict是不同的。

  • 每创建一个实例都会创建一个新的dict,这个新创建的dict会占用一定空间。

  • 可以在类中使用__slots__​​属性,之后类和实例都不会再有dict属性

    • 类中定义了__slots__​​属性之后,实例不会再有__slots__​​之外的属性。

      • 如果一些函数或者库需要利用到其他属性,这时是会报错的,除非把这些属性加到__slots__​​之中。例如弱引用以来的__weakref__​​属性。
    • 类的slots和实例的slot使用的是同一个,实际上只是一个属性名组成的元组,复用是可以的

    • 通过类可以对这两个属性进行赋值和修改

    • 对于实例而言,不能修改只读的类的属性,但尚未赋值的属性能够通过实例赋值,并且不会传递会类,可以看做是在本地创建了一个同名的变量。

    • 实际上使用了slot之后对象不会再用dict去存储属性,而是用元组去存储(按照slot中的顺序,以此来避免dict对象内存空间的消耗)。

    • 同时对象将无法新增新的属性,因为新增属性需要使用到__dict__​​属性-除非把__dict__​​属性加到__slots__​​属性之中。

    • 不过注意__slots__​​存在的真正意义是做内存优化,而不是禁止类的用户新增实例属性。

    class A:
        __slots__ = ['a','b','c']
    
    
    a1 = A()
    a2 = A()
    print(A.__dict__) # {'__module__': '__main__', '__slots__': ['a', 'b', 'c'], 'a': <member 'a' of 'A' objects>, 'b': <member 'b' of 'A' objects>, 'c': <member 'c' of 'A' objects>, '__doc__': None}
    print(a1.__slots__ is A.__slots__)  # True
    print(a1.__slots__ is a2.__slots__) # True
    print(A.__slots__)
    print(a1.__slots__)
    print(a2.__slots__)
    
    a1.a = 1
    print(A.a)      # <member 'a' of 'A' objects>
    print(a1.a)     # 1
    # print(a2.a)     # AttributeError: a
    
    A.a = 2
    print(A.a)      # 2
    print(a1.a)     # 2
    print(a2.a)     # 2
    
    # a1.a = 1        #  'A' object attribute 'a' is read-only  
    
    
    class B:
        pass
    print(B.__dict__) # {'__module__': '__main__', '__dict__': <attribute '__dict__' of 'B' objects>, '__weakref__': <attribute '__weakref__' of 'B' objects>, '__doc__': None}
    

# __getattribute__​和__getattr__

  • 当访问一个类的属性的时会默认调用__getattribute__
  • 当访问的是类和实例都未定义的属性时则调用 __getattr__
  • object类自带__getattribute__​拦截方法,只是不进行任何操作直接返回,但是注意不带__getattr__
  • 同理含有setattr,delattr
from utils import debug
class A:
  
    def __init__(self) -> None:
        self.p = 1

    @debug
    def __getattribute__(self, item):
        """
        属性查找首先调用__getattribute__
        """
        return object.__getattribute__(self, item)
  
    @debug
    def __getattr__(self,item):
        '''
        当属性不存在时则调用__getattr__
        '''
        return "NOTHING"
        # return object.__getattr__(self, item)

    @debug
    def fun(self):
        pass

def main():
    a = A()
    a.fun()
    '''
Calling __getattribute__
    with args: (<__main__.A object at 0x00000235B01113A0>, 'fun')
    kwargs: {}
    local variables: {'a': <__main__.A object at 0x00000235B01113A0>}
Calling fun
    with args: (<__main__.A object at 0x00000235B01113A0>,)
    kwargs: {}
    local variables: {'a': <__main__.A object at 0x00000235B01113A0>}
    '''
    a.p
    '''
Calling __getattribute__
    with args: (<__main__.A object at 0x00000235B01113A0>, 'p')
    kwargs: {}
    local variables: {'a': <__main__.A object at 0x00000235B01113A0>}
    '''
    a.nothing
    '''
Calling __getattribute__
    with args: (<__main__.A object at 0x00000235B01113A0>, 'nothing')
    kwargs: {}
    local variables: {'a': <__main__.A object at 0x00000235B01113A0>}
Calling __getattr__
    with args: (<__main__.A object at 0x00000235B01113A0>, 'nothing')
    kwargs: {}
    local variables: {'a': <__main__.A object at 0x00000235B01113A0>}
    '''


if __name__ == '__main__':
    main()
    print(hasattr(object ,'__getattr__')) # False

class Message():
    def __init__(self, note):
        self.note = note

    def remove_note(self):
        del self.note

    def get_note(self):
        return self.note

    def __setattr__(self, key, value):
        print("setAttr key = {},value = {}".format(key, value))
        # 如果这里使用了这个方法,那么必须明确的使用dict这个方法,将属性保存在其中
        self.__dict__[key] = value

    def __getattr__(self, item):
        print("getAttr item = {}".format(item))
        # 当属性不存在的时候会调用这个方法;
        return "属性不存在"

    def __delattr__(self, item):
        print("delattr item = {}".format(item))
        # 从字典里面删除属性
        self.__dict__.pop(item)


def main():
    msg = Message("www.baidu.com")
    print("获取存在的属性 {}".format(msg.get_note()))
    print("获取不存在的属性 {}".format(msg.contant))
    msg.remove_note()


if __name__ == '__main__':
    main()

# setAttr key = note,value = www.baidu.com
# 获取存在的属性 www.baidu.com
# getAttr item = contant
# 获取不存在的属性 属性不存在
# delattr item = note

# __copy__​ **、**​__deepcopy__

有时候,尤其是当你在处理可变对象时,你可能想要复制一个对象,然后对其做出一些改变而不希望影响原来的对象。这就是Python的copy所发挥作用的地方。

__copy__(self):

定义了当对你的类的实例调用 copy.copy()​ 时所产生的行为。copy.copy()​ 返回了你的对象的一个浅拷贝——这意味着,当实例本身是一个新实例时,它的所有数据都被引用了——例如,当一个对象本身被复制了,它的数据仍然是被引用的(因此,对于浅拷贝中数据的更改仍然可能导致数据在原始对象的中的改变)。

__deepcopy__(self, memodict={ }):

定义了当对你的类的实例调用 copy.deepcopy()​时所产生的行为。copy.deepcopy()​ 返回了你的对象的一个深拷贝——对象和其数据都被拷贝了。memodict​ 是对之前被拷贝的对象的一个缓存——这优化了拷贝过程并且阻止了对递归数据结构拷贝时的无限递归。当你想要进行对一个单独的属性进行深拷贝时,调用copy.deepcopy()​,并以 memodict​ 为第一个参数。