python的面向对象
面向过程的编程
着重于做什么
面向对象的编程(oop)
着重于谁去做
类
用来描述具有相同属性或方法的对象的集合
**属性:**对象的描述信息
方法:对象的行为
类的特性是:封装、继承、多态
class ATM():
# 属性
money=50000
country='China'
# 方法
def get_money(self,money):
print(f"get money:{money}")
定义:
命名规范:一般首字母大写(驼峰式命名)
Person,GoodsInfo
定义关键字:class
python2 ---->经典类,形式类(显示继承object的是形式类,没有的其他都是经典类)
python3 ---->形式类(默认都是继承object,所以都是形式类)
class A:
pass
class B():
pass
class c(object):
pass
class ATM():
# 属性
money=50000
country='China'
bank="gonghang"
# 方法
def get_money(self,money):
print(f"get money:{money}")
# 实例化:创建一个类的实例
a1=ATM()
a2=ATM()
# print(dir(a1))
# 属性访问
print("实例访问".center(20,"*"))
print(a1.money,a2.country)
a1.get_money(500)
# 用类去访问
print("类访问".center(20,"*"))
print(ATM.money)
print("="*30)
a1.area="hunan"
print(a1.area)
ATM.color="blue"
print(a1.color,a2.color)
# 类空间和实例空间
print("类空间".center(30,"*"))
print(ATM.__dict__)
print("a1的空间".center(30,"*"))
print(a1.__dict__)
print("a2的空间".center(30,"*"))
print(a2.__dict__)
# 指针可以访问类空间里的值,但不可以改变类空间里的值,而是在实例空间内创建一个属性
a1.money=20000
print(a1.money)
print(a2.money)
#类创建的时候会生成类空间
#实例化对象生成实例空间,不同的实例之间,空间都是独立的
#实例查找属性方法的时候,先在实例空间去查找,找不到就去类空间查找
#类空间我不到,就去父类空间找
#实例空会有一个类对象指针,通过这个指针,实例就可以访问类的属性方法了
所有的这些对象都有命名空间,而继承就是由下而上,从左到右原则搜索此树,来寻找属性名称所 出现的最低的地方
对象/实例
具体的某个事物,实实在在的一个例子
实例化:创建一个类的实例
a1=ATM()
a2=ATM()
# print(dir(a1))
print(a1.money,a2.country)
a1.get_money(500)
结果
50000 China
get money:500
面向对象:
迭代更新方便
结构清晰,易于管理
特殊的方法 __init__方法
实例对象的构造方法(初始化方法)
实例化对象的时候自动调用__init __方法
class ATM():
# 类属性
money=50000
country='China'
bank="gonghang"
# 方法
def __int__(self,area):
# 实例属性
self.area=area
def get_money(self,money):
print(f"get money:{money}")
# 实例化
atm1=ATM("hunan")
atm2=ATM("hubei")
print(atm1.area,atm2.area)
总结:
——new——是创建实例的方法
——init——是创建好的实例进行初始化的方法
——new——方法必须要传入一个参数(cls),代表当前类
——new——必须返回一个实例对象
——init——的self就表示——new——返回的实例,——init——再对这个实例进行初始化工作
子类没有定义——new——,就会去找父类的——new——
新式类才有——new——
如果实例化对象和本身class不一致,——init——不会执行
——new——方法属于静态方法
——init——方法是实例方法
单例模式—会用于配置文件
无论实例化多少次,都返回同一个对象实例
——new——用同一个空间
class Danli:
instance=None
# 属性不会重复创建
def __new__(cls, *args, **kwargs):
if cls.instance is None:
cls.instance=object.__new__(cls)
return object.__new__(cls)
d1=Danli()
d2=Danli()
先执行new,再执行__init_
# self 代表当前对象,而不是类
# self不必非要写成self
# 约定俗成都使用self
#self可以不写
class User():
name="sc"
def infO(self):
print(f"I am {self.name}")
print(self)
print(self.__class__)
user1=User()
user1.infO()
user2=User()
user2.infO()
类的继承
面向对象的好处
面向对象的编程带来的主要好处之一是代码的重用,
实现这种重用的方法之一是通过继承机制。
继承完全可以理解成类之间的类型和子类型关系。
• 可以节省很多的代码,不需要写,直接使用
• 衍生出不同的子类,大部分代码一样,部分代码不一样
类的继承
• 代码:父类Parent/子类Child
• 子类有__ini__: 执行自己的初始化函数
• 子类没有__init__: 在子类实例化的时候会自动执行父类的构造函数
class Animal():
species = "Animal"
count = 0
def __init__(self):
self.name = "animal"
Animal.count += 1
print("初始化animal。。。")
def breath(self):
print("i can breath")
def eat(self):
print("i can eat")
class Person(Animal):
species = "Person" #重写父类的属性
animal1 = Animal()
print(animal1.count)
p = Person() #初始化自己没有init,执行父类的__init__
print(p.species, p.count)
#继承父类的属性和方法
#对象属性查找,先在实例空间找, 没有就去类空间找
#类空间也没有就去父类空间, 层层往上寻找 直到找到object
class Dog(Animal):
def __init__(self): #重写父类的__init__
print("i am dog")
def eat(self): #重写父类的eat方法
print("dog is eating...")
d = Dog()
print(p.name)
# print(d.name) 因为d重写了父类的init
d.eat()
class Pig(Animal):
count = 0 #重写父类的count属性
def __init__(self): #重写父类的__init__
super().__init__() #子类去访问父类的方法属性, 去执行父类__init__()
# self.name = "animal"
# Animal.count += 1
# print("初始化animal。。。")
# super().eat()
self.name = "pig"
Pig.count += 1
print("初始化pig")
# super().__init__()(最好放在前面,不然self.name又会被重写)
# self.name = "animal"
# Animal.count += 1
# print("初始化animal。。。")
# super().eat()
print("*"*20)
pig1 = Pig()
print(pig1.count, animal1.count, pig1.name)
#####################类与实例的关系
# print("****" * 20)
# print(isinstance(pig1, Pig),isinstance(pig1, Animal))pig1属于pig这个类,pig1属于animal类
# print(type(pig1))
# print(type(10), type("abc"))(内置的类)(python里一切皆对象)
# a = str(10)
# print(type(a), str.lower("ABC"), "ABC".lower())
使用继承的好处
• 如果需要给Pig/Person同时增加新的功能时,只需要在Animal中添加即可
• 如果你的父类方法的功能不能满足你的需求,你可以在子类重写你父类的方法父类里的方法
多态
接口重用,让程序更加灵活
class moneypay():
pass
class zhifubao(moneypay):
def pay(self):
print("this is zhifubao pay")
class bank(moneypay):
def pay(self):
print("this is bank pay")
class weixin(moneypay):
def pay(self):
print("this is weixin pay")
z = zhifubao()
b = bank()
w = weixin()
def pay(obj): #接口的多种形态, 接口的重用
obj.pay()
pay(z)
######
# def pay(int a, str b)
# a.p()
#
# def pay(moneypay z):
# z.p()
#python不支持多态 语法上的多态, 不需要额外实现多态代码
#按照多态的语法 不属于多态 (父类作为参数类型, 传递子类对象
#
#python 里处处是多态 python是一个多态类型语言,本身实现了多态
#崇尚鸭子类型, 不关心对象是什么类型,到底是不是鸭子,只关心有没有这个行为
Python中的多态
• 多态(Polymorphism)按字面的意思就是“多种状态” 。在面向对象语言中,接口的多种不同的 实现方式即为多态。
• Python是一种多态语言,崇尚鸭子类型。
• 在程序设计中,鸭子类型是动态类型的一种风格。
• 鸭子模型是指:"当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就 可以被称为鸭子。 "
• 我们并不关心对象是什么类型,到底是不是鸭子,只关心行为。
形式类和经典类
1、类型区别
# 经典类 通过type查看到的实例都是 instance
# 类和实例之间只能够通过__class__属性进行关联
# 新式类 通过type查看到的实例类型就是类名
# Type "help", "copyright", "credits" or "license" for more information.
# >>> class A: pass
# ...
# >>> a = A()
# >>> type(a)
# <type 'instance'>
# >>> a.__class__
# <class __main__.A at 0x7f59fe557f30>
# >>>
# ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────
# Type "help", "copyright", "credits" or "license" for more information.
# >>> class A: pass
# ...
# >>> a = A()
# >>> type(a)
# <class '__main__.A'>
# >>> a.__class__
# <class '__main__.A'>
# >>>
2、继承顺序
#多重继承时,继承顺序 经典类和新式类是有区别的
#经典类 – 深度优先算法
#新式类 – C3算法
class A:
def test(self):
print("from A")
class B(A):
def test(self):
print("from B")
class C(A):
def test(self):
print("from C")
class D(B):
def test(self):
print("from D")
class E(C):
def test(self):
print("from E")
class F(D,E):
def test(self):
print("from F")
f = F()
f.test()
#mro方法解析序列
#经典类: F --》 D --》B --》A --》 E --》C
#新式类: F --》 D --》B --》E --》C --》A
#c3算法
#首先 将自身类加入本序列, 然后再对继承序列的元素依次判断
#若某个元素不在其他序列或者它是所有继承序列的第一个,那么就把这个元素提取到本序列
在Python 2.x及以前的版本中,由任意内置类型派生出的类(只要一个内置类型位于类树的某个位 置),都属于“新式类”
不由任意内置类型派生出的类,则称之为“经典类” 。
“新式类”和“经典类”的区分在Python 3.x之后就已经不存在,在Python 3.x之后的版本,因为所有 的类都派生自内置类型object(即使没有显示的继承object类型),即所有的类都是“新式类” 。
建议使用新式
经典类与新式类在类型上的区别
继承了python内置类的是新式类,其他是经典类
• 经典类
• 所有的类都是classobj类型,而类的实例都是instance类型。
• 类与实例只有通过__class__属性进行关联
• 新式类
• 类实例的类型是这个实例所创建自的类(通常是和类实例的__class__相同)
Python的类如果继承了多个类,那么其寻找方法的方式有两种,分别是:深度优先和广度优先
经典类与新式类的继承继承原理
• MRO(Method Resolution Order):方法解析顺序
• MRO是在Python类的多重继承中,解决当定义了多个同名的方法/属性时,为避免产生歧义,保证 用户找到正确的对象所实现的一种算法。
• 对于你定义的每一个类,Python会计算出一个方法解析顺序(MRO)列表,这个MRO列表就是一 个简单的所有基类的线性顺序列表
经典类与新式类的继承继承原理
• Python2.2以前的版本:经典类(classic class)时代
• MRO的方法为DFS(深度优先搜索(子节点顺序:从左到右))
• Python2.2版本:新式类(new-style class)诞生,这时有两种MRO的方法
• 经典类MRO为DFS(深度优先搜索(子节点顺序:从左到右))
• 新式类MRO为BFS(广度优先搜索(子节点顺序:从左到右))
• Python2.3到Python2.7:经典类、新式类和平发展
• 从Python2.3开始新式类的MRO取而代之的是C3算法
• Python3到至今:新式类一统江湖
• Python3开始就只存在新式类了,采用的MRO也依旧是C3算法
C3算法:
它实际上就是合并所有父类的MRO列表并遵循如下三条准则:
• 子类会先于父类被检查
• 多个父类会根据它们在列表中的顺序被检查
• 如果对下一个类存在两个合法的选择,选择第一个父类
#c3算法
#首先 将自身类加入本序列, 然后再对继承序列的元素依次判断
#若某个元素不在其他序列或者它是所有继承序列的第一个,那么就把这个元素提取到本序列
静态方法、类方法、实例方法
属性
• 静态属性(所有的实例共用一份=>引用)
• 普通属性(每个实例都不同的数据=>引用)
方法
• 普通(实例)方法(使用实例中的数据) => 给实例使用(一定要传递一个实例,由实例调用)
• 静态方法(无需使用实例封装的内容@staticmethod)
• 类方法(会自动加当前类的类名 @classmethod) => cls表示类本身
class A:
name = "class A"
def __init__(self):
self.country = "china"
#实例方法(__init__方法)
#第一参数代表实例本身 self -- 实例
def normal_method(self, name):
#方法的里面 既可以访问类属性,又可以访问实例属性
print("normal:")
print(self.name, name)
#类方法
#第一参数代表类 cls-- 类
@classmethod
def class_method(cls, name):----第一个参数cls代表属于那个类
#类方法里面 可以访问类属性
print("classmethod:")
print(cls, cls.name)
#静态方法---可以不传参,但实例方法和类方法必须传参
@staticmethod(__new_方法)
def static_method(name):
#静态方法里面,可以通过类名去访问类属性
print("static_method:",name, A.name)
a1 = A()
##############静态方法、实例方法、类方法都能通过实例去调用
a1.normal_method("a1")
a1.class_method("a1")
a1.static_method("a1")
#也可以通过类去调用
A.normal_method(a1, "A")
A.class_method("A")(第一个参数不用传,会自动传)
A.static_method("A") (有参数传就传,没有就不传)
各种方法的区别(@staticmethod、@classmethod)
• 实例方法不能通过类名调用,但是静态方法和类方法是可以(不实例化访问)
• 实例方法:可以通过self访问实例属性
• 类方法:可以通过cls访问类属性(希望取的值不受实例影响时使用)
• 静态方法:不可以访问,通过传值的方式
属性包装(@propert)
• 把函数包装成属性,使用的时候用对象名.属性名
• Person类中有一个属性为age,控制age的范围只能在0~150之间
属性包装的应用
• property广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检 查,这样,程序运行时就减少了出错的可能性
Python类中的下划线
• 标识符是用来标识某种对象的名称。
• 在命名标识符时,需要遵循一定规则。标识符的第一个字符必须是字母(大小写均可),或者是一个下划 线(“_”)。
• 以下划线开头的标识符是有特殊意义的
class P:
'''(文档注释)
this is P
P test
'''
_min = 1 #保护属性
__max = 10 #私有属性
def __init__(self):
self.name = "sc"
self._age = 4
self.__desc = "it"
def __make(self):
print("这是一个私有方法")
print(self.__desc)
def _protectmake(self):
print("这是一个保护方法")
def show(self):
print(self.__max, self.__desc)
class Child(P):
def show(self):
print(self.__max)
p = P()
c = Child()
# print(c._min, p._min,Child._min, P._min)
#子类访问不到私有成员
# c.show()
# print(c.__make)
#类对象也不能访问私有成员
# print(p.__max)
# p.__make()
#私有成员只能在类的内部进行访问
# p.show()
# print(dir(p))(查看属性)
#python 中的私有都是伪私有 实际其实就是将双下划线开头的标识符,
# 改了一个名字存储 _类名__标识符
就是让实例对象访问类的私有属性
# print(p._P__max)
#常见的以双下划线开头和以双下划线结尾的特殊变量
#__dict__ 查看命名空间
# print(P.__dict__)
# print(p.__dict__)
#__name__类名
# print(P.__name__)
# def func1(cls):
# a = cls()
# print(f"{cls.__name__}进行了实例化" )
# class A:
# pass
# class B:
# pass
# func1(A)
# func1(B)
#__class__查看对象属于哪个类
# print(p.__class__)
#__module__查看所在模块
# print(P.__module__)
#__doc__ 文档注释
# print(P.__doc__) ##help自动调用__doc__属性
常用魔术方法和属性
魔术方法:
一般以双下划线开头和双下划线结尾,而且是有特殊含义的方法,一般不需要手动去调用,在特定场景下自动执行
构造函数(__new__/__init__)
• __new__:创建实例
• __init__:初始化实例
析构函数(__del__)
• 在实例释放、销毁的时候自动执行的,通常用于做一些收尾工作, 如关闭一些数据库连接,关闭 打开的临时文件
调用方法(__call__)
• 把类实例化后的对象当做函数来调用的时候自动被调用
class A:
def __del__(self):
print("this is A.del")
# a1 = A()
# # del a1
# print("xxxxxxxxxxxxxxxx")
#########__call_
#把实例化之后的对象当作函数去调用
# class A:
# def __call__(self,name):
# print(f"i am A.__call__, name is {name}")
# a1 = A()
# a1("sc")
# def func1():
# pass
#
# print(dir(func1))
__str__ __repr__
__str__ 给用户看的, 没有__str__ 默认调用__repr__
__repr__ 给程序员看的, 更加官方一点
返回对象的描述信息
>>> class A:
... def __str__(self):
... return "str...A"
... def __repr__(self):
... return "repr...A"
...
>>> a = A()
>>> print(a)
str...A
>>> a
repr...A
>>>
class NotIsIntException(Exception):
def __str__(self):
return 'NotIsIntException类型不是整数'
n = input('请输入一个整数')
try:
if n.isdigit():
print(n)
else:
raise NotIsIntException
except NotIsIntException as ni:
print(ni)
__getitem__
__setitem__
__delitem__
对象以字典的形式去设置或获取参数
class A:
def __init__(self):
self.data = {}
def __getitem__(self, key):
print("get data")
return self.data.get(key, 0)
def __setitem__(self, key, value):
print("set data:", key, value)
self.data[key] = value
def __delitem__(self,key):
print("delete data")
del(self.data[key])
a1 = A()
print(a1["key1"])
a1["key1"] = "xxxxxxxxxxx"
del a1["key1"]
获取数据(getitem)
• a[key]来访问对象,a[1:3]切片时调用__getitem__
删除数据(delitem)
• del r[1] 时调用__delitem__方法
设置数据(setitem)
• r[‘e’]=213时调用__setitem__
其他魔术方法
• eq(self, other) 定义了等号的行为, ==
• ne(self, other) 定义了不等号的行为, !=
• lt(self, other) 定义了小于号的行为, <
• gt(self, other) 定义了大于等于号的行为, >=
• add(self, other) +运算
class A:
def __init__(self, num):
self.num = num
def __add__(self, x):
print("this is add")
return self.num + x
a = A(4)
print(a + 6)
• mul(self, other) *运算
• len(self, other) 获得长度
查看类属性和方法
• dir(Person):了解Person类里的变量和方法
• Person.dict :可以了解哪些是变量哪些是方法
内置属性及其功能
• dict:类的属性(包含一个字典,由类的数据属性组成)
• doc:类的文档字符串
• name:类名
• module:类定义所在的模块
• bases:类的所有父类构成元素
python的自省
什么是自省
• 在计算机编程中,自省是指这种能力:检查某些事物以确定它是什么、它知道什么以及它能做什 么。自省向程序员提供了极大的灵活性和控制力。
• 有时我们要访问某个变量或是方法时并不知道到底有没有这个变量或方法,所以就要做些判断。判 断是否存在字符串对应的变量及方法。
NameError: name 'b' is not defined---没有定义变量
'....'object has no attribute---没有方法
• 我们知道访问变量时是不能加引号的,否则会被当成字符串处理。如果要通过字符串找到对应的变 量,那该怎么办呢
自省有4个方法
• getattr(obj, ‘name’): 获取成员
• 根据字符串去获取obj对象里的对应的方法的内存地址
• hasattr(obj, ‘name’): 检查是否含有成员
• 判断一个对象obj里是否有对应的name_str字符串的方
• setattr(obj, ‘age’ , 18): 设置成员
• delattr(obj, ‘name’): 删除成员
import sc
#python 自省
print(hasattr(sc,'a'))#判断scli有没有a属性,为Ture表示有属性,反之没有
print(hasattr(sc,'b'))
print(getattr(sc,'a'))#获取sc里的属性a的值
print(setattr(sc,"b","scbbbbb"))#设置属性
print(getattr(sc,'b'))
delattr(sc,"b")
print(hasattr(sc,'b'))#删除属性
print(sc.a)
class A:
def func1(self):
print("i am func1")
def func2(self):
print("i am func2")
a=A()
#接收用户从键盘的输入,可以无限输入按q推出
#如果输入func1就执行fun1
#如果func2就执行fun2
#其他情况提示没有这个函数
#思考:判断函数是否要传参数
choose=input("please input function:")
while choose != "q":
if hasattr(a,choose):
ruslut=getattr(a, choose)
if callable(ruslut):----判断是否是函数(if hasattr(ruslut,'__call__'))
ruslut()
else:
print(ruslut)
else:
print("没有这个函数")
choose = input("please input function:")
else:
print("已退出")
元类
类也是对象 ,类也是由另一个类创建的
- Python中一切皆对象, 包括函数和类。这意味着函数与类都可以作为参数提供、以类实例的成员 形式存在 ,且可以完成其他对象所能完成的工作
- 对象在实例化的过程中,会调用__init__和__new__方法创建新对象, 该过程对于类而言也不例 外。一个类本身又是另一个类的实例,用于创建类的类。负责生成其他类的类就是元类 (Metaclass)
- 元类就是用来创建类的类。
- type是最上层的类
class A:
pass
a1=A()
print(type(a1))
print(type(A))
<class '__main__.A'>
<class 'type'>----type元类
type创建类
• 语法:
type(类名, 父类的元组(针对继承的情况,可以为空),包含属性的字典(名称和值))
• 创建类:
MyClass = type('MyClass' , (), {})
函数type实际上是一个元类, type就是Python在背后用来创建所有类的元类(类工厂) 如:1 -> int -> type type就是Python的内建元类,我们也可以创建自己的元类
class Animal():
# def __init__(self,name):
# self.name=name
# def eat(self):
# print("i am eationg...")
##=怎么使用type创建和上面一样的类
def init(self,name):
self.name=name
def eat(self):
print("i am eating..")
#第一个参数:类名
#第二个参数:继承关系,元组---只有一个元素的时候要打“,“
#第三个参数:属性和方法,字典----属性和方法名作为key,自设属性和方法名作为值
Animal=type("Animal",(object,),{"__init__":init,"eat":eat})
a1=Animal("sc1")
print(a1.name)
指定元类
• 默认元类为
type class Foo(Bar):
__metaclass__ = something
class Foo(Bar, metaclass=something):pass
• Python会在类的定义(父类)中寻找__metaclass__属性,如果找到了,Python就会用它来创建类 Foo,如果没有找到,就会用内建的type来创建这个类。
• __metaclass__
中放置可以创建一个类的东西(元类)
编写元类
• 编写元类非常简单,只需要声明一个继承自type的类。元类的行为继承自type,因此type的子类 都可以作为元类。
元类的主要目的就是为了当创建类时能够自动地改变类
• 拦截类的创建
• 修改类,返回修改之后的类
class MyMate(type):
def __new__(cls,name,bases,attrs):
if 'ufo' not in attrs:
raise TypeError("必须设置ufo属性")
class A(metaclass=MyMate):---调用MyMate创建实例A,但是__new__没有返回值
ufo="sc"
输出:None
class MyMate(type):
def __new__(cls,name,bases,attrs):
if 'ufo' not in attrs:
raise TypeError("必须设置ufo属性")
return type.__new__(cls,name,bases,attrs)
class A(metaclass=MyMate):
ufo="sc"
print(A)
输出:<class '__main__.A'>
python面向对象的关系
1.继承关系 object是所有类的父类,是最顶层的类
2.创建关系、实例与类的关系type是最顶层的类
print(type(object))
print(type(type))
print(object.__bases__)
print(type.__bases__)
<class 'type'>
<class 'type'>
()
(<class 'object'>,)
__metaclass__中放置些什么代码呢?
• 答案就是:可以创建一个类的东西(元类)。
元类的主要目的就是为了当创建类时能够自动地改变类
• 拦截类的创建
• 修改类
• 返回修改之后的类
抽象基类
定义了接口规范,子类必须实现抽象基类父类里的抽象方法,不是抽象方法不需要实现
抽象基类不能实例化
from abc import ABC,abstractmethod
class Animal(ABC):
#定义抽象方法
@abstractmethod
def eat(self):
print("this is eat")
def drink(self):
pass
class Dog(Animal):
pass
d1=Dog()
Traceback (most recent call last):
File "D:\运维开发\python\python项目\2022-5-28面向对象\12.抽象基类.py", line 11, in <module>
d1=Dog()
TypeError: Can't instantiate abstract class Dog with abstract method eat
from abc import ABC,abstractmethod
class Animal(ABC):
#定义抽象方法
@abstractmethod
def eat(self):--不用写
pass
def drink(self):
pass
class Dog(Animal):----要重写抽象基类
def eat(self):
print("this is eat")
d1=Dog()
- 只需要定义接口规范,不需要具体实现
Abstract Base Class(抽象基类)
• python中并没有提供抽象类与抽象方法,但是提供了内置模块abc(abstract base class)来模拟 实现抽象类。
• ABC,A bstract Base Class(抽象基类),主要定义了基本类和最基本的抽象方法,可以为子类 定义共有的API,不需要具体实现。
• 抽象基类提供了逻辑和实现解耦的能力,即在不同的模块中通过抽象基类来调用
• ABC类实际上是一个元类