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__
允许我们显式地声明数据成员(例如特征属性)并禁止创建 dict 和 weakref (除非是在 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)