面向对象进阶
Posted midworld
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面向对象进阶相关的知识,希望对你有一定的参考价值。
1. 类的其他内置函数
1.1 isinstance 和 issubclass
1. isinstance(obj, cls)
判断第一个参数是否是第二个参数的实例对象,返回布尔值。第一个参数为对象,第二个参数为类。
class Animal:
pass
class Dog(Animal):
pass
d1 = Dog()
print(isinstance(d1, Dog)) # True
print(isinstance(d1, Animal)) # True
# 也可以和 type 一样用来判断类型
print(isinstance(‘python‘, str)) # True
在继承关系中,一个对象的数据类型是某个子类,那么它也可以被看作是父类,反之则不行。
2. issubclass(cls, class_or_tuple))
判断第一个参数是否是第二个参数的子类,返回布尔值。
class Animal:
pass
class Dog(Animal):
pass
print(issubclass(Dog, Animal)) # True
1.2 __getattribute__
__getattribute__(object, name)
在获取对象属性时,不管是否存在都会触发。
class Animal:
def __init__(self, name):
self.name = name
def __getattribute__(self, item):
print(‘触发 getattribute‘)
a1 = Animal(‘小黑‘)
a1.color # 触发 getattribute
a1.name # 触发 getattribute
与 __getattr__
同时存在时,只会触发 __getattribute__
,除非抛出 AttributeError
异常(其他异常不行):
class Animal:
def __init__(self, name):
self.name = name
def __getattr__(self, item):
print(‘触发 getattr‘)
def __getattribute__(self, item):
print(‘触发 getattribute‘)
raise AttributeError(‘触发‘) # 使用异常处理
a1 = Animal(‘小黑‘)
a1.color
------------
触发 getattribute
触发 getattr
这里我们使用异常处理 raise 模拟 Python内部机制,抛出 AttributeError,使得 __getattr__
也会被触发。
1.3 __getitem__、__setitem__、__delitem__
与 __getattr__、__setattr__、__delattr__
类似,不同之处在于它是以字典形式访问、修改或删除,并且只要访问、修改或删除就会触发。
class Animal:
def __getitem__(self, item):
print(‘__getitem__‘)
return self.__dict__[item]
def __setitem__(self, key, value):
print(‘__setitem__‘)
self.__dict__[key] = value
def __delitem__(self, item):
print(‘__delitem__‘)
self.__dict__.pop(item)
a1 = Animal()
print(a1.__dict__) # {}
# set
a1[‘name‘] = ‘rose‘ # __setitem__
print(a1.__dict__) # {‘name‘: ‘rose‘}
# get
print(a1[‘name‘]) # __getitem__ rose
# del
del a1[‘name‘] # __delitem__
print(a1.__dict__) # {}
1.4 __str__、__repr__
__str__ 和 __repr__
都是用来定制对象字符串显示形式
# L = list(‘abcd‘)
# print(L) # [‘a‘, ‘b‘, ‘c‘, ‘d‘]
# f = open(‘a.txt‘, ‘w‘)
# print(f) # <_io.TextIOWrapper name=‘a.txt‘ mode=‘w‘ encoding=‘cp936‘>
class Animal:
pass
a1 = Animal()
print(a1) # <__main__.Animal object at 0x00000000053E5358> 标准的对象字符串格式
Python 中一切皆对象,L、f 和 a1 都是对象,但是它们的显示形式却是不一样。这是因为 Python 内部对不同的对象定制后的结果。我们可以重构 __str__ 或 __repr__
方法,能够更友好地显示:
重构 __str__
class Animal:
pass
def __str__(self):
return ‘自定制对象的字符串显示形式‘
a1 = Animal()
print(a1)
m = str(a1) # print() 的本质是在调用 str(),而 str() 的本质是在调用 __str__()
print(m)
print(a1.__str__()) # obj.__str__()
---------
# 自定制对象的字符串显示形式
# 自定制对象的字符串显示形式
# 自定制对象的字符串显示形式
可以看出,我们重构 __str__
方法后,返回的是我们定制的字符串。而 print() 和 str() 的本质都是在调用 object.__str__()
。
重构 __repr__
class Animal:
pass
def __repr__(self):
return ‘repr‘
a1 = Animal()
print(a1) # repr 返回 repr
print(a1.__repr__()) # repr
没有重构 __repr__
方法时,输出 a1,结果是对象内存地址。当构建了后,输出 repr。
当 __str__
没定义时,用 __repr__
代替输出。当两者都存在时,__str__
覆盖 __repr__
。
两者区别:
- 打印操作,首先尝试
__str__
,若类本身没有定义则使用__repr__
代替输出,通常是一个友好的显示。 __repr__
适用于所有其他环境(包括解释器),程序员在开发期间通常使用它。
1.5 __format__
用于类中自定制 format。
s = ‘{0}{0}{0}‘.format(‘rose‘)
print(s) # ‘roseroserose‘
重构 __format__
# 定义格式化字符串格式字典
date_dic = {
‘ymd‘: ‘{0.year}-{0.month}-{0.day}‘,
‘mdy‘: ‘{0.month}-{0.day}-{0.year}‘
}
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
def __format__(self, format_spec):
# 当用户输入的格式为空或其他时,设定一个默认值
if not format_spec or format_spec not in date_dic:
format_spec = ‘ymd‘
fm = date_dic[format_spec]
return fm.format(self)
d1 = Date(2018, 11, 28)
# x = ‘{0.year}-{0.month}-{0.day}‘.format(d1)
# print(x) # 打印 2018-11-28
print(format(d1, ‘ymd‘)) # 2018-11-28
print(format(d1, ‘mdy‘)) # 11-28-2018
print(format(d1)) # 当为空时,输出默认格式 ymd
format 实际调用的是 __format__()
1.6 __slots__
__slots__
实质是一个类变量,它可以是字符串、列表、元组也可以是个可迭代对象。
它的作用是用来覆盖类的属性字典 __dict__
。我们都知道类和对象都有自己独立的内存空间,每产生一个实例对象就会创建一个属性字典。当产生成千上万个实例对象时,占用的内存空间是非常可怕的。
当我们用定义 __slots__
后,实例便只能通过一个很小的固定大小的数组来构建,而不是每个实例都创建一个属性字典,从而到达节省内存的目的。
class Animal:
# __slots__ = ‘name‘
__slots__ = [‘name‘, ‘age‘]
pass
a1 = Animal()
# # print(a1.__dict__) # AttributeError: ‘Animal‘ object has no attribute ‘__dict__‘
# a1.name = ‘rose‘
# print(a1.__slots__) # name
# print(a1.name) # rose
a1.name = ‘lila‘ # 设置类变量的值
a1.age = 1
print(a1.__slots__) # [‘name‘, ‘age‘]
print(a1.name, a1.age) # lila 1
Tips:
- 它会覆盖
__dict__
方法,因此依赖__dict__
的操作,如新增属性会报没有这个属性,因此要慎用。
- 定义为字符串形式时,表示只有一个类变量。列表或元组,其中元素有多少个就表示有多少个类变量。
1.7 __doc__
用于查看类的文档字符串,不能继承。若类没有定义文档字符串,则返回 None(Python 内部自定义的)。
class Animal:
"""动物类"""
pass
class Dog(Animal):
pass
print(Animal.__doc__)
print(Dog.__doc__)
print(Dog.__dict__)
----------
# 动物类
# None
# {‘__module__‘: ‘__main__‘, ‘__doc__‘: None}
1.8 __module__ 和 __class__
用于查看当前操作对象是在哪个模块里和哪个类里。
# a.py
class Cat:
name = ‘tom‘
pass
# b.py
from a import Cat
c1 = Cat()
print(c1.__module__) # a
print(c1.__class__) # Cat
1.9 __del__
析构方法,当对象再内存中被释放时(如对象被删除),自动触发。
Python 有自动回收机制,无需定义 __del__
。但是若产生的对象还调用了系统资源,即有用户级和内核级两种资源,那么在使用完毕后需要用到 __del__
来回收系统资源。
class Foo:
def __init__(self, name):
self.name = name
def __del__(self):
print(‘触发 del‘)
f1 = Foo(‘rose‘)
# del f1.name # 不会触发
del f1 # 会触发
print(‘-------->‘)
# -------->
# 触发 del
# 触发 del
# -------->
- 第一种情况时删除数据属性,不会触发,先打印 ---->,再触发 (这是由 Python 内部自动回收机制导致的,当程序执行完毕后内存自动被回收)
- 第二种情况删除对象,触发。所有先触发后打印。
1.10 __call__
当对象加上括号后,触发执行它。若没有类中没有重构 __call__
,则会报 not callable
。
class Animal:
def __call__(self):
print(‘对象%s被执行‘ % self)
a1 = Animal()
a1()
-------------------
# 对象<__main__.Animal object at 0x00000000053EC278>被执行
Python 一切皆对象,类也是个对象。而类加括号能调用执行,那么它的父类(object)中必然也会有个 __call__
方法。
1.11 __iter__ 和 __next__ 实现迭代器协议
- 迭代器协议:对象必须有一个
__next__或next()
方法,执行该方法可返回迭代项的下一项,直到超出边界,引发 StopIteration 终止迭代。
- 可迭代对象:指的是实现了迭代器协议的对象,其内部必须要有一个
__iter__
方法。
- for 循序其实质是调用对象内部的
__inter__
方法,它能够自动捕捉 StopIteration 异常,因此超出边界也不会报错。
因此可迭代对象内部必须有一个 __iter__ 和 __next__
方法。
class Dog:
def __init__(self, age):
self.age = age
d1 = Dog(1)
for i in d1:
print(i) # TypeError: ‘Dog‘ object is not iterable
对象 d1 中没有 __iter__()
所有它是不可迭代对象
class Dog:
def __init__(self, age):
self.age = age
def __iter__(self):
# return None # iter() returned non-iterator of type ‘NoneType‘
return self # 需要返回可迭代类型
def __next__(self):
if self.age >= 6:
raise StopIteration(‘终止迭代~‘)
self.age += 1
return self.age
d1 = Dog(1)
# print(d1.__next__()) # 2
# print(d1.__next__()) # 3
for i in d1: # obj = d1.__iter__()
print(i) # obj.__next__()
----------
# 2
# 3
# 4
# 5
# 6
对象 d1 实现 __iter__ 和 __next__
后变成可迭代对象。
1.12 迭代器协议实现斐波拉契数列
class Fib:
def __init__(self, n):
self._x = 0
self._y = 1
self.n = n
def __iter__(self):
return self
def __next__(self):
self._x, self._y = self._y, self._x + self._y
if self._x > self.n:
raise StopIteration(‘终止‘)
return self._x
f1 = Fib(13)
for i in f1:
print(i)
------------------
# 1
# 1
# 2
# 3
# 5
# 8
# 13
1.13 描述符(descriptor)
描述符就是将某种特殊类型的类的实例指派给另一个类的属性,这两个类必须都是新式类,而且这个特殊类至少实现了 __get__()、__set__()、__delete__()
中的一种。
魔法方法 | 含义 |
---|---|
__get__(self, instance, owner) |
调用属性时触发,返回属性值 |
__set__(self, instance, value) |
为一个属性赋值时触发,不返回任何内容 |
__delete__(self, instance) |
采用 del 删除属性时触发,不返回任何内容 |
- self :描述符类自身的实例 (Descriptor 的实例)
- instance:描述符的拥有者所在类的实例 (Test 的实例)
- owner:描述符的拥有者所在的类本身 (Test 本身)
- value:设置的属性值
class Descriptor:
def __get__(self, instance, owner):
print(self, instance, owner)
def __set__(self, instance, value):
print(self, instance, value)
def __delete__(self, instance):
print(self, instance)
class Test:
x = Descriptor() # 类 Descriptor 是类 Test 的类属性 x
描述符是用来代理另一个类的类属性,而不能定义到构造函数中去。那么它是在哪里以什么样的条件才能触发呢?
事实上它只能在它所描述的类中,当其实例访问、修改或删除类属性时才会触发:
t1 = Test()
print(t1.x) # 访问
print(t1.__dict__)
t1.x = 10 # 修改
print(t1.__dict__) # 凡是与被代理的类属性相关的操作,都是找描述符,因此代理类的实例属性字典为空。
print(Test.__dict__) # 查看 Test 的属性字典
<__main__.Descriptor object at 0x0000000004F217B8> <__main__.Test object at 0x0000000004F217F0> <class ‘__main__.Test‘>
None
{}
<__main__.Descriptor object at 0x0000000004F217B8> <__main__.Test object at 0x0000000004F217F0> 10
{}
{‘__module__‘: ‘__main__‘, ‘x‘: <__main__.Descriptor object at 0x0000000004F217B8>, ‘__dict__‘: <attribute ‘__dict__‘ of ‘Test‘ objects>, ‘__weakref__‘: <attribute ‘__weakref__‘ of ‘Test‘ objects>, ‘__doc__‘: None}
可以看出实例 t1 在调用、修改属性 x 时,都会自动触发 __get__ 和 __set__
方法 ,删除也是一样。查看 Test 的属性字典,发现其属性 x 的值是 Descriptor 的实例。
描述符分两种:
- 数据描述符: 至少实现了
__get__() 和 __set__()
- 非数据描述符: 没有实现
__set__()
。
Tips:
- 描述符本身应该是新式类,其代理的类也应是新式类
- 只能定义在类属性中,不能定义到构造函数中
- 遵循优先级(由高到低):类属性 —— 数据描述符 —— 实例属性 —— 非数据描述符 —— 找不到的属性触发
__getattr__()
。 - 凡是与被代理的类属性相关的操作,都是找描述符,因此代理类的实例属性字典为空。
1.14 描述符优先级
类属性 > 数据描述符
class Descriptor:
def __get__(self, instance, owner):
print(‘get‘)
def __set__(self, instance, value):
print(‘set‘)
def __delete__(self, instance):
print(‘delete‘)
class Test:
x = Descriptor()
t1 = Test()
Test.x # 调用数据属性
print(Test.__dict__) # ‘x‘: <__main__.Descriptor object at 0x0000000004F21EF0>
Test.x = 1 # 设置类属性
print(Test.__dict__) # ‘x‘: 1
get
{‘__module__‘: ‘__main__‘, ‘x‘: <__main__.Descriptor object at 0x0000000004F21EF0>, ‘__dict__‘: <attribute ‘__dict__‘ of ‘Test‘ objects>, ‘__weakref__‘: <attribute ‘__weakref__‘ of ‘Test‘ objects>, ‘__doc__‘: None}
{‘__module__‘: ‘__main__‘, ‘x‘: 1, ‘__dict__‘: <attribute ‘__dict__‘ of ‘Test‘ objects>, ‘__weakref__‘: <attribute ‘__weakref__‘ of ‘Test‘ objects>, ‘__doc__‘: None}
上面例子中,描述符被定义为类属性,因此在调用时,触发了描述符中的 __get__()
方法。而赋值操作,直接将类属性的 x 的值由原来而的描述符赋值为 1,因此没有被触发。
问题:为什么实例赋值操作会触发 __set__()
方法?
因为实例的属性字典中本身没有属性 x,它只能在类中查找。赋值操作,操作的是自己的属性字典,而不会改变类属性 x。
类属性 > 数据描述符 > 实例属性
t1 = Test()
Test.x = 10000
t1.x
结果为空,什么都没触发。这是因为类属性 x 被修改,覆盖了数据描述符,因此实例属性也不能触发。
实例属性 > 非数据描述符
class Descriptor:
def __get__(self, instance, owner):
print(‘get‘)
class Test:
x = Descriptor()
t1 = Test()
t1.x = 1
print(t1.__dict__) # {‘x‘: 1}
非数据描述符,没有 __set__()
方法。因此在修改实例属性时,不会触发 __set__()
方法,而是修改了自己的属性字典。
非数据描述符 > __getattr__
class Descriptor:
def __get__(self, instance, owner):
print(‘get‘)
class Test:
x = Descriptor()
def __getattr__(self, item):
print(‘触发 getattr‘)
t1 = Test()
t1.x = 1 # 调用实例属性
print(t1.__dict__)
t1.y # 调用不存在的实例属性
-------------------
# {‘x‘: 1}
# 触发 getattr
从上可以看出,先进行了调用了非数据描述符。再触发 __getattr__
。但是因为实例属性大于非数据描述符,因此先看到的是实例的属性字典。
1.15 描述符应用
Python 是弱语言类型,在变量定义时不需要指定其类型,类型之间也可以互相轻易转换,但这也会导致程序在运行时类型错误导致整个程序崩溃。Python 内部没有相应的类型判断机制,因此我们有如下需求:
自定制一个类型检查机制,在用户输入的类型与预期不符时,提示其类型错误。
现有一个 Dog 类,想要实现用户输入的名字只能是字符串,年龄只能是整型,在输入之前对其进行判断:
class Descriptor:
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‘)
print(value)
if isinstance(value, self.expected_type):
instance.__dict__[self.key] = value
else:
raise TypeError(‘你输入的%s不是%s‘ % (self.key, self.expected_type))
class Dog:
name = Descriptor(‘name‘, str)
# age = Descriptor(‘age‘, int)
def __init__(self, name, age, color):
self.name = name
self.age = age
self.color = color
d1 = Dog(‘rose‘, 2, ‘white‘) # 触发 d1.__set__()
print(d1.__dict__)
d1.name = ‘tom‘
print(d1.__dict__)
---------
# set
# rose
# {‘name‘: ‘rose‘, ‘age‘: 2, ‘color‘: ‘white‘}
# set
# tom
# {‘name‘: ‘tom‘, ‘age‘: 2, ‘color‘: ‘white‘}
类 Descriptor 是 Dog 的修饰符(修饰类变量 name、age),当d1 = Dog(‘rose‘, 2, ‘white‘)
,触发 d1.__set__()
方法,也就会调用对象 d1 的属性字典,进行赋值操作。
当用户输入的名字不是字符串时:
d2 = Dog(56, 2, ‘white‘)
------
# set
# 56
# ---------------------------------------------------------------------------
# TypeError Traceback (most recent call last)
# <ipython-input-36-a60d3743b7a0> in <module>()
# 33 # print(d1.__dict__)
# 34
# ---> 35 d2 = Dog(56, 2, ‘white‘)
# <ipython-input-36-a60d3743b7a0> in __init__(self, name, age, color)
# 22 # age = Descriptor(‘age‘, int)
# 23 def __init__(self, name, age, color):
# ---> 24 self.name = name
# 25 self.age = age
# 26 self.color = color
# <ipython-input-36-a60d3743b7a0> in __set__(self, instance, value)
# 15 instance.__dict__[self.key] = value
# 16 else:
# ---> 17 raise TypeError(‘你输入的%s不是%s‘ % (self.key, self.expected_type))
# 18
# 19
# TypeError: 你输入的name不是<class ‘str‘>
直接报错,提示用户输入的不是字符串,同理 age 也是一样。这样就简单地实现了定义变量之前的类型检查。
1.16 上下文管理协议
使用 with 语句操作文件对象,这就是上下文管理协议。要想一个对象兼容 with 语句,那么这个对象的类中必须实现 __enter__ 和 __exit__
。
class Foo:
def __init__(self, name):
self.name = name
def __enter__(self):
print(‘next‘)
return self
def __exit__(self,exc_type, exc_val, exc_tb):
print(‘触发exit‘)
with Foo(‘a.txt‘) as f: # 触发 __enter__()
print(f.name) # a.txt,打印完毕,执行 __exit__()
print(‘------>‘)
---------
# next
# a.txt
# 触发exit
# ------>
with Foo(‘a.txt‘) as f
触发 __enter__()
,f 为对象本身。执行完毕后触发 __exit__()
。
__exit__()
的三个参数
在没有异常的时候,这三个参数皆为 None:
class Foo:
...
def __exit__(self, exc_type, exc_val, exc_tb):
print(‘触发exit‘)
print(exc_type)
print(exc_val)
print(exc_tb)
with Foo(‘a.txt‘) as f:
print(f.name)
------
# a.txt
# 触发exit
# None
# None
# None
有异常的情况下,三个参数分别表示为:
- ext_type:错误(异常)的类型
- ext_val:异常变量
- ext_tb:追溯信息 Traceback
__exit__()
返回值为 None 或空时:
class Foo:
...
def __exit__(self, exc_type, exc_val, exc_tb):
print(‘触发exit‘)
print(exc_type)
print(exc_val)
print(exc_tb)
with Foo(‘a.txt‘) as f:
print(f.name)
print(dkdkjsjf) # 打印存在的东西,执行 __exit__(),完了退出整个程序
print(‘--------->‘) # 不会被执行
print(‘123‘) # 不会被执行
aa.txt
触发exit
<class ‘NameError‘>
name ‘dkdkjsjf‘ is not defined
<traceback object at 0x0000000004E62F08>
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-39-9fe07cc1dbe0> in <module>()
13 with Foo(‘a.txt‘) as f:
14 print(f.name)
---> 15 print(dkdkjsjf)
16 print(‘--------->‘)
17 print(‘123‘)
NameError: name ‘dkdkjsjf‘ is not defined
with 语句运行触发 __enter__()
返回,并获得返回值赋值给 f。遇到异常触发 __exit__()
,并将错误信息赋值给它的三个参数,整个程序结束,with 语句中出现异常的语句将不会被执行。
__exit__()
返回值为 True 时:
class Foo:
...
def __exit__(self, exc_type, exc_val, exc_tb):
print(‘触发exit‘)
print(exc_type)
print(exc_val)
print(exc_tb)
return True
with Foo(‘a.txt‘) as f:
print(f.name)
print(dkdkjsjf)
print(‘--------->‘)
print(‘123‘)
a.txt
触发exit
<class ‘NameError‘>
name ‘dkdkjsjf‘ is not defined
<traceback object at 0x0000000004E5AA08>
123
当 __exit__()
返回值为 True 时,with 中的异常被捕获,程序不会报错。__exit__()
执行完毕后,整个程序不会直接退出,会继续执行剩余代码,但是 with 语句中异常后的代码不会被执行。
好处:
- 把代码放在 with 中执行,with 结束后,自动清理工作,不需要手动干预。
- 在需要管理一下资源如(文件、网络连接和锁的编程环境中),可以在
__exit__()
中定制自动释放资源的机制。
总结:
- with 语句触发
__enter__()
,拿到返回值并赋值给 f。 - with 语句中无异常时,程序执行完毕触发
__exit__()
,其返回值为三个 None。 - 有异常情况时,从异常处直接触发
__exit__
:- 若
__exit__
返回值为 true,那么直接捕获异常,程序不报错 。且继续执行剩余代码,但不执行 with 语句中异常后的代码。 - 若
__exit__
返回值不为 True,产生异常,整个程序执行完毕。
- 若
2. 类的装饰器
Python 中一切皆对象,装饰器同样适用于类:
无参数装饰器
def foo(obj):
print(‘foo 正在运行~‘)
return obj
@foo # Bar=foo(Bar)
class Bar:
pass
print(Bar())
foo 正在运行~
<__main__.Bar object at 0x0000000004ED1B70>
有参数装饰器
利用装饰器给类添加类属性:
def deco(**kwargs): # kwargs:{‘name‘:‘tom‘, ‘age‘:1}
def wrapper(obj): # Dog、Cat
for key, val in kwargs.items():
setattr(obj, key, val) # 设置类属性
return obj
return wrapper
@deco(name=‘tom‘, age=1) # @wrapper ===> Dog=wrapper(Dog)
class Dog:
pass
print(Dog.__dict__) # 查看属性字典
print(Dog.name, Dog.age)
@deco(name=‘john‘)
class Cat:
pass
print(Cat.__dict__)
print(Cat.name)
{‘__module__‘: ‘__main__‘, ‘__dict__‘: <attribute ‘__dict__‘ of ‘Dog‘ objects>, ‘__weakref__‘: <attribute ‘__weakref__‘ of ‘Dog‘ objects>, ‘__doc__‘: None, ‘name‘: ‘tom‘, ‘age‘: 1}
tom 1
{‘__module__‘: ‘__main__‘, ‘__dict__‘: <attribute ‘__dict__‘ of ‘Cat‘ objects>, ‘__weakref__‘: <attribute ‘__weakref__‘ of ‘Cat‘ objects>, ‘__doc__‘: None, ‘name‘: ‘john‘}
john
@deco(name=‘tom‘, age=1)
首先执行 deco(name=‘tom‘, age=1)
,返回 wrapper
。再接着执行 @wrapper
,相当于 Dog = wrapper(Dog)
。最后利用 setattr(obj, key, value)
为类添加属性。
描述符+装饰器实现类型检查
class Descriptor:
def __init__(self, key, expected_type):
self.key = key # ‘name‘
self.expected_type = expected_type # str
def __get__(self, instance, owner): # self:Descriptor对象, instance:People对象,owner:People
return instance.__dict__[self,key]
def __set__(self, instance, value):
if isinstance(value, self.expected_type):
instance.__dict__[self.key] = value
else:
raise TypeError(‘你传入的%s不是%s‘ % (self.key, self.expected_type))
def deco(**kwargs): # kwargs:{‘name‘: ‘str‘, ‘age‘: ‘int‘}
def wrapper(obj): # obj:People
for key, val in kwargs.items():
setattr(obj, key, Descriptor(key, val)) # key:name、age val:str、int
return obj
return wrapper
@deco(name=str, age=int) # @wrapper ==> People = wrapper(People)
class People:
# name = Descriptor(‘name‘, str)
def __init__(self, name, age, color):
self.name = name
self.age = age
self.color = color
p1 = People(‘rose‘, 18, ‘white‘)
print(p1.__dict__)
p2 = People(‘rose‘, ‘18‘, ‘white‘)
{‘__module__‘: ‘__main__‘, ‘__init__‘: <function People.__init__ at 0x00000000051971E0>, ‘__dict__‘: <attribute ‘__dict__‘ of ‘People‘ objects>, ‘__weakref__‘: <attribute ‘__weakref__‘ of ‘People‘ objects>, ‘__doc__‘: None, ‘name‘: <__main__.Descriptor object at 0x00000000051BFDD8>, ‘age‘: <__main__.Descriptor object at 0x00000000051D1518>}
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-18-010cd074c06d> in <module>()
30 print(People.__dict__)
31
---> 32 p2 = People(‘rose‘, ‘18‘, ‘white‘)
<ipython-input-18-010cd074c06d> in __init__(self, name, age, color)
25 def __init__(self, name, age, color):
26 self.name = name
---> 27 self.age = age
28 self.color = color
29 p1 = People(‘rose‘, 18, ‘white‘)
<ipython-input-18-010cd074c06d> in __set__(self, instance, value)
11 instance.__dict__[self.key] = value
12 else:
---> 13 raise TypeError(‘你传入的%s不是%s‘ % (self.key, self.expected_type))
14
15 def deco(**kwargs): # kwargs:{‘name‘: ‘str‘, ‘age‘: ‘int‘}
TypeError: 你传入的age不是<class ‘int‘>
在利用 setattr()
设置属性的时候,对 value 进行类型检查。
3. 自定制 property
静态属性 property 可以让类或类对象,在调用类函数属性时,类似于调用类数据属性一样。下面我们利用描述符原理来模拟 property。
把类设置为装饰器,装饰另一个类的函数属性:
class Foo:
def __init__(self, func):
self.func = func
class Bar:
@Foo # test = Foo(test)
def test(self):
pass
b1 = Bar()
print(b1.test)
<__main__.Foo object at 0x00000000051DFEB8>
调用 b1.test
返回的是 Foo 的实例对象。
利用描述符模拟 property
class Descriptor: # 1
"""描述符"""
def __init__(self, func): # 4 # func: area
self.func = func # 5
def __get__(self, instance, owner): # 13
"""
调用 Room.area
self: Descriptor 对象
instance: Room 对象(即 r1或Room中的 self)
owner: Room 本身
"""
res = self.func(instance) #14 # 实现调用 Room.area,area需要一个位置参数 self,即 r1
return res # 17
class Room: # 2
def __init__(self, name, width, height): # 7
self.name = name # 8
self.width = width # 9
self.height = height # 10
# area = Descriptor(area),Descriptor 对象赋值给 area,实际是给 Room 增加描述符
# 设置 Room 的属性字典
@Descriptor # 3
def area(self): # 15
"""计算面积"""
return self.width * self.height # 16
r1 = Room(‘卧室‘, 3, 4) # 6
print(Room.__dict__) # 11
print(r1.area) # 12 调用 Descriptor.__get__()
{‘__module__‘: ‘__main__‘, ‘__init__‘: <function Room.__init__ at 0x0000000005206598>, ‘area‘: <__main__.Descriptor object at 0x000000000520C6A0>, ‘test‘: <property object at 0x00000000051FFDB8>, ‘__dict__‘: <attribute ‘__dict__‘ of ‘Room‘ objects>, ‘__weakref__‘: <attribute ‘__weakref__‘ of ‘Room‘ objects>, ‘__doc__‘: None}
12
首先执行 @Descriptor
,相当于 area = Descriptor(area)
,类似于给类 Room 设置 area属性。area 属性又被 Descriptor 代理(描述)。
所有当执行 r1.area
的时候,触发调用 Descriptor.__get__()
方法,然后执行 area() 函数
,并返回结果。
到目前为止,模拟 property 已经完成了百分之八十。唯一不足的是:property 除了可以使用实例对象调用外,还可以使用类调用。只不过返回的是 property 这个对象:
class Room:
...
@property
def test(self):
return 1
print(Room.test)
<property object at 0x000000000520FAE8>
那么我们也用类调用看看:
print(Room.area)
AttributeError: ‘NoneType‘ object has no attribute ‘width‘
发现类没有 width
这个属性,这是因为我们在使用 __get__()
方法时,其中 instance 参数接收的是类对象,而不是类本身。当使用类去调用时,instance = None
。
因此在使用模拟类调用时,需要判断是否 instance 为 None:
def __get__(self, instance, owner):
if instance == None:
return self
...
print(Room.area)
<__main__.Descriptor object at 0x0000000005212128>
?````
发现返回的是 Descriptor 的对象,与 property 的返回结果一样。到此为止,我们使用描述符成功地模拟 property 实现的功能。
**实现延迟计算功能**
要想实现延迟计算功能,只需每计算一次便缓存一次到实例属性字典中即可:
?```python
def __get__(self, instance, owner):
if instance == None:
return self
res = self.func(instance)
setattr(instance, self.func.__name__, res)
return res
...
print(r1.area) # 首先在自己的属性字典中找,自己属性字典中有 area 属性。因为已经缓存好上一次的结果,所有不需要每次都去计算
总结
- 描述符可以实现大部分 Python 类特性中的底层魔法,包括
@property、@staticmethod、@classmethod
,甚至是__slots__
属性。 - 描述符是很多高级库和框架的重要工具之一,通常是使用到装饰器或元类的大小框架中的一个组件。
4. 描述符自定制
4.1 描述符自定制类方法
类方法 @classmethod
可以实现类调用函数属性。我们也可以描述符模拟出类方法实现的功能:
class ClassMethod:
def __init__(self, func):
self.func = func
def __get__(self, instance, owner):
def deco(*args, **kwargs):
return self.func(owner, *args, **kwargs)
return deco
class Dog:
name = ‘tom‘
@ClassMethod # eat = ClassMethod(eat) 相当于为 Dog 类设置 eat 类属性,只不过它被 Classmethod 代理
def eat(cls, age):
print(‘那只叫%s的狗,今年%s岁,它正在吃东西‘ % (cls.name, age))
print(Dog.__dict__)
Dog.eat(2) # Dog.eat:调用类属性,触发 __get__,返回 deco。再调用 deco(2)
{‘__module__‘: ‘__main__‘, ‘name‘: ‘tom‘, ‘eat‘: <__main__.ClassMethod object at 0x00000000052D1278>, ‘__dict__‘: <attribute ‘__dict__‘ of ‘Dog‘ objects>, ‘__weakref__‘: <attribute ‘__weakref__‘ of ‘Dog‘ objects>, ‘__doc__‘: None}
那只叫tom的狗,今年2岁,它正在吃东西
类 ClassMethod 被定义为一个描述符,@ClassMethod
相当于 eat = ClassMethod(eat)
。因此也相当于 Dog 类设置了类属性 eat,只不过它被 ClassMethod 代理。
运行 Dog.eat(2)
,其中 Dog.eat
相当于调用 Dog 的类属性 eat,触发 __get__()
方法,返回 deco 。最后调用 deco(2)
。
4.2 描述符自定制静态方法
静态方法的作用是可以在类中定义一个函数,该函数的参数与类以及类对象无关。下面我们用描述符来模拟实现静态方法:
class StaticMethod:
def __init__(self, func):
self.func = func
def __get__(self, instance, owner):
def deco(*args, **kwargs):
return self.func(*args, **kwargs)
return deco
class Dog:
@StaticMethod
def eat(name, color):
print(‘那只叫%s的狗,颜色是%s的‘ % (name, color))
print(Dog.__dict__)
Dog.eat(‘tom‘, ‘black‘)
d1 = Dog()
d1.eat(‘lila‘, ‘white‘)
print(d1.__dict__)
{‘__module__‘: ‘__main__‘, ‘eat‘: <__main__.StaticMethod object at 0x00000000052EF940>, ‘__dict__‘: <attribute ‘__dict__‘ of ‘Dog‘ objects>, ‘__weakref__‘: <attribute ‘__weakref__‘ of ‘Dog‘ objects>, ‘__doc__‘: None}
那只叫tom的狗,颜色是black的
那只叫lila的狗,颜色是white的
{}
类以及类对象属性字典中,皆没有 name 和 color 参数,利用描述符模拟静态方法成功。
5. property 用法
静态属性 property 本质是实现了 get、set、delete 三个方法。
class A:
@property
def test(self):
print(‘get运行‘)
@test.setter
def test(self, value):
print(‘set运行‘)
@test.deleter
def test(self):
print(‘delete运行‘)
a1 = A()
a1.test
a1.test = ‘a‘
del a1.test
get运行
set运行
delete运行
另一种表现形式:
class A:
def get_test(self):
print(‘get运行‘)
def set_test(self, value):
print(‘set运行‘)
def delete_test(self):
print(‘delete运行‘)
test = property(get_test, set_test, delete_test)
a1 = A()
a1.test
a1.test = ‘a‘
del a1.test
get运行
set运行
delete运行
应用
class Goods:
def __init__(self):
self.original_price = 100
self.discount = 0.8
@property
def price(self):
"""实际价格 = 原价 * 折扣"""
new_price = self.original_price * self.discount
return new_price
@price.setter
def price(self, value):
self.original_price = value
@price.deleter
def price(self):
del self.original_price
g1 = Goods()
print(g1.price) # 获取商品价格
g1.price = 200 # 修改原价
print(g1.price)
del g1.price
80.0
160.0
6. 元类
Python 中一切皆对象,对象是由类实例化产生的。那么类应该也有个类去产生它,利用 type()
函数我们可以去查看:
class A:
pass
a1 = A()
print(type(a1))
print(type(A))
<class ‘__main__.A‘>
<class ‘type‘>
由上可知,a1 是类 A 的对象,而 A 是 type 类产生的对象。
当我们使用 class 关键字的时候,Python 解释器在加载 class 关键字的时候会自动创建一个对象(但是这个对象非类实例产生的对象)。
6.1 什么是元类
元类是类的类,也就是类的模板。用于控制创建类,正如类是创建对象的模板一样。
在 Python 中,type 是一个内建的元类,它可以用来控制生成类。Python 中任何由 class 关键字定义的类都 type 类实例化的对象。
6.2 定义类的两种方法
使用 class 关键字定义:
class Foo:
pass
使用 type()
函数定义:
type()
函数有三个参数:第一个为类名(str 格式),第二个为继承的父类(tuple),第三个为属性(dict)。
x = 2
def __init__(self, name, age):
self.name = name
self.age = age
def test(self):
print(‘Hello %s‘ % self.name)
Bar = type(‘Bar‘, (object,), {‘x‘: 1, ‘__init__‘: __init__, ‘test‘: test, ‘test1‘: test1}) # 类属性、函数属性
print(Bar)
print(Bar.__dict__)
b1 = Bar(‘rose‘, 18)
b1.test()
<class ‘__main__.Bar‘>
{‘x‘: 1, ‘__init__‘: <function __init__ at 0x00000000055A6048>, ‘test‘: <function test at 0x00000000055A60D0>, ‘test1‘: <function test1 at 0x0000000005596BF8>, ‘__module__‘: ‘__main__‘, ‘__dict__‘: <attribute ‘__dict__‘ of ‘Bar‘ objects>, ‘__weakref__‘: <attribute ‘__weakref__‘ of ‘Bar‘ objects>, ‘__doc__‘: None}
Hello rose
6.3 __init__ 与 __new__
构造方法包括创建对象和初始化对象,分为两步执行:先执行 __new__
,再执行 __init__
。
__new__
:在创建对象之前调用,它的任务是创建对象并返回该实例,因此它必须要有返回值,是一个静态方法。__init__
:在创建对象完成后被调用,其功能是设置对象属性的一些属性值。
也就是 __new__
创建的对象,传给 __init__
作为第一个参数(即 self)。
class Foo:
def __init__(self):
print(‘__init__方法‘)
print(self)
def __new__(cls):
print(‘__new__方法‘)
ret = object.__new__(cls) # 创建实例对象
print(ret)
return ret
f1 = Foo()
__new__方法
<__main__.Foo object at 0x00000000055AFF28>
__init__方法
<__main__.Foo object at 0x00000000055AFF28>
总结
__new__
至少要有一个参数 cls,代表要实例化的类,由解释器自动提供。必须要有返回值,可以返回父类出来的实例,或直接object
出来的实例。__init__
的第一个参数 self 就是__new__
的返回值。
6.4 自定义元类
如果一个类没有指定元类,那么它的元类就是 type,也可以自己自定义一个元类。
class MyType(type):
def __init__(self, a, b, c): # self:Bar a:Bar b:() 父类 c:属性字典
print(self)
print(‘元类 init 执行~‘)
def __call__(self, *args, **kwargs):
print(‘call 方法执行~‘)
obj = object.__new__(self) # Bar 产生实例对象,即 b1
print(obj)
self.__init__(obj, *args, **kwargs) # Bar.__init__(b1, name)
return obj
class Bar(metaclass=MyType): # Bar = MyType(‘Bar‘, (object,), {}) 触发 MyType 的 __init__()
def __init__(self, name): # self:f1
self.name = name
b1 = Bar(‘rose‘) # Bar 也是对象,对象加括号,触发 __call__()
print(b1.__dict__)
<class ‘__main__.Bar‘>
元类 init 执行~
call 方法执行~
<__main__.Bar object at 0x00000000055D3F28>
{‘name‘: ‘rose‘}
加载完程序后,首先执行 metaclass=MyType
,它相当于 Bar = MyType(‘Bar‘, (object,), {})
,它会执行 MyType 的 __init__
执行。
再接着执行 b1=Bar(‘rose)
,因为 Bar 也是对象,对象加括号就会触发 __call__()
方法,再由 __new__()
方法产生实例对象,最后返回。
总结
- 元类是类的模板,其功能是产生类对象,那么就可以模拟通过
__new__()
产生一个类的对象,并返回。 - 一切皆对象,那么类也是个对象,它是由 type 产生。
以上是关于面向对象进阶的主要内容,如果未能解决你的问题,请参考以下文章