python 魔术方法
前言
在做python开发的过程中,我们大家都会遇到在class(类)中使用双下划线的方法,这些都是我们经常所说的"魔法"方法.这些方法可以对类添加特殊的功能,使用恰当可以很大的提升我们在开发过程中的便捷性,方便的进行扩展.
概览
目前我们常见的魔法方法大致可分为以下几类:
- 构造与初始化
- 类的表示
- 访问控制
- 比较操作
- 容器类操作
- 可调用对象
- Pickling序列化
我们这次主要介绍这几类常用魔法方法:
1.构造与初始化
__init__
构造方法是我们使用频率最高的魔法方法了,几乎在我们定义类的时候,都会去定义构造方法,它的主要作用就是在初始化一个对象时,定义这个对象的初始值。
class Person(object):
def __init__(self, name, age):
self.name = name
self.age = age
p1 = Person('Jack', 25)
p2 = Person('shuke', 20)
__new__
- 事实上,当我们理解了new方法后,我们还可以利用它来做一些其他有趣的事情,比如实现 设计模式中的 单例模式(singleton)
- 依照Python官方文档的说法,new方法主要是当你继承一些不可变的class时(比如int, str, tuple), 提供给你一个自定义这些类的实例化过程的途径。还有就是实现自定义的metaclass
- 这个方法我们一般很少定义,不过我们在一些开源框架中偶尔会遇到定义这个方法的类。实际上,这才是"真正的构造方法",它会在对象实例化时第一个被调用,然后再调用init,它们的区别主要如下:
- new的第一个参数是cls,而init的第一个参数是self
- new返回值是一个实例,而init没有任何返回值,只做初始化操作
- new由于是返回一个实例对象,所以它可以给所有实例进行统一的初始化操作
- 由于new优先于init调用,且返回一个实例,所以我们可以利用这种特性,每次返回同一个实例来实现一个单例类:
__new__的作用:
class PositiveInteger(int):
def __init__(self, value):
super(PositiveInteger, self).__init__(self, abs(value))
i = PositiveInteger(-3)
print(i)
但运行后会发现,结果根本不是我们想的那样,我们仍然得到了-3。这是因为对于int这种不可变的对象,我们只有重载它的new方法才能起到自定义的作用。
修改后的代码如下:
class PositiveInteger(int):
def __new__(cls, value):
return super(PositiveInteger, cls).__new__(cls, abs(value))
i = PositiveInteger(-3)
print(i)
通过重载new方法,我们实现了需要的功能.
class g(float):
"""千克转克"""
def __new__(cls, kg):
return float.__new__(cls, kg * 2)
# 50千克转为克
a = g(50)
print(a) # 100.0
print(a + 100) # 200.0 由于继承了float,所以可以直接运算,非常方便!
用new来实现单例
因为类每一次实例化后产生的过程都是通过new来控制的,所以通过重载new方法,我们 可以很简单的实现单例模式。
# 写法一
class Singleton(object):
def __new__(cls):
# 关键在于这,每一次实例化的时候,我们都只会返回这同一个instance对象
if not hasattr(cls, 'instance'):
cls.instance = super(Singleton, cls).__new__(cls)
return cls.instance
obj1 = Singleton()
obj2 = Singleton()
obj1.attr1 = 'value1'
print(obj1.attr1, obj2.attr1)
print(obj1 is obj2)
"""
>>>
value1 value1
True
"""
可以看到obj1和obj2是同一个实例。
# 写法二
class Singleton(object):
"""单例"""
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super(Singleton, cls).__new__(cls, *args, **kwargs)
return cls._instance
class MySingleton(Singleton):
pass
a = MySingleton()
b = MySingleton()
print(a is b)
"""
>>>
True
"""
2. del析构方法
这个方法代表析构方法,也就是在对象被垃圾回收时被调用。但是请注意,执行del x不一定会执行此方法。
由于Python是通过引用计数来进行垃圾回收的,也就是说,如果这个实例还是有被引用到,即使执行del销毁这个对象,但其引用计数还是大于0,所以不会触发执行del。
例子:
此时我们没有对实例进行任何操作时,del在程序退出后被调用。
class Person(object):
def __del__(self):
print('__del__')
a = Person()
print('exit')
"""
exit
__del__
"""
由于此实例没有被其他对象所引用,当我们手动销毁这个实例时,del被调用后程序正常退出。
class Person(object):
def __del__(self):
print('__del__')
a = Person()
b = a # b引用a
del a # 手动销毁,不触发__del__
print('exit')
"""
exit
__del__
"""
此时实例有被其他对象引用,尽管我们手动销毁这个实例,但依然不会触发del方法,而是在程序正常退出后被调用执行。
为了保险起见,当我们在对文件、socket进行操作时,要想安全地关闭和销毁这些对象,最好是在try异常块后的finally中进行关闭和释放操作!
3. 类的表示
str/repr
这两个魔法方法一般会放到一起进行讲解,它们的主要差别为:
str强调可读性,而repr强调准确性/标准性
str的目标人群是用户,而repr的目标人群是机器,它的结果是可以被执行的
%s调用str方法,而%r调用repr方法
来看几个例子,了解内置类实现这2个方法的效果:
>>> a = 'hello'
>>> str(a)
'hello'
>>> '%s' % a # 调用__str__
'hello'
>>>
>>> repr(a) # 对象a的标准表示,也就是a是如何创建的
"'hello'"
>>> '%r' % a # 调用__repr__
"'hello'"
>>>
>>>
>>> import datetime
>>> b = datetime.datetime.now()
>>> str(b)
'2018-05-03 19:08:45.921879'
>>> print(b) # 等同于print str(b)
2018-05-03 19:08:45.921879
>>>
>>>
>>> repr(b) # 展示对象b的标准创建方式(如何创建的
'datetime.datetime(2018, 5, 3, 19, 8, 45, 921879)'
>>> b
datetime.datetime(2018, 5, 3, 19, 8, 45, 921879)
>>>
>>> c = eval(repr(b))
>>> c
datetime.datetime(2018, 5, 3, 19, 8, 45, 921879)
从上面的例子可以看出这两个方法的主要区别,在实际中我们定义类时,一般这样定义即可:
class Person(object):
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
# 格式化,友好对用户展示
return 'name: %s, age: %s' % (self.name, self.age)
def __repr__(self):
# 标准化展示
return "Person('%s', %s)" % (self.name, self.age)
person = Person('zhangsan', 20)
print(str(person)) # name: zhangsan, age: 20
print('%s' % person) # name: zhangsan, age: 20
print(repr(person)) # Person('zhangsan', 20)
print('%r' % person) # Person('zhangsan', 20)
"""
name: zhangsan, age: 20
name: zhangsan, age: 20
Person('zhangsan', 20)
Person('zhangsan', 20)
"""
这里值得注意的是,如果只定义了str或repr其中一个,那会是什么结果?
如果只定义了str_,那么repr(person)输出<main.Person object at 0x10783b400>
如果只定义了repr,那么str(person)与repr(person)结果是相同的
也就是说,repr在表示类时,是一级的,如果只定义它,那么str = repr。
而str展示类时是次级的,用户可自定义类的展示格式,如果没有定义repr,那么repr(person)将会展示缺省的定义。
4. 对象判断
hash/eq
hash方法返回一个整数,用来表示该对象的唯一标识,配合eq方法判断两个对象是否相等(==):
class Person(object):
def __init__(self, uid):
self.uid = uid
def __repr__(self):
return 'Person(%s)' % self.uid
def __hash__(self):
return self.uid
def __eq__(self, other):
return self.uid == other.uid
p1 = Person(1)
p2 = Person(1)
print(p1 == p2)
p3 = Person(2)
print(set([p1, p2, p3])) # 根据唯一标识去重输出 set([Person(1), Person(2)])
"""
True
{Person(1), Person(2)}
"""
如果我们需要判断两个对象是否相等,只要我们重写hash和eq方法就可以完成此功能。此外使用set存放这些对象时,会根据这两个方法进行去重操作。
5. 对象布尔判断
bool
当调用bool(obj)时,会调用bool方法,返回True/False。
class Person(object):
def __init__(self, uid):
self.uid = uid
def __bool__(self):
return self.uid > 10
p1 = Person(1)
p2 = Person(15)
print(bool(p1)) # False
print(bool(p2)) # True
"""
False
True
"""
⚠️: 在Python3中,nonzero被重命名bool
6. 访问控制
访问控制相关的魔法方法,主要涉及以下几个:
setattr:通过.设置属性或setattr(key, value)
getattr:访问不存在的属性
delattr:删除某个属性
getattribute:访问任意属性或方法
来看一个完整的例子:
class Person(object):
def __setattr__(self, key, value):
"""属性赋值"""
if key not in ('name', 'age'):
return
if key == 'age' and value < 0:
raise ValueError()
super(Person, self).__setattr__(key, value)
def __getattr__(self, key):
"""访问某个不存在的属性"""
return 'unknown'
def __delattr__(self, key):
"""删除某个属性"""
if key == 'name':
raise AttributeError()
super(Person, self).__delattr__(key)
def __getattribute__(self, key):
"""所有属性/方法调用都经过这里"""
if key == 'money':
return 100
if key == 'hello':
return self.say
return super(Person, self).__getattribute__(key)
def say(self):
return 'hello'
p1 = Person()
p1.name = 'zhangsan' # 调用__setattr__
p1.age = 20 # 调用__setattr__
print(p1.name) # zhangsan
print(p1.age) # 20
setattr(p1, 'name', 'lisi') # 调用__setattr__
setattr(p1, 'age', 30) # 调用__setattr__
print(p1.name) # lisi
print(p1.age) # 30
p1.gender = 'male' # __setattr__中忽略对gender赋值
print(p1.gender) # gender不存在,调用__getattr__返回:unknown
print(p1.money) # money不存在,在__getattribute__中返回100
print(p1.say()) # hello
print(p1.hello()) # hello,调用__getattribute__,间接调用say方法
del p1.name # __delattr__中引发AttributeError
p2 = Person()
p2.age = -1 # __setattr__中引发ValueError
- setattr
通过此方法,对象可在在对属性进行赋值时进行控制,所有的属性赋值都会经过它。
一般常用于对某些属性赋值的检查校验逻辑,例如age不能小于0,否则认为是非法数据等等。 - getattr
很多同学以为此方法是和setattr完全对立的,其实不然!
这个方法只有在访问某个不存在的属性时才会被调用,看上面的例子,由于gender属性在赋值时,忽略了此字段的赋值操作,所以此属性是没有被成功赋值给对象的。当访问这个属性时,getattr被调用,返回unknown。 - del
删除对象的某个属性时,此方法被调用。一般常用于某个属性必须存在,否则无法进行后续的逻辑操作,会重写此方法,对删除属性逻辑进行检查和校验。 - getattribute
这个方法我们很少用到,它与getattr很容易混淆。它与前者的区别在于:
getattr访问某个不存在的属性被调用,getattribute访问任意属性被调用
getattr只针对属性访问,getattribute不仅针对所有属性访问,还包括方法调用
7. Python的类下面的item系列
xxxitem:使用 [''] 的方式操作属性时被调用
setitem:每当属性被赋值的时候都会调用该方法,因此不能再该方法内赋值 self.name = value 会死循环
getitem:当访问不存在的属性时会调用该方法
delitem:当删除属性时调用该方法
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# __author__ = "shuke"
# Date: 2018/5/2
class A(object):
def __init__(self):
self['B'] = "BB"
self['D'] = "DD"
del self['B']
def __setitem__(self, name, value):
'''''
@summary: 每当属性被赋值的时候都会调用该方法,因此不能再该方法内赋值 self.name = value 会死循环
'''
print("__setitem__:Set %s Value %s" % (name, value))
self.__dict__[name] = value
def __getitem__(self, name):
'''''
@summary: 当访问不存在的属性时会调用该方法
'''
print("__getitem__:No attribute named '%s'" % name)
return 123
def __delitem__(self, name):
'''''
@summary: 当删除属性时调用该方法
'''
print("__delitem__:Delect attribute '%s'" % name)
del self.__dict__[name]
print(self.__dict__)
if __name__ == "__main__":
X = A()
X['bb'] = "BB"
print(X.__dict__)
"""
>>>
__setitem__:Set B Value BB
__setitem__:Set D Value DD
__delitem__:Delect attribute 'B'
{'D': 'DD'}
__setitem__:Set bb Value BB
{'D': 'DD', 'bb': 'BB'}
"""
越是强大的魔法方法,责任越大,如果你不能正确使用它,最好还是不用为好,否则在出现问题时很难排查!
参考原文
魔术方法二
延伸