定制类
反射
反射又称为自省,指的是程序可以访问、检测和修改它本身状态和行为的一种能力。python中提供了以下四个自检功能的函数。
hasattr(object, name):用来检测object(适用于类、文件、模块或对象,一切皆对象)中有没有一个name字符串对应的方法或属性。
>>> class Person:
... def __init__(self, name, age):
... self.name = name
... self.age = age
... def get_info(self):
... return '%s的年龄是%s岁!'%(self.name, self.age)
...
>>> p1 = Person('Jack', 22)
>>> p1.name # 直接调用name属性
'Jack'
>>> p1.__dict__['name'] # name属性存放于__dict__中,name是字符串
'Jack'
>>> hasattr(p1, 'name') # 判断p1中是否有name属性,name是字符串
True
>>> hasattr(p1, 'get_info') # 方法属性也可判断,传入方法名的字符串
True
getattr(object, name, default=None):返回对象的name字符串对应的属性值或方法地址。
>>> getattr(p1, 'name') # 返回name属性的值,等同于p1.name
'Jack'
>>> getattr(p1, 'age')
22
>>> getattr(p1, 'get_info') # 返回方法的地址等同于p1.get_info
<bound method Person.get_info of <__main__.Person object at 0x10e51b860>>
>>> getattr(p1, 'sex') # 若属性或方法不存在,而getattr又没有提供默认值,则报错
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Person' object has no attribute 'sex'
>>> getattr(p1, 'sex', 'male')# 若属性或方法不存在,而getattr提供了默认值,则返回默认值
'male'
setattr(object, key, value):给对象object的属性key赋值value,既可以新增属性也可以修改已有属性的值。
>>> setattr(p1, 'sex', 'male') # 为对象p1新增sex属性,等同于p1.sex = ‘male’
>>> p1.__dict__
{'name': 'Jack', 'age': 22, 'sex': 'male'}
>>> setattr(p1, 'name', 'zhangsan') # 修改p1对象已有属性name的值
>>> p1.__dict__
{'name': 'zhangsan', 'age': 22, 'sex': 'male'}
>>> setattr(p1, 'func', lambda x:x+1) # 还可以增加对象的方法属性,为对象添加方法
>>> p1.__dict__
{'name': 'zhangsan', 'age': 22, 'func': <function <lambda> at 0x10e3f61e0>}
delattr(object, name):删除对象object的name属性值。
>>> delattr(p1, 'sex') # 删除对象p1的sex属性,等同于del p1.sex
>>> p1.__dict__
{'name': 'zhangsan', 'age': 22}
注意:以上四个自省的函数,传入的对象的属性名都要以字符串的形式。
类是对象,类也是对象类型。
判断一个对象是否属于一个类,使用内建函数isinstance(),用它可以判断一个对象是否是一个类或者子类的实例;也可以判断一个对象是否是某个类型。
>>> isinstance(3, int)
True
>>> isinstance([2, 3], list)
True
>>> class A:
... pass
...
>>> a = A()
>>> isinstance(a, A)
True
因此,创建了一个类就是创建了一种对象类型。
自定义对象类型:
自定义类,就要用到类的特殊方法,比如__init__()初始化方法;__str__()输出字符串方法;__add__()定义加法运算符等。
>>> class Fraction:
... def __init__(self, num1, num2):
... self.num1 = num1
... self.num2 = num2
... def __str__(self):
... return str(self.num1) + '/' + str(self.num2)
... __repr__ = __str__
... def __add__(self, other): # 魔术方法,实现加法运算符
... return str(self.num1+other.num1) + '/' + str(self.num2+other.num2)
...
>>> m = Fraction(2, 3)
>>> n = Fraction(5, 6)
>>> s = m + n # 执行+号时自动调用__add__方法,相当于执行m.__add__(n)
>>> print(s)
7/9
代码中__repr__ = __str__的含义是在类被调用(实例化对象)时,向变量(即实例化的对象)提供__str__()里的内容。
我们在代码中增加了特殊方法__add__(),它就是实现加法运算符的魔术方法。在python中,运算符的作用是简化书写,实现运算的运算符都有其对应的特殊方法支撑才得以实现的。
常见运算符及其对应的特殊方法
二元运算符 |
特殊方法 |
+ |
__add__ , __radd__ |
- |
__sub__ , __rsub__ |
* |
__mul__ , __rmul__ |
/ |
__div__ , __rdiv__ , __truediv__ , __rtruediv__ |
// |
__floordiv__ , __rfloordiv__ |
% |
__mod__ , __rmod__ |
** |
__pow__ , __rpow__ |
<< |
__lshift__ , __rlshift__ |
>> |
__rshift__ , __rrshift__ |
and |
__and__ , __rand__ |
== |
__eq__ |
!= |
__ne__ |
> |
__ge__ |
< |
__lt__ |
>= |
__ge__ |
<= |
__le__ |
以“+”为例,不论是实现“1 + 2”, 还是实现“[1, 3, ‘a’] + [4, ‘dd’]”,都会自动执行1.__add__(2)或者[1, 3, ‘a’].__add__([4, ‘dd’])操作。即两个对象是否能进行加法运算,首先要看对象是否有__add__()方法,一旦对象有__add__()方法,就可以定义对象的相加操作。
黑魔法
优化内存
我们知道类中有__dict__属性,它包含了当前类中的所有属性和方法;类的每个对象也有各自的__dict__属性,它包含了对应的对象中的属性。
>>> class Foo:
... name = 'zhangsan'
... age = 'male'
...
>>> Foo.__dict__ # 类的__dict__属性
mappingproxy({'__module__': '__main__', 'name': 'zhangsan', 'age': 'male', '__dict__': <attribute '__dict__' of 'Foo' objects>, '__weakref__': <attribute '__weakref__' of 'Foo' objects>, '__doc__': None})
>>> f = Foo()
>>> f.__dict__ # 实例的__dict__属性
{}
>>> f.name = 'lisi'
>>> f.__dict__
{'name': 'lisi'}
假设有无数个对象,则就会有无数个__dict__属性,这将占用很大的内存资源。需要一种能够控制__dict__的方法,这就是特殊属性__slots__。
>>> class Spring:
... __slots__ = ('tree', 'flower') # 类中定义__slots__属性
...
>>> dir(Spring) # 类中的__dict__属性被__slots__属性替代
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__slots__', '__str__', '__subclasshook__', 'flower', 'tree']
>>> Spring.__slots__
('tree', 'flower')
>>> Spring.tree = 'baiyang'
>>> Spring.tree
'baiyang'
>>> t = Spring()
>>> t.__slots__ # 实例中也是__slots__属性
('tree', 'flower')
>>> f = Spring()
>>> f.__slots__
('tree', 'flower')
>>> id(f) # 实例的__slots__属性和类的__slots__属性都为同一个
4418820360
>>> id(t)
4418820304
>>> id(f.__slots__)
4418824584
>>> id(t.__slots__)
4418824584
>>> id(Spring.__slots__)
4418824584
由以上代码可以看出,所有实例的__slots__属性与类的完全一样,这跟前面的__dict__属性大不一样了。内存中只有一个__slots__属性,再增加实例时__slots__属性不会增加。
类属性赋值后,可以通过任何一个实例来调用它,但不能通过任何一个实例修改类属性的值。
>>> Spring.tree # 赋值后的类属性可以通类名和对象调用
'baiyang'
>>> t.tree
'baiyang'
>>> f.tree
'baiyang'
>>> t.tree = 'xiangzhangshu' # 实例不能修改类属性
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Spring' object attribute 'tree' is read-only
>>> Spring.tree = 'huyang' # 可通过类名修改类属性
类属性是静态属性,对实例来说,不能修改,是只读的,只能通过类名修改。
对于尚未赋值的类属性名,实例可以定义与其同名的实例属性。
>>> t.flower = 'yuejihua'
>>> Spring.flower
<member 'flower' of 'Spring' objects>
>>> Spring.flower = 'guihua'
>>> Spring.flower
'guihua'
>>> t.flower
'guihua'
由以上代码可以看出,实例属性没有传回到类属性中,这里只是实例建立了一个临时的与类属性同名的实例属性。定义了__slots__属性后,由于只有这么一个属性空间用来存放属性和方法,所以类属性对实例属性具有决定作用。因为操作实例属性会自动调用__setattr__方法,底层操作实例的__dict__属性字典,而定义了__slots__后,__dict__不存在了。
__slots__属性是类和对象共用的,它把实例属性牢牢地管控起来,只能是定义类时指定的属性。如果要增加、修改属性,只能通过类来实现。它不像__dict__属性是类和每个对象都是自己独立的,可以存放各自的属性。
__slots__总结:
1.__slots__是什么:是一个类属性变量,变量值可以是列表、元组或者可迭代对象,也可以是一个字符串(意味着所有实例只有一个数据属性)。
2.引子:使用点来访问属性本质上就是在访问类或者对象的__dict__属性字典(类的字典是共享的,而每个实例的是独立的)中的内容。
3.为何使用__slots__:__dict__属性字典会占用大量内存。如果有一个属性很少的类,但是有很多实例,每个实例又不需要定义各自的实例属性,此时为了节省内存,可以使用__slots__取代__dict__。
当定义__slots__后,__slots__就会为实例使用一种更加紧凑的内部表示。实例通过一个很小的固定大小的数组来构建,而不是为每个实例定义一个__dict__属性字典。
使用__slots__一个不好的地方就是我们不能再给实例添加新的属性了,因为实例中已经没有了用来保存属性的__dict__字典,只能使用在__slots__中定义的那些属性,即类中的__slots__中定义了哪些属性,对象也只能使用那些属性,对象不能自己去创建新属性(因为没有了__dict__),也不能修改类的属性,因为受类控制。
4.注意事项:__slots__的很多特性都依赖于普通的基于字典的实现。另外,定义了__slots__后的类不再支持一些普通类特性了,比如多继承。
大多数情况下,应该只在那些经常被使用到的用作数据结构的类上定义__slots__,
比如在程序中需要创建某个类的几百万个实例对象 。
关于__slots__的一个常见误区是它可以作为一个封装工具来防止用户给实例增加新的属性。尽管使用__slots__可以达到这样的目的,但是这个并不是它的初衷。更多的是用来作为一个内存优化工具。
属性拦截
当调用未定义的属性时,会直接报错,属性不存在。
>>> class A:
... pass
...
>>> a = A()
>>> a.x # 变量x在类中没有定义,调用会报错
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'A' object has no attribute 'x'
如果在调用类中未定义的属性时,我们希望有别的提示、操作等,而不希望等着报错,这时就用到了属性拦截。
python中的具有属性拦截功能的方法:
__setattr__(self, name, value):如果要给name赋值,就调用这个方法。
__getattr__(self, name):如果name被访问,但它又不存在,则此方法被调用。
__getattribute__(self, name):当name被访问时自动被调用,无论name是否存在,都会被调用。
__delattr__(self, name):如果要删除name,则这个方法被调用。
>>> class A:
... def __getattr__(self, name): # 定义黑魔法__getattr__和__setattr__
... print('You use getattr')
... def __setattr__(self, name, value):
... print('You use setattr')
... self.__dict__[name] = value
...
>>> a = A()
>>> a.x # 属性x不存在,自动调用__getattr__,在__getattr__中做处理
You use getattr
>>> a.y = 9 # 增加属性并赋值,自动调用__setattr__,设置a.__dict__属性内容
You use setattr
>>> a.y # 属性存在,不会调用__getattr__
9
a.x的属性本来是不存在的,但由于类中有了__getattr__(self, name)方法,当发现属性x不存在于对象的__dict__中时,就调用了__getattr__,即属性拦截。
给对象的属性赋值时,调用了__setattr__(self, name, value)方法,这个方法中有一句self.__dict__[name] = value,通过这个语句,就将属性和数据保存到了对象的__dict__中,而不用self.name = value,因为如果用self.name = value,只要一赋值就会自动触发__setattr__,导致无限递归。
也可以在类中定义__getattribute__(self, name),并且只要访问属性,不论该属性是否存在,都会自动调用它。当类中同时定义了__getattribute__(self, name)和__getattr__(self, name),而__getattribute__(self, name)中又没有抛出AttributeError异常时,__getattr__(self, name)将不起作用;当__getattribute__(self, name)中抛出了AttributeError异常,__getattribute__(self, name)执行到抛出异常语句时,会调用__getattr__(self, name)。当自定义了__getattribute__(self, name),且__getattribute__(self, name)中又抛出AttributeError异常时,可以定义__getattr__(self, name)配合__getattribute__(self, name)使用。还可以定义__delattr__(self, name),当删除属性时,不论要删除的属性是否存在,都自动调用该方法。
>>> class B:
... def __getattribute__(self, name):
... print('You are using getattribute')
... return object.__getattribute__(self, name) # 通过这种方法返回属性值
... def __delattr__(self, name):
... print('You are using delattr')
...
>>> b = B()
>>> b.x # 属性不存在,也会调用__getattribute__,并报错
You are using getattribute
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in __getattribute__
AttributeError: 'B' object has no attribute 'x'
>>> b.y = 3
>>> b.y # 属性存在,也会调用__getattribute__
You are using getattribute
3
>>> del b.y # 删除已存在的属性,调用__delattr__
You are using delattr
>>> del b.z # 删除不存在的属性,也调用__delattr__
You are using delattr
我们注意到上面的代码中,返回属性内容用的是return object.__getattribute__(self, name),而没有用return self.__dict__[name]的方式,是因为如果用self.__dict__[name]的方式,就是访问self.__dict__,只要访问这个属性,就会调用__getattribute__(),会导致无限递归。当新增属性并赋值后,属性就存在于__dict__中了,再调用时,依然会被拦截,但由于存在于__dict__中了,会将结果返回。以上黑魔法中的参数name就对应于对象或类中的属性。
以上的黑魔法在建立类的时候,就会存在于类中,重新定义则会覆写类中原有的。需要注意的是,__setattr__会在给属性赋值时自动触发,所以在自定义的__setattr__方法中不能出现类似于self.key = value的形式的直接赋值操作,这样会陷入无限递归,应使用self.__dict__[key] = value的形式操作底层字典。同样的还有__delattr__,删除属性时会自动触发,因此在自定义的__delattr__方法中,不能出现类似于del self.key形式的直接删除,这样也会陷入无限递归,要用self.__dict__.pop(key)的操作底层字典的形式。__setattr__和__delattr__中的定义属于多此一举,不定义这两种方法也是在默认执行赋值或删除操作,因此很少使用。__getattr__用的较多。
结合属性拦截对字符串、列表、元组、字典的补充:
由上面介绍的属性拦截可知在对象通过点(.)的方式操作属性会触发上面四种具有属性拦截功能的方法。而在序列(字符串、列表、元组)通过索引或字典通过键操作其元素时也会自动触发一些内置方法:__getitem__、__setitem__、__delitem__。同样的,对象通过键的方式操作属性时,也会触发上面的三种方法。需要注意的是,字符串和元组只能取出其元素,不等删除和修改元素,所以字符串和元组中只有__getitem__方法。
__getitem__:在通过索引或键取出元素或属性时触发。
__setitem__:在通过索引或键设置元素或属性时触发。
__delitem__:在通过索引或键删除元素或属性时触发。
>>> class Foo:
... def __getitem__(self, item):
... print('通过键的方式取得对象属性')
... return self.__dict__[item]
... def __setitem__(self, key, value):
... print('通过键的方式为对象属性赋值')
... self.__dict__[key] = value
... def __delitem__(self, key):
... print('通过键的方式删除对象属性')
... self.__dict__.pop(key)
...
>>> foo = Foo()
>>> foo['x'] = 9 # 自动触发__setitem__方法
通过键的方式为对象属性赋值
>>> foo['name'] = 'zhangsan'
通过键的方式为对象属性赋值
>>> foo['age'] = 'male'
通过键的方式为对象属性赋值
>>> print(foo.__dict__)
{'x': 9, 'name': 'zhangsan', 'age': 'male'}
>>> print(foo['name']) # 自动触发__getitem__方法
通过键的方式取得对象属性
zhangsan
>>> del foo['age'] # 自动触发__delitem__方法
通过键的方式删除对象属性
>>> print(foo.__dict__)
{'x': 9, 'name': 'zhangsan'}
注意:自定义的类,即使继承了python内置的类型(list、str、tuple、dict)等,也不会受其父类限制的影响,因为我们可以完全的去自定义子类,子类中可以增加父类没有的功能,对于继承了已有对象类型的子类,我们在操作__getitem__、__setitem__、delitem__属性时,必然会跟__dict__属性联系在一起,此时基本上已经脱离了list、str、tuple、dict父类的内容,大部分都是自身定义的内容。当然子类依然可以使用父类中的内容。
反射和黑魔法应用
要在一个类中使用另一个类中的方法,可以利用继承的方式去实现,继承也可以实现覆写,即定义自己的同名方法。这里介绍利用组合的方式完成授权,同样实现在一个类中使用另一个类中的方法,并且可以重新定义同名方法。
import time
class HandleFile:
def __init__(self, filename, mode='r', encoding='utf-8'):
self.filename = filename
self.mode = mode
self.encoding = encoding
self.file = open(filename, mode, encoding=encoding) # 组合方式,将open类关联到自定义类HandleFile组合
def write(self, item): # 自定义write方法
if not item.isdigit(): # 写入的内容不能全为数字
t = time.localtime() # 在写入的内容前加上写入的时间
self.file.write(time.strftime('%Y-%m-%d %X', t)+': '+item)
else:
print('写入的内容不能全为数字!')
def __getattr__(self, item): # 定义__getattr__方法,当类中属性不存在时,自动调用
print(item, type(item)) # 打印对象和对象的属性的类型
return getattr(self.file, item) # 调用对象关联的类中的方法
f = HandleFile('a.txt', 'w+')
f.write('11111111111') # 调用自定义的write方法
f.write('abcde33fgh\n')
time.sleep(1)
f.write('learn python\n')
time.sleep(1)
f.write('Simple is better than complex\n')
f.seek(0) # 通过__getattr__调用和对象关联的类中的方法
print(f.read())
写入的内容不能全为数字!
seek <class 'str'> # 对象属性的类型是字符串
read <class 'str'>
2018-10-18 17:35:01: abcde33fgh
2018-10-18 17:35:02: learn python
2018-10-18 17:35:03: Simple is better than complex