Python中有趣的魔术方法

Python中有趣的魔术方法

由单例引发的思考

相信熟悉 python 的开发者,都熟悉以下代码,它实现了 单例,但它为什么可以实现 单例 呢?如果此方法中没有返回值会发生什么呢?可以利用特殊方法实现什么呢?

class Singleton:
    def __new__(cls, *args, **kwargs):
        if not hasattr(cls, '_instance'):
            cls._instance = super(Singleton, cls).__new__(cls, *args, **kwargs)
        return cls._instance

魔术方法

官方文档中称为 特殊方法(Special Method),可以通过重写对应的方法来实现 操作符重载,比如我们可以通过重写 __new__ 方法来实现单例,也可以通过重写 __init__ 来实现自定义初始化实例的动作,当然,也可以实现更多的功能。

__new__

官方文档中对此方法的解释是 Called to create a new instance of class *cls*.,顾名思义,创建类的实例,是通过调用此方法来实现的。

如果 __new__ 方法中没有返回值会发生什么呢?如下代码

class Cat:
    def __new__(cls, *args, **kwargs):
        pass

tom = Cat()
print(tom)
>>> None

可以看到未能创建我们期待的实例,此点可以证明创建实例是在 __new__ 中完成的,当需要重写此方法时,需要保证返回的实例是真实需要的对象,如下示例,此时虽然返回了一个对象,但它并不是正确的对象

class Dog:
    pass

class Cat:
    def __new__(cls, *args, **kwargs):
        return Dog()


tom = Cat()
print(tom)
>>> <__main__.Dog object at 0x10afa7400>

正确的代码如下,当然,大多数情况下,此部分代码是可以省略的

class Cat:
    def __new__(cls, *args, **kwargs):
        return super(Cat, cls).__new__(cls, *args, **kwargs)

tom = Cat()
print(tom)
>>> <__main__.Cat object at 0x100670eb8>

这里留下一个问题:上面正确的代码在实际应用过程中真的正确吗?(答案见__init__部分)

__init__

官方文档的解释是 Called after the instance has been created (by __new__()), but before it is returned to the caller.,可以看到__new____init__ 是协作来创建一个自定义的实例对象的。

沿用 __new__ 中最后的代码示例,如此时,需要在初始化实例对象时,给予它一些自定义的属性,如下示例,猜一猜输出是什么?

class Cat:
  
    def __new__(cls, *args, **kwargs):
        return super(Cat, cls).__new__(cls, *args, **kwargs)
      
    def __init__(self, name):
        self.name = name

tom = Cat('Tom')

在实际运行过程中,会发现上面代码报错

>>> Traceback (most recent call last):
    ...
>>> TypeError: object() takes no parameters

这是因为在 __new__ 返回给调用者之前,会调用 __init__ 来初始化实例对象,文档中的原话是

Called after the instance has been created (by __new__()), but before it is returned to the caller. The arguments are those passed to the class constructor expression. If a base class has an __init__() method, the derived class’s __init__() method, if any, must explicitly call it to ensure proper initialization of the base class part of the instance.

因此正确的代码如下,所以如果没有必须要改写 __new__ 才能实现的原因(如单例),尽量不要修改 __new__ 方法

class Cat:

    def __new__(cls, *args, **kwargs):
        return super(Cat, cls).__new__(cls)

    def __init__(self, name):
        self.name = name

tom = Cat('Tom')
print(tom)

另一个有趣的问题是**如果 __init__ 有返回值会发生什么?**如下示例

class Cat:

    def __new__(cls, *args, **kwargs):
        return super(Cat, cls).__new__(cls)

    def __init__(self, name):
        self.name = name
        return self.name

tom = Cat('Tom')
print(tom)

上方示例代码将抛出 TypeError ,这是因为 __init__ 方法不允许返回任何非 None 值,这点在官方文档中也有体现

Because __new__() and __init__() work together in constructing objects (__new__() to create it, and __init__() to customize it), no non-None value may be returned by __init__(); doing so will cause a TypeError to be raised at runtime.

__str__

官方文档的解释是通过 str()format()print() 方法调用时生成一个对象格式的“非正式”或格式良好的字符串。如下示例

class Cat:

    def __init__(self, name):
        self.name = name

    def __str__(self):
        return f'Cat Object: {self.name}'

tom = Cat('Tom')
print(str(tom))
print(f'{tom}')
print('{}'.format(tom))
>>> Cat Object: Tom
>>> Cat Object: Tom
>>> Cat Object: Tom

__repr__

repr() 内置函数调用以输出一个对象的“官方”字符串表示,返回值必须是一个字符串对象。此方法大部分的应该场景是方便代码调试。如下示例,在控制台中,将输出对应的信息

>>> class Cat:
>>>    def __new__(cls, *args, **kwargs):
>>>        return super(Cat, cls).__new__(cls)
>>>    def __init__(self, name):
>>>        self.name = name
>>>    def __repr__(self):
>>>        return f'Cat Object: {self.name}'

>>> tom = Cat('Tom')
>>> tom
Cat Object: Tom

__dir__

在调试时,如不熟悉一个对象,经常会使用到 dir() 方法来查看它具有的属性。当一个对象的 __dir__ 方法被重载后,将返回重载后的值,需要注意的是返回值必须是可迭代的,否则将抛出异常

class Cat:

    def __new__(cls, *args, **kwargs):
        return super(Cat, cls).__new__(cls)

    def __init__(self, name):
        self.name = name

    def __dir__(self):
        return ['name']

tom = Cat('Tom')
print(dir(tom))
>>> ['name']

如果__dir__返回的是字符串会,输出的内容会是什么?如下示例,可以看到会将返回值序列化后排序返回

class Cat:

    def __new__(cls, *args, **kwargs):
        return super(Cat, cls).__new__(cls)

    def __init__(self, name):
        self.name = name

    def __dir__(self):
        return '字符串'

tom = Cat('Tom')
print(dir(tom))
>>> ['串', '字', '符']

__dict__

一个字典或其他类型的映射对象,用于存储对象的(可写)属性。当然也可以像 __dir__ 方法一样来修改,需要注意的是,它的返回值是字典对象

class Cat:

    def __init__(self, name):
        self.name = name

tom = Cat('Tom')
print(tom.__dict__)
>>> {'name': 'Tom'}

__slots__

__slots__ 允许我们显式地声明数据成员(例如特征属性)并禁止创建 dictweakref (除非是在 slots 中显式地声明或是在父类中可用。)。如下示例,将尝试添加不在 __slots__ 中的属性时,会抛出异常。

class Cat:

    __slots__ = ('name', 'age',)

    def __init__(self, name):
        self.name = name

tom = Cat('Tom')
tom.voice = '喵'
>>> ...
>>> AttributeError: 'Cat' object has no attribute 'voice'

富比较

__eq____lt____le____ne____gt____ge__ 等方法被统称为 富比较方法,运算符号与方法名称的对应关系如下:x<y 调用 x.__lt__(y)x<=y 调用 x.__le__(y)x==y 调用 x.__eq__(y)x!=y 调用 x.__ne__(y)x>y 调用 x.__gt__(y)x>=y 调用 x.__ge__(y)

下方示例代码中都对比较的两个对象的类型进行了校验,实际上不校验也不会报错,如果做比较的对象不是同一种类型,将执行返回False

class Cat:

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __eq__(self, other):
        # 使用 == 判断两个对象时,会执行此方法
        return isinstance(other, self.__class__) and self.name == other.name

    def __ne__(self, other):
        # 使用 != 判断两个对象时,会执行此方法
        return isinstance(other, self.__class__) and self.age != other.age

    def __le__(self, other):
        # 使用 <= 判断两个对象时,会执行此方法
        return isinstance(other, self.__class__) and self.age <= other.age

    def __lt__(self, other):
        # 使用 < 判断两个对象时,会执行此方法
        return isinstance(other, self.__class__) and self.age < other.age

    def __ge__(self, other):
        # 使用 >= 判断两个对象时,会执行此方法
        return isinstance(other, self.__class__) and self.age >= other.age

    def __gt__(self, other):
        # 使用 > 判断两个对象时,会执行此方法
        return isinstance(other, self.__class__) and self.age > other.age

tom = Cat('Tom', 1)
tom2 = Cat('Tom', 2)
print(tom == tom2)  # 将执行 __eq__ 方法
print(tom != tom2)  # 将执行 __ne__ 方法
print(tom <= tom2)  # 将执行 __le__ 方法
print(tom < tom2)   # 将执行 __lt__ 方法
print(tom >= tom2)  # 将执行 __ge__ 方法
print(tom > tom2)   # 将执行 __gt__ 方法

__hash__

通过内置函数 hash() 调用以对哈希集的成员进行操作,需要注意的是,如果它定义了 __eq__() 但没有定义 __hash__(),则其实例将不可被用作可哈希集的项。

class Cat:

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __eq__(self, other):
        # 使用 == 判断两个对象时,会执行此方法
        return isinstance(other, self.__class__) and self.name == other.name
      
tom = Cat('Tom', 1)
print(hash(tom))

上方代码将抛出 TypeError: unhashable type: 'Cat',如果需要对对象使用 hash() 方法,需要定义 __hash__ 方法,如下示例

class Cat:

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __eq__(self, other):
        # 使用 == 判断两个对象时,会执行此方法
        return isinstance(other, self.__class__) and self.name == other.name

    def __hash__(self):
        return hash((self.name, self.age))

tom = Cat('Tom', 1)
print(hash(tom))

__len__

通常调用此方法来返回对象的长度

class Cat:

    def __init__(self, name, age):
        self.name = name
        self.age = age
        self.voice = ['喵', '喵', '喵']

    def __len__(self):
        return len(self.voice)

tom = Cat('Tom', 1)
print(len(tom))
>>> 3

有个需要注意点的是,如果 __len__ 方法的返回值是 0 ,且此时未定义 __bool__ 方法,做布尔运算中会被认为是假值,如下示例

class Cat:

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __len__(self):
        return 0

tom = Cat('Tom', 1)
print(not tom)
>>> True

__sizeof__

返回一个对象所占用的字节大小。如果是内置对象,总能返回正确的结果,但如果是自定义的对象,大概率返回的是错误的结果,如下示例

class Cat:

    def __init__(self, name, age):
        self.name = name
        self.age = age
        self.voice = ['喵', '喵', '喵']

tom = Cat('Tom', 1)
print(tom.__sizeof__())
print(sys.getsizeof(tom))
>>> 32
>>> 56

在未重新定义 __sizeof__ 的情况下, 增加 self.voice 列表内的元素数量,返回值将维持不变,如果需要准确知道一个对象真实占内存的大小,需要重写 __sizeof__ 方法,如下示例

class Cat:

    def __init__(self, name, age):
        self.name = name
        self.age = age
        self.voice = ['喵', '喵', '喵']

    def __sizeof__(self):
        return sys.getsizeof(self.voice) + sys.getsizeof(self.name) + sys.getsizeof(self.age)

tom = Cat('Tom', 1)
print(tom.__sizeof__())
print(sys.getsizeof(tom))

有个需要注意的点是,__sizeof__()sys.getsizeof() 的返回值,总是不一致的,这是因为如果对象是由垃圾回收器管理,则会增加额外的垃圾收集器的开销。

属性相关的方法

__getattr____setattr____getattribute____delattr__ 是用来定义或操作对象的属性的方法,其中请慎用 __setattr__ 方法。

__getattr__

当使用 a.x 获取对象 a 的属性时,如果属性 x 不存在于 a.__dict__ 中,将执行 __getattr__ 来尝试获取该属性,使用一个示例来说明

class Cat:

    def __init__(self, name):
        self.name = name

    def __getattr__(self, key):
        # self.__dict__ 中包含 key时,不执行此方法
        print('do __getattr__')
        if key in self.__dict__:
            return self.__dict__[key]
        raise AttributeError('Cat Object does not have an attribute named %s' % key)

tom = Cat('Tom')
print(tom.__dict__)
>>> {'name': 'Tom'}
print(tom.name)
>>> Tom
print(tom.voice)
>>> do __getattr__
>>> ...
>>> AttributeError: Cat Object does not have an attribute named voice

可以利用此方法来自定义获取属性,还请根据实际情况自由发挥。

__getattribute__

pythn3 中,只要访问对象的属性,就会调用此方法,因此在方法中不可使用 self.x 来获取对象的属性,会引起无限递归,要避免之,示例如下

class Cat:

    def __init__(self, name):
        self.name = name

    def __getattribute__(self, key):
        print('do __getattribute__')
        return object.__getattribute__(self, key)
      
tom = Cat('Tom')
print(tom.name)
>>> do __getattribute__
>>> Tom
__delattr__

当尝试删除对象的属性时,此方法将被执行,默认将属性从 __dict__ 中删除,此方法较容易理解。

__setattr__

此方法用来给对象添加属性,需要注意的是,不论是在 __init__ 中初始化属性,还是通过 a.x = value 来赋值,都会执行此方法,如下示例

class Cat:

    def __init__(self, name):
        self.name = name

    def __setattr__(self, key, value):
        self.__dict__[key] = value

请注意,这里通过 self.__dict__ 来实现赋值,如果给 Cat 添加 __slots__ 属性会发生什么呢?

class Cat:

    __slots__ = ('name', 'age',)

    def __init__(self, name):
        self.name = name

    def __setattr__(self, key, value):
        self.__dict__[key] = value

tom = Cat('Tom')
>>> ...
>>> AttributeError: 'Cat' object has no attribute '__dict__'

可以看到,创建实例失败,抛出了异常,这是由于 __slots__ 限制的执行过程是在实例创建完成之前,因此正确的方法是

class Cat:

    __slots__ = ('name', 'age',)

    def __init__(self, name):
        self.name = name

    def __setattr__(self, key, value):
        object.__setattr__(self, key, value)
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值