面向对象进阶

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__()中定制自动释放资源的机制。

总结:

  1. with 语句触发 __enter__(),拿到返回值并赋值给 f。
  2. with 语句中无异常时,程序执行完毕触发 __exit__(),其返回值为三个 None。
  3. 有异常情况时,从异常处直接触发 __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 产生。
















以上是关于面向对象进阶的主要内容,如果未能解决你的问题,请参考以下文章

java学习---面向对象进阶

python-前方高能-面向对象-进阶3

python-面向对象进阶

进阶面向对象

python学习笔记-面向对象进阶&异常处理

python学习笔记-面向对象进阶&异常处理