魔法方法
在Python中,所有以双下划线包起来的方法,都统称为"魔术方法"
。比如我们接触最多的__init__
。 魔法方法帮助我们定义更加符合 Python 风格的对象。
一、构造和初始化
__new__
是在实例创建之前被调用的,因为它的任务就是创建实例然后返回该实例,是个静态方法。
__init__
是当实例对象创建完成后被调用的,然后设置对象属性的一些初始值。
故而“ 本质上 ”来说,__new__()
方法负责创建实例,而__init__()
仅仅是负责实例属性相关的初始化而已,执行顺序是,先new后init。
二、属性访问控制
通常情况下,我们在访问类或者实例对象的时候,会牵扯到一些属性访问的魔法方法,主要包括:
① __getattr__ (self, name)
: 访问不存在的属性时调用
② __getattribute__ (self, name)
:访问存在的属性时调用(先调用该方法,查看是否存在该属性,若不存在,接着去调用①)
③ __setattr__ (self, name, value)
:设置实例对象的一个新的属性时调用
④ __delattr__ (self, name)
:删除一个实例对象的属性时调用 一个例子
class Foo:
x=1
def __init__(self,y):
self.y=y
def __getattr__(self, item):
print('----> from getattr:你找的属性不存在')
def __setattr__(self, key, value):
print('----> from setattr')
# self.key=value #这就无限递归了,你好好想想
self.__dict__[key]=value #应该使用它
def __delattr__(self, item):
print('----> from delattr')
# del self.item #无限递归了
self.__dict__.pop(item)
def __getattribute__(self, item):
print('----> __getattribute__')
return super().__getattribute__(item)
#__setattr__添加/修改属性会触发它的执行
f1=Foo(10)
print(f1.__dict__) # 因为你重写了__setattr__,凡是赋值操作都会触发它的运行,你啥都没写,就是根本没赋值,除非你直接操作属性字典,否则永远无法赋值
f1.z=3
print(f1.__dict__)
#__delattr__删除属性的时候会触发
f1.__dict__['a']=3#我们可以直接修改属性字典,来完成添加/修改属性的操作
del f1.a
print(f1.__dict__)
#__getattr__只有在使用点调用属性且属性不存在的时候才会触发
f1.xxxxxx
输出:
----> from setattr
----> __getattribute__
----> __getattribute__
{'y': 10}
----> from setattr
----> __getattribute__
----> __getattribute__
{'y': 10, 'z': 3}
----> __getattribute__
----> from delattr
----> __getattribute__
----> __getattribute__
{'y': 10, 'z': 3}
----> __getattribute__
----> from getattr:你找的属性不存在
注意,调用
self.__dict__[key]=value
会触发
def __getattribute__(self, item):
print('----> __getattribute__')
return super().__getattribute__(item)
因为 虽然是要给字典self.__dict__
添加键值对,其中隐含着首先获得self.__dict__
。 另外__getattribute__
需要返回super().__getattribute__(item)
,否则函数默认返回None,报错。
三、描述符
一个实现了 描述符协议
的类就是一个描述符
。
什么是描述符协议:实现了 __get__()
、__set__()
、__delete__()
其中至少一个方法的类,就是一个描述符。
-
__get__
: 用于访问属性。它返回属性的值,若属性不存在、不合法等都可以抛出对应的异常。 -
__set__
:将在属性分配操作中调用。不会返回任何内容。 -
__delete__
:控制删除操作。不会返回内容。
描述器应用---验证参数类型
class Typed:
def __init__(self, key, expected_type): # 构造函数接收所传入的参数和参数类型
self.key = key
self.expected_type = expected_type
def __get__(self, instance, owner):
print('get方法')
return instance.__dict__[self.key] # 从底层字典获取值
def __set__(self, instance, value):
print('set方法')
if not isinstance(value, self.expected_type): # 类型判断
raise TypeError('%s 传入的类型不是%s' % (self.key, self.expected_type)) # 格式化抛出异常
instance.__dict__[self.key] = value # 修改底层字典
def __delete__(self, instance):
print('delete方法')
instance.__dict__.pop(self.key)
class People:
name = Typed('name', str) # p1.__set__() self.__set__(),触发描述符__set__方法,设置参数类型传给构造函数
age = Typed('age', int) # p1.__set__() self.__set__()
salary = Typed('salary', float) # p1.__set__() self.__set__()
def __init__(self, name, age, salary):
self.name = name
self.age = age
self.salary = salary
# p1=People('alex','13',13.3)#类型有误,报错
p1 = People('alex', 13, 13.3)
print(p1.__dict__)
print(p1.name)
p1.name = 'egon'
print(p1.__dict__)
del p1.name
print(p1.__dict__)
# print(p1.name) # 相应的键值对已在底层字典中删除了,报错
四、构造自定义容器(Container)
在Python中,如果我们想实现创建类似于序列和映射的类(可以迭代以及通过[下标]返回元素),可以通过重写魔法方法__getitem__
、__setitem__
、__delitem__
、__len__
方法去模拟。
魔术方法的作用:
__getitem__
(self,key):返回键对应的值。
__setitem__
(self,key,value):设置给定键的值
__delitem__
(self,key):删除给定键对应的元素。
__len__()
:返回元素的数量
'''
desc:尝试定义一种新的数据类型
等差数列
'''
class ArithemeticSequence:
def __init__(self, start=0, step=1):
print('Call function __init__')
self.start = start
self.step = step
self.myData = {}
# 定义获取值的方法
def __getitem__(self, key):
print('Call function __getitem__')
try:
return self.myData[key]
except KeyError:
return self.start + key * self.step
# 定义赋值方法
def __setitem__(self, key, value):
print('Call function __setitem__')
self.myData[key] = value
# 定义获取长度的方法
def __len__(self):
print('Call function __len__')
return len(self.myData)
# 定义删除元素的方法
def __delitem__(self, key):
print('Call function __delitem__')
del self.myData[key]
s = ArithemeticSequence(1, 2)
print(s[0])
print(s[1])
print(s[2])
print(s[3])# 这里应该执行self.start+key*self.step,因为没有3这个key
s[3] = 100 # 进行赋值
print(s[3]) # 前面进行了赋值,那么直接输出赋的值100
print(len(s))
del s[3] # 删除3这个key
这些魔术方法的原理就是:当我们对类的属性item进行下标的操作时,首先会被__getitem__()
、__setitem__()
、__delitem__()
拦截,从而执行我们在方法中设定的操作,如赋值,修改内容,删除内容等等。
五、上下文管理
使用上下文管理器有三个好处:
- 提高代码的复用率;
- 提高代码的优雅度;
- 提高代码的可读性;
参考 https://juejin.im/post/5c87b165f265da2dac4589cc
六、比较运算符
你想让某个类的实例支持标准的比较运算(比如>=,!=,<=,<等),但是又不想去实现那一大丢的特殊方法。
Python类对每个比较操作都需要实现一个特殊方法来支持。 例如为了支持>=操作符,你需要定义一个 __ge__()
方法。 尽管定义一个方法没什么问题,但如果要你实现所有可能的比较方法那就有点烦人了。
装饰器 functools.total_ordering
就是用来简化这个处理的。 使用它来装饰一个来,你只需定义一个 __eq__()
方法, 外加其他方法(__lt__, __le__, __gt__, or __ge__)
中的一个即可。 然后装饰器会自动为你填充其它比较方法。
from functools import total_ordering
class Room:
def __init__(self, name, length, width):
self.name = name
self.length = length
self.width = width
self.square_feet = self.length * self.width
@total_ordering
class House:
def __init__(self, name, style):
self.name = name
self.style = style
self.rooms = list()
@property
def living_space_footage(self):
return sum(r.square_feet for r in self.rooms)
def add_room(self, room):
self.rooms.append(room)
def __str__(self):
return '{}: {} square foot {}'.format(self.name,
self.living_space_footage,
self.style)
def __eq__(self, other):
return self.living_space_footage == other.living_space_footage
def __lt__(self, other):
return self.living_space_footage < other.living_space_footage
装饰器functools.total_ordering原理:
class House:
def __eq__(self, other):
pass
def __lt__(self, other):
pass
# Methods created by @total_ordering
__le__ = lambda self, other: self < other or self == other
__gt__ = lambda self, other: not (self < other or self == other)
__ge__ = lambda self, other: not (self < other)
__ne__ = lambda self, other: not self == other
七、__str__
和__repr__
方法
可以实现类到字符串的转化
__str__
和__repr__
的差别:
__str__
的返回结果可读性强。__str__
的意义是得到便于人们阅读的信息
__repr__
的返回结果应更准确。__repr__
存在的目的在于调试,便于开发者使用。若将__repr__
返回的方式直接复制到命令行上,是可以直接执行的。
注:每个类都最好有一个 repr 方法
小结:
- 我们可以使用
__str__
和__repr__
方法定义类到字符串的转化方式,而不需要手动打印某些属性或是添加额外的方法。 - 一般来说,
__str__
的返回结果在于强可读性,而__repr__
的返回结果在于准确性。 - 我们至少需要添加一个
__repr__
方法来保证类到字符串的自定义转化的有效性,__str__
是可选的。因为默认情况下,在需要却找不到__str__
方法的时候,会自动调用__repr__
方法。
八、魔法方法之__call__
在Python中,函数其实是一个对象:
>>> f = abs
>>> f.__name__
'abs'
>>> f(-123)
123
由于 f 可以被调用,所以,f 被称为可调用对象。
所有的函数都是可调用对象。
一个类实例也可以变成一个可调用对象,只需要实现一个特殊方法_call_()
。
我们把 Person 类变成一个可调用对象:
class Person(object):
def __init__(self, name, gender):
self.name = name
self.gender = gender
def __call__(self, friend):
print 'My name is %s...' % self.name
print 'My friend is %s...' % friend
现在可以对 Person 实例直接调用:
>>> p = Person('Bob', 'male')
>>> p('Tim')
My name is Bob...
My friend is Tim...