python面对对象编程
类
# 封装
class Hero:
hero_work = "射手"
def __init__(self, name, hp, speed, atk):
self.name = name
self.hp = hp
self.speed = speed
self.atk = atk
self.equipment = []
def print_hero(self):
print(
f"英雄职业:{self.hero_work},英雄名称:{self.name},英雄血量:{self.hp},英雄移速:{self.speed},英雄攻击力:{self.atk},所有装备:{self.equipment}。")
def hero_speed_add(self, speed):
self.speed += speed
def hero_buy_equipment(self, equipment):
self.equipment.append(equipment)
@staticmethod
def test():
print("你好!")
hero1 = Hero("卡萨丁", 900, 350, 60)
hero2 = Hero("卡莎", 910, 360, 70)
hero1.print_hero()
hero1.hero_speed_add(60)
hero1.hero_buy_equipment("反甲")
hero1.hero_work = "法师"
# 访问类里面的数据和功能的方式叫属性访问,类里面定义的变量属于数据属性,类里面定义的函数属于函数属性
print(Hero.__dict__) # 这里可以获得关于类的信息(以字典形式表现)
print(Hero.__dict__['hero_work']) # 这里就可以用字典方法调用类里的值
print(Hero.hero_work) # 这里只是Python给出的简便语法,本质上还是通过字典取值
hero1.print_hero()
hero2.print_hero() # 这里调用函数只需加上括号即可,若有必须传的参数也要填上
调用类的过程:
1、创建空对象
2、调用__init__方法,同时把空对象,以及调用类的时候括号里传的参数,一同传递给__init__
3、返回初始化之后的对象(注意这里的对象并不是__init__返回的而是类底层做到的事情)
调用类的时候需要自动执行的代码我们都可以放在__init__里面
类的属性查找
# 类的属性查找
print(hero1.name)
print(hero1.hero_work)
print(hero2.name)
print(hero2.hero_work)
- 这里我们可以看到,我们在创建hero1这个对象的时候并没有定义相关的职业参数,返回的是类当中自带的职业参数
- 所以说关于类的属性查找首先是在对象当中找属性,找不到了才返回类当中定义的属性
- 那么在这里如果修改了类里的属性,那么根据这个类说实例化的对象都能感知到这个属性的变化
类的函数属性(绑定方法)
# 类的函数属性
Hero.print_hero(hero1)
Hero.hero_speed_add(hero1, 60)
Hero.print_hero(hero1)
- 这里使用类去调用函数,就需要给函数传输对象才能正常使用
- 但是类里面定义的函数,主要是拿给对象去用的,而且是绑定给对象去用的,虽然所有对象指向的都是相同的功能,但是绑定到不同的对象,就会变成不同的绑定方法
print(Hero.hero_speed_add)
print(hero1.hero_speed_add)
print(hero2.hero_speed_add)
以上运行结果如下所示:
<function Hero.hero_speed_add at 0x000002040FA8BAC0>
<bound method Hero.hero_speed_add of <__main__.Hero object at 0x000002040FAA**3FD0**>>
<bound method Hero.hero_speed_add of <__main__.Hero object at 0x000002040FAA**3DC0**>>
这里我把不同的部分标注出来,让大家看得更清楚,可以看到在第一次输出类里的函数得到的是一个普通的函数地址,但是之后通过对象去调用函数的时候,它就变成了一个绑定方法地址,而且不同的绑定对象对应的函数地址也不相同,这就是所谓的绑定到不同的对象,就会变成不同的绑定方法
hero1.print_hero()
hero2.print_hero()
可以看到当我们通过对象去调用函数的时候我们并不需要像使用类调用函数那样给函数传输对象,默认就将这个对象传进函数中了,这也是绑定方法的一种体现,也就是说绑定方法会和对象绑定,在调用绑定方法时会自动把对象,也就是“self”这个参数自动传进去
hero1.hero_buy_equipment("黑色收割者")
hero2.hero_buy_equipment("亡者的板甲")
hero1.print_hero()
hero2.print_hero()
同样的因为绑定方法的存在,我们在类当中定义函数的时候,最少都需要一个形参,这里的形参就是对绑定方法时所用到的对象本身
如果我们不设置这个形参的话,那么就不会形成绑定方法,不形成绑定方法就无法使用对象来调用该函数,但是我们仍旧可以通过类来调用,如下:
Hero.test() # 这里正常运行,输出“你好!”
hero1.test() 这里报错,提示hero1只能接受0个位置参数,但是你传了一个。这就是绑定方法传输的对象本身
绑定方法:谁调用这个功能,这个功能处理的就是谁的数据
封装
封装:整合
类的隐藏属性
类的隐藏属性可以让使用者没办法直接使用,但并不是无法使用
- 隐藏属性的本质,其实只是对名称进行了更改
- 对外不对内,即在类的内部可以正常调用,在类的外部就需要在隐藏属性前加上 _(类名)才能调用
- 改名操作只会在类定义的时候,检查子代码语法的时候执行一次,之后定义的__开头的属性,都不会改名
class Test:
__x = 10 # 类的定义名称为:_Test__x 这一步只在类的定义阶段运行一次
@staticmethod
def __f1(): # _Test__f1
print("f1")
def f2(self):
print(self.__x) # 对外不对内,这里可以正常访问
print(self.__f1)
obj = Test()
obj.f2()
Test.__y = 20 # 这里定义的__y并没有变成_Test__y
print(obj.__dict__) # 空对象,什么也没有
print(Test.__dict__)
print(obj.__x) # 这里运行报错
包括隐藏函数也可以通过下述方法调用,属于不规范用法
print(obj._Test__x)
为什么要隐藏属性:
1、隐藏属性后,使用者只能通过开放给他的接口获取属性
2、对开放的接口进行一定的限制就能避免使用者修改属性时的不合理操作
3、隐藏函数可以让使用者在使用时看不见一些乱七八糟的对于使用者没啥用的函数,增加其实用性
如下:
class Man1:
def __init__(self, name, age):
self.__name = name
self.__age = age
def get_age(self):
return self.__age
def get_name(self):
return self.__name
def change_age(self, new_age):
if type(new_age) is not int:
print("输入错误,年龄必须是整数")
elif new_age > 120 or new_age < 0:
print("输入错误,年龄必须大于等于0且小于120")
else:
self.__age = new_age
print("修改成功")
def del_age(self):
del self.__age
man = Man1("SB", 10)
man.change_age(12.4) # 这里修改失败,输出:输入错误,年龄必须是整数
print(man.get_age()) # 输出:10
以上就是隐藏属性后,针对用户在修改属性时避免一些不合理操作的具体实现
类装饰器
在上面的隐藏属性的实例代码中,我们在修改属性的时候就需要另写一个函数,然后调用该函数才能对属性进行修改,但是在我们一开始学习python的时候,有个叫字典的数据类型,在修改字典中某一个值的时候,我们只需要dict ['键名称'] = '新的值'
就可以完成值的修改了,而删除也只需要del site['name']
就可以删除了,明显这样的方法对于使用类的人来说更加直观好用,那么我们自建的类要如何实现以上方法修改属性呢,就需要引入类装饰器了。如下所示:
class Man2:
def __init__(self, name, age):
self.__name = name
self.__age = age
@property # 查看属性
def age(self):
return self.__age
@age.setter # 修改属性
def age(self, new_age):
if type(new_age) is not int:
print("输入错误,年龄必须是整数")
elif new_age > 120 or new_age < 0:
print("输入错误,年龄必须大于等于0且小于120")
else:
self.__age = new_age
print("修改成功")
@age.deleter # 删除属性
def age(self):
del self.__age
@property
def name(self):
return self.__name
@name.setter
def name(self, new_name):
self.__name = new_name
@name.deleter
def name(self):
del self.__name
man1 = Man2("tjy", 24)
man1.age = 20.4
man1.age = 15
print(man1.age)
# del man1.age
# print(man1.age) 这里删除后报错提示没有这个参数
通过以上方法我们就做到了像字典那样直观的删改方法。注意,带有装饰器的函数名都是相同的。
- 装饰器只是让使用类创建的对象的人用起来更直观,对于编程来说,我们还是需要在类内部写三个函数来实现属性的查、改、删,也就是说我们一样可以限制用户在修改时的规范来避免一些错误。
继承
继承:创建新类的方式,通过继承创建的类称之为子类,被继承的类称之为父类(基类)
class Test1:
x = 10
pass
class Test2(object):
pass
class Parent1(Test1): # 单继承
pass
class Parent2(Test1, Test2): # 多继承
pass
# 接下来我们让他输出自己的父类,如下:
print(Parent1.__bases__) # (<class '__main__.Test1'>,)
print(Parent2.__bases__) # (<class '__main__.Test1'>, <class '__main__.Test2'>)
在Python2中有新式类和经典类的区分
- 新式类:继承了object类的子类,以及继承了这个子类的子子孙孙类
- 经典类:没有继承object类的子类,,以及继承了这个子类的子子孙孙类
print(Test1.__bases__) # (<class 'object'>,) Python3默认创建类的时候继承object类,所以所有类都是新式类,为了兼容Python2需要在创建类的时候加上括号和object
print(Test2.__bases__) # (<class 'object'>,)
# 继承的特性:遗传
print(Parent1.x) # 这里的Parent1中是没有x这个属性的,但是仍旧输出10
继承的特性:逐级寻找,先检查自己有没有,自己没有就用爸爸的,爸爸还没有就用爷爷的
多继承
优点:
一个子类可以同时获得多个父类的属性
缺点:
1、违背人的思维习惯
2、使代码的可读性变差
如果必须使用多继承,应该使用Mixins(一种多继承编程规范)
派生
在Python中,派生(Derivation)通常指的是从一个基类(Base Class)创建一个子类(Subclass)的过程。这个过程涉及到面向对象编程(Object-Oriented Programming,OOP)中的继承和扩展的概念。
子类会继承基类中的所有属性和方法,同时还可以定义自己的新属性和方法,或者覆盖(Override)基类中的方法。通过派生,我们可以创建一个更特定的类型,这个类型继承了基类的通用特性,并添加了额外的特性或行为。
下面是一个简单的例子来说明派生:
class Human:
star = "earth"
def __init__(self, name, age, gender):
self.name = name
self.age = age
self.gender = gender
class Chinese(Human):
nation = "china"
def __init__(self, name, age, gender, balance): # 派生3:在父类的基础上修改改进扩展
Human.__init__(self, name, age, gender)
self.balance = balance
def speak_chinese(self):
print(f"{self.name}正在说普通话")
class American(Human):
star = "moon"
nation = "America"
def speak_english(self): # 派生1:子类派生父类没有的属性
print(f"{self.name}正在说英语")
tjy_obj = Chinese("唐季宇", 22, "男", 100)
print(tjy_obj.__dict__) # 使用的父类的__init__创建的对象
print(tjy_obj.star) # 从父类中调取的属性
print(tjy_obj.nation)
tjy_obj.speak_chinese()
print("\n")
ygr_obj = American("cheng_yi_peng", 23, "男")
print(ygr_obj.__dict__) # 使用的父类的__init__创建的对象
print(ygr_obj.star) # 派生2:因为自己有star属性所以用自己的不用父类的
print(ygr_obj.nation)
ygr_obj.speak_english()
属性查找
属性查找
对象-类-父类-祖父类-…-object
多继承属性查找是根据MRO列表依次查询,这个MRO列表根据C3算法实现,MRO列表就是一个简单的所有基类的线性顺序列表
# 菱形继承
class A:
def f1(self):
print("A.f1")
pass
class B(A):
def f1(self):
print("B.f1")
pass
class C(A):
def f1(self):
print("C.f1")
pass
class D(B, C):
def f2(self):
print(self.__dict__)
pass
print(D.mro()) # [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
obj = D()
obj.f1() # B.f1 这里根据以上输出的列表顺序进行查找,B类里有,所以返回B.f1
# 非菱形继承
class A1:
A = 10
B = 100
@staticmethod
def A1():
print("A.f1")
pass
class B1:
A = 20
B = 200
def f1(self):
print("B.f1")
pass
class C1(A1):
C = 30
D = 300
@staticmethod
def f1():
print("C.f1")
pass
class D1(B1):
C = 40
D = 400
def f1(self):
print("D.f1")
pass
class E1:
E = 50
@staticmethod
def f1():
print("E.f1")
pass
class F1(C1, D1, E1):
F = 60
def f2(self):
print(self.__dict__)
pass
print(F1.mro()) # [<class '__main__.F1'>, <class '__main__.C1'>, <class '__main__.A1'>, <class '__main__.D1'>, <class '__main__.B1'>, <class '__main__.E1'>, <class 'object'>]
obj_1 = F1()
print(obj_1.D) # 输出C1类中的300
print(obj_1.B) # 输出A1类中的100
class Z2:
pass
class A2(Z2):
A = 10
B = 100
def f1(self):
print("A.f1")
pass
class B2(Z2):
A = 20
B = 200
def f1(self):
print("B.f1")
pass
class C2(A2):
C = 30
D = 300
def f1(self):
print("C.f1")
pass
class D2(B2):
C = 40
D = 400
def f1(self):
print("D.f1")
pass
class E2(Z2):
E = 50
@staticmethod
def f1():
print("E.f1")
pass
class F2(C2, D2, E2):
F = 60
def f2(self):
print(self.__dict__)
pass
print(F2.mro())
多继承非菱形继承属性查找规则:
1、对象中没有找类里面,类没有找父类,这里找父类的顺序为括号中继承的顺序
2、当括号中第一个父类没有的话不会立即找括号里第二个类,而是找第一个类父类的父类,即会把第一个父类继承的所有类找完,才会找括号中第二个父类
3、object类永远是最后一个找的
复杂菱形结构继承属性查找规则:
经典类(没有继承object的类):深度优先查找
即优先将一条分支的所有父类查找完毕,如果第二条分支中有重复部分,重复部分不再查找
新式类(继承了object的类):广度优先查找
即找完所有的分支之后,在对重叠部分的父类进行查找
MixIns机制
多继承需要解决的问题:
1、继承结构简单化
2、满足“”是“”的关系
MixIns机制
- 通过命名规范来提升代码的可读性
- 在使用MixIns机制的时候,应该把子类继承的真正的父类放在最后一个
- 在对应的MixIns类里编写功能的时候,不要调用子类中的数据或功能,使其单独成立,仅仅作为功能接口使用
MixIns机制后缀:MixIns、able
class Fowl: # 家禽类
pass
class SwimMixIns: # 加入MixIns来区分这个类,告诉我们他并不是鸭子和鹅的父类,而是混入其中的一个功能
pass
class Chicken(Fowl): # 鸡
pass
class Duck(SwimMixIns, Fowl): # 鸭
pass
class Goose(SwimMixIns, Fowl): # 鹅
pass
super方法
super() 是一个内置函数,用于调用父类(超类)的方法。它可以解决多重继承的问题,因为 Python 支持多继承,如果子类继承了多个父类,而多个父类又继承了相同的父类,那么子类在调用时就可能出现冲突。使用 super() 函数可以避免这种冲突。
super() 函数的主要作用是让子类调用父类的方法,通常用在子类的__init__方法中。在子类的__init__方法中,通过 super() 调用父类的__init__方法,可以确保父类的__init__方法被正确执行,从而避免子类覆盖父类的属性或方法。
super() 函数的语法是:super(class_type, self).method(args)
其中
- class_type 是当前类的类型;
- self 是当前类的实例;
- method 是要调用的父类的方法名;
- args 是传递给方法的参数。
在实际使用中,super() 函数通常被简化为 super().method(args) 的形式。这是因为 Python 解释器会自动获取当前类和实例,并调用相应的父类方法。
super() 函数的主要优点是:
- 它可以动态地查找和调用父类的方法,这使得代码更加灵活和易于维护。
- 同时还可以避免硬编码父类的名称,从而提高了代码的可读性和可移植性。
class Human1:
star = "earth"
def __init__(self, name, age, gender):
self.name = name
self.age = age
self.gender = gender
class Chinese1(Human1):
nation = "china"
def __init__(self, name, age, gender, balance): # 派生3:在父类的基础上修改改进扩展
# Human.__init__(self, name, age, gender)
# super(Chinese1, self).__init__(name, age, gender) # python2写法
super().__init__(name, age, gender) # python3写法
# super会参照self所属类的MRO列表,从super所在的类所处位置的下一个类开始找属性
self.balance = balance
def speak_chinese(self):
print(f"{self.name}正在说普通话")
class American1(Human1):
star = "moon"
nation = "America"
def speak_english(self): # 派生1:子类派生父类没有的属性
print(f"{self.name}正在说英语")
tjy_obj = Chinese1("tjy", 22, "男", 100)
print(Chinese1.mro())
print(tjy_obj.__dict__) # 使用的父类的__init__创建的对象
print(tjy_obj.star) # 从父类中调取的属性
print(tjy_obj.nation)
tjy_obj.speak_chinese()
多态
多态是一种编程思想
栗子:汽车的多种品牌、橘子的不同品种、人类的各个个体
class Car:
def run(self):
if self != 0:
print("开始跑")
pass
class Benz(Car):
def run(self):
print("加95号汽油", end=" ")
super(Benz, self).run()
pass
class Lx(Car):
def run(self):
print("充满电", end=" ")
super(Lx, self).run()
pass
class Auto(Car):
def run(self):
print("加92号汽油", end=" ")
super(Auto, self).run()
pass
car1 = Benz()
car2 = Lx()
car3 = Auto()
car1.run()
car2.run()
car3.run()
import abc
class Car1(metaclass=abc.ABCMeta):
@abc.abstractmethod # 作用:所有继承了Car1类的子类都必须有run函数,将Car1类作为一个基础类使用,仅仅用来限制子类的规范
# 用来规定子类必须自己实现run函数,不定义就会报错
def run(self):
if self != 0:
print("开始跑")
pass
class Benz1(Car1):
def run(self):
print("加95号汽油", end=" ")
super(Benz1, self).run()
pass
class Lx1(Car1):
def run(self):
print("充满电", end=" ")
super(Lx1, self).run()
pass
class Auto1(Car1):
def run(self):
print("加92号汽油", end=" ")
super(Auto1, self).run()
pass
鸭子类型
意思是:当你走路像鸭子、叫声像鸭子、长得像鸭子的时候,尽管你本质上不是鸭子,但是仍旧可以把你当成鸭子来看
在类当中,这是一种编程思想,就是当你不想用一个父类去限制子类的时候,你就可以把所有的类都做成相似的类,即是把所有的类都做得像“鸭子”。
抽象基类
目的:使父类成为一个规范标准使用,所有继承这个抽象基类的子类必须定义抽象基类规定的方法
注意:抽象基类不能直接实例化,就如下代码,不能直接使用Car1类来实例化对象
类方法
绑定方法可以是绑定给对象使用的,也有可以绑定给类使用的,而绑定给类使用的绑定方法就被称为类方法
前面知道绑定给对象的方法会自动把对象传进去,而绑定给类使用的方法就会自动把类传进去
import settings
class Mysql:
def __init__(self, ip, port):
self.ip = ip
self.port = port
def f1(self):
print(self.ip, self.port)
@staticmethod # 这里的用法即是把f2设置为静态方法,这样不管是类还是对象都可以调用它,但是没有自动传参的功能,也就不属于绑定方法了
def f2():
print("rnm!退钱!")
@classmethod # 设置instance_from_conf方法绑定给类
def instance_from_conf(cls):
print(cls)
test_obj = cls(settings.IP, settings.PORT)
return test_obj
obj = Mysql.instance_from_conf() # 这里调用类会创造一个空对象,然后触发init方法自动把空对象传进去,这就是绑定给对像的方法
# 至于一个方法应该版定给对象还是类,需要看这个方法里用到的是类还是对象
print(obj.__dict__)
print(obj.f1) # <bound method Mysql.f1 of <__main__.Mysql object at 0x000001C811EA2830>> 通过对象调用f1是一个绑定方法,提示我们这是绑定给Mysql实例化的一个对象的
print(Mysql.f1) # <function Mysql.f1 at 0x000001C811EAD7E0> 这里f1通过类调用时不在是绑定方法只是一个普通函数
print(Mysql.f2) # <function Mysql.f2 at 0x000001C811EAD870>
print(obj.f2) # <function Mysql.f2 at 0x000001C811EAD870> 这里不管是类调用还是对象调用,f2都只是一个普通函数,因为我们在定义的时候就已经把它设置为静态了,不会和任何对象绑定
print(Mysql.instance_from_conf) # <bound method Mysql.instance_from_conf of <class '__main__.Mysql'>> 这里的instance_from_conf也是绑定方法,也提示了我们这是绑定给Mysql这个类的
反射机制
Python是一门强类型动态解释型语言
什么是动态语言:即在程序当中定义变量的是时候不需要指定数据的类型,只有在执行代码赋值的时候,才识别它的数据类型
静态语言就是在定义变量的时候就需要指定它的数据类型
而只要是动态语言就一定会有反射机制
反射机制指的是在程序运行过程中,动态获取对象信息,以及动态调用对象方法的功能,就是说程序只要开始运行,就是计算机来识别代码
而计算机识别代码有一个问题,就是计算机如何获取对象的信息,如何调用对象方法,反射机制就是解决这个问题的。
注意:不是所有对象都有__dict__
# dir()可以以列表的形式返回对象所有可以调用的属性,注意,这个列表中的所有属性名都以字符串形式保存
print(dir("nihao"))
# hasattr、getattr、setattr和delattr
# hasattr------判断一个对象是否有某一个属性
# getattr------获取对象的某一个属性
# setattr------给某一个属性赋值
# delattr------删除某一个属性
# 以上四个函数均以字符串的形式操作对象的属性,以下为具体用法:(这里创建的对象使用的第一个类)
hero3 = Hero("夜露", 900, 350, 60)
print(hasattr(hero3, 'name')) # True
print(hasattr(hero3, "hello")) # False
print(getattr(hero3, "name")) # 与hero.name相同,不过这里以字符串形式访问
setattr(hero3, "name", "奇乐")
print(getattr(hero3, "name")) # 这里输出“奇乐”,说明改名成功
print(hero3.__dict__)
delattr(hero3, "name")
print(hero3.__dict__) # 可以看到这里的hero3里面没有name属性了
# 以上四个函数不仅可以传对象,同样也可以传类进去
# 同样的,如果访问的属性属于函数的话,就会获取到函数的内存地址,不过通过对象拿到的是绑定方法的函数内存地址,通过类拿到的是函数的内存地址
# 反射案例:
print(hasattr(18, "hp")) # False 表示18没有hp这个属性
# print(getattr(18, "hp")) # 这里报错表示18没有hp这个属性
if hasattr(18, "hp"):
print(getattr(18, "hp"))
else:
print("没有该属性!")
print(getattr(18, "hp", None)) # 这里第三个参数设置为默认值,即没有该属性时返回其默认值,这里返回None
print(getattr(hero3, "hp", None)) # 这里正常返回hp为900
# 以上为映射的两种解决方法
class Ftp:
def __init__(self):
pass
@staticmethod
def put():
print("正在上传数据.........")
@staticmethod
def get():
print("正在下载数据.........")
# 初始方案:
# def interact(self):
# put = input(">>>")
# if hasattr(self, put):
# getattr(self, put)(self)
# else:
# print("功能不存在")
# 优化方案:
def interact(self):
put = input(">>>")
getattr(self, put, self.warning)
@staticmethod
def warning():
print("功能不存在!")
obj_2 = Ftp()
# obj_2.interact() # 这里需要用到的时候取消注释
内置方法
内置方法:会在满足条件的时候自动执行,如__init__
__init__在创建对象的时候自动执行,__str__在被打印的时候自动执行,如下:
class People:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
print("__str__运行了")
return f"{self.name}:{self.age}"
def __del__(self):
print("__del__运行了")
people = People("sun的迅游日志", 22)
print(people) # 可以看到这里打印了对象的__str__的返回值
# __del__
# 在删除对象的时候先执行它
# 如果没有手动删除,__del__也会在最后执行,因为程序运行完毕释放计算机资源时会将对象删除,但是如果手动删除对象就会先一步触发__del__,如下:
del people # 这行代码可以注释掉看看现象
print("=" * 50)
元类
# 定义一个类
class Sb:
def __init__(self, name, age):
self.name = name
self.age = age
def info(self):
print(f"name:{self.name}, age:{self.age}")
# 基于类创建对象
sb_obj = Sb("sb", 18)
如果说类也是对象的话,那么Sb这个类应该是这样产生的: Sb = 元类()
但是这里的Sb是通过‘class’这个关键字定义的,并没有调用其他类创造这个Sb
这里就要了解到,任何一个关键字,其背后对应的都是一系列的功能,我们平时在调用类的时候确实没有调用一个用于造类的类,然后把结果赋值给Sb
只是这个‘class’的底层帮我们做了这件事,接下来我们一步步分析如何造类,分析完后,我们甚至可以不用‘class’这个关键字也能造类
元类:用来实例化产生类的类
元类—>实例化—>类(Sb)—>实例化—>对象(sb_obj)
# 我们查看sb_obj的类可以通过如下方法:
print(type(sb_obj)) # 输出结果:<class '__main__.Sb'> 可以看到它的类“Sb”
# 那么查看一个类的元类是什么我们也可以通过相同的操作,如下:
print(type(Sb)) # 输出结果:<class 'type'> 那么这里就可以知道Sb的元类就是“type”,这里的type就是内置的元类
print(type(str)) # 输出结果:<class 'type'>
我们使用‘class’创建的所有类和所有内置的类,都是由内置的元类type实例化产生的
现在我们来看看class关键字是怎么一步一步把类实例化出来的
经过以上分析来看,class在实例化过程中一定调用了type这个内置元类
即:Human = type(参数1, 参数2…)
我们现在应该做的事情就是得知在调用元类type的时候给它传了哪些参数
# 1、类名
class_name = 'Human1'
# 2、基类
class_bases = (object,)
# 3、类子代码(类子代码会在定义阶段执行,执行类子代码就会产生类的名称空间)
class_dic = {}
class_body = '''
def __init__(self, name, age):
self.name = name
self.age = age
def info(self):
print(f"我是{self.name},今年{self.age}岁")
'''
exec(class_body, {}, class_dic) # 这里三个参数分别代表:类子代码、全局名称空间和类名称空间
print(class_dic) # {'__init__': <function __init__ at 0x0000011D97972E60>, 'info': <function info at 0x0000011D97972EF0>}
print(Human.__dict__) # {'__module__': '__main__', '__init__': <function Human.__init__ at 0x000001F53B4A2290>, 'info': <function Human.info at 0x000001F53B4A2E60>, '__dict__': <attribute '__dict__' of 'Human' objects>, '__weakref__': <attribute '__weakref__' of 'Human' objects>, '__doc__': None}
# 4、调用元类
Human1 = type(class_name, class_bases, class_dic)
print(Human1) # <class '__main__.Human1'>
wl_human = Human1('wl', 22)
wl_human.info()
以上四步我们就实现了脱离class关键字创建了一个类,那这样有什么用呢?以上方法没有比class关键字更方便,也不如class关键字更直观
所以我们在平时创建类的时候更多还是使用class关键字,不过我们了解了类是由元类产生的,那么我们能不能自己创建一个元类,更加个性化、定制化
比如在定义元类的时候我们可以设置检测类名中有没有下划线,如果没有下划线就报错
raise NameError("类名不能有下划线") # 运行此行代码报错内容为括号内的字符串内容
# 自定义元类
class Mytype(type):
def __init__(cls, name, bases, dic):
# if "_" not in name:
# raise NameError("类名不能没有下划线!")
if not dic.get("__doc__"):
raise NameError("没有关于类的文档注释!")
super().__init__(name, bases, dic)
print(bases)
print(cls.__bases__)
def __new__(cls, *args, **kwargs):
print(cls)
print(args)
print(kwargs)
return super().__new__(cls, *args, **kwargs)
pass
这里就相当于Human2 = Mytype(class_name, class_bases, class_dic)
call
1、调用Mytype的__new__方法产生一个空对象Human
2、调用Mytype的__init__方法,初始话对象Human2
3、返回初始化对象Human2
class Human2(metaclass=Mytype): # 这里metaclass后跟的参数就是元类名称,继承的父类正常写就行,只需要metaclass在最后一个就好
"""
测试元类
"""
def __init__(self, name, age):
self.name = name
self.age = age
def info(self):
print(f"我是{self.name},今年{self.age}岁")
print(Human2)
wys_human = Human2('wys', 29)
wys_human.info()
__call__方法
class Learning:
def __init__(self, name, age):
self.name = name
self.age = age
def __call__(self, *args, **kwargs): # __call__方法会在对象加括号调用时触发
print("hello!")
print(args) # 可以看见没有指定名称的参数都被放进了args当中
print(kwargs) # 而被指定了名称的参数都放进了kwargs当中
wl_obj = Learning("wl", 26)
wl_obj("name", 1, 2, 3, age=12, number=13) # 当Learning中没有__call__方法时报错,有__call__方法时运行__call__方法
如果想要把一个对象做成一个加括号可以调用的对象的时候,就在对象的类里面加一个call方法
对象() ----> 类当中的__call__
类() ----> 自定义元类当中的__call__
自定义元类() ----> 内置元类当中的__call__
class Mytype1(type):
def __call__(cls, *args, **kwargs):
Human_obj = cls.__new__(cls)
cls.__init__(Human_obj, *args, **kwargs)
return Human_obj
class Human3(metaclass=Mytype1): # 这里metaclass后跟的参数就是元类名称,继承的父类正常写就行,只需要metaclass在最后一个就好
"""
测试元类
"""
def __init__(self, name, age):
self.name = name
self.age = age
def info(self):
print(f"我是{self.name},今年{self.age}岁")
def __new__(cls, *args, **kwargs):
obj__ = super().__new__(cls)
return obj__
zdx_obj = Human3("zdx", 99)
print(zdx_obj)
print(zdx_obj.__dict__)
zdx_obj.info()
属性查找
- 对象中没有的属性就去创建对象的类里面找,创建对象的类里面没有就去它的父类里面找,父类里面没有的话就去object里面找,再没有就报错
- 学习了元类之后我们要注意,父类和元类并不是一种概念,父类是被继承的类,继承了父类的函数和属性;而元类是创建类的类,只在创建的时候用到,创建完后就没有关系了
- 现在我们来感受一下有父类和元类的时候属性查找是怎么样的
class Mytype_1(type):
age = 18
def __call__(cls, *args, **kwargs):
obj_ = cls.__new__(cls)
cls.__init__(obj_, *args, **kwargs)
return obj_
class Animal_1(object):
age = 17
pass
class Human_1(Animal_1):
age = 16
pass
class Chinese_1(Human_1, metaclass=Mytype_1):
age = 15
pass
class ScPerson(Chinese_1):
age = 14
pass
sun_obj = ScPerson()
print(sun_obj.age) # 这里只会根据父类关系一步一步找,不会找到元类的属性
print(ScPerson.age) # 这里一样会根据父类关系一步一步找,但是如果所有父类没有该属性的话,才会到元类中找
# 注意只有通过类才能追溯到元类,非类对象会被元类屏蔽
print(type(Chinese_1)) # <class '__main__.Mytype_1'>
print(type(ScPerson)) # <class '__main__.Mytype_1'>
print(type(Human_1)) # <class 'type'>
- 这里注意,我们在创建类的时候只指定了Chinese_1使用了自定义的元类创建,而它的子类ScPerson我们并没有指定
- 但是在查看ScPerson的类型的时候显示他的元类是我们自定义的元类类型,这说明只要父类使用了自定义元类,那么它所有的子类都默认使用自定义的元类
- 而父类的父类不受影响
单例模式
单例模式实现方法:
1、模块
from settings import obj_test
obj_test.info()
模块方法无法更改对象,因为对象在导入前就已经创建好了
2、类装饰器
def singleton_mode(cls):
res = None
def wrapper(*args, **kwargs):
nonlocal res
if not res:
res = cls(*args, **kwargs)
return res
return wrapper
@singleton_mode
class Human_2:
def __init__(self, name, age):
self.name = name
self.age = age
def info(self):
print(f"{self.name}:{self.age}")
wln_obj = Human_2("瓦伦尼古拉斯", 18)
wln2_obj = Human_2("伞兵一号", 26)
wln_obj.info()
wln2_obj.info()
使用类装饰器在逻辑上只触发第一次创建对象的过程,后面再次创建时会发现已经有一个对象了就不会再触发
3、类绑定方法
class Human_3:
obj = None
def __init__(self, name, age):
self.name = name
self.age = age
def info(self):
print(f"{self.name}:{self.age}")
@classmethod
def get_obj(cls, *args, **kwargs):
if not cls.obj:
cls.obj = cls(*args, **kwargs)
return cls.obj
ly_obj = Human_3.get_obj("ly", 16)
ly2_obj = Human_3.get_obj("lbw", 22)
ly_obj.info()
ly2_obj.info()
# 逻辑上与类装饰器相同
4、__new__方法
class Human_4:
obj = None
def __init__(self, name, age):
self.name = name
self.age = age
def info(self):
print(f"{self.name}:{self.age}")
def __new__(cls, *args, **kwargs):
if not cls.obj:
cls.obj = super().__new__(cls)
return cls.obj
jys_obj = Human_4("jyc", 26)
jys2_obj = Human_4("菜就多练", 24)
jys_obj.info()
jys2_obj.info()
jys_obj.name = "jys"
jys2_obj.info() # jys:24
这里的单例模式为所有的用Human_4创建的对象都使用一个内存地址,每一次新建对象就会把原来对像的数据刷新,不管改哪一个对象都会对所有的对象产生影响
5、元类
class Mytype_2(type):
obj = None
def __call__(cls, *args, **kwargs):
if not cls.obj:
cls.obj = cls.__new__(cls)
cls.__init__(cls.obj, *args, **kwargs)
return cls.obj
class Human_5(metaclass=Mytype_2):
obj = None
def __init__(self, name, age):
self.name = name
self.age = age
def info(self):
print(f"{self.name}:{self.age}")
def __new__(cls, *args, **kwargs):
if not cls.obj:
cls.obj = super().__new__(cls)
return cls.obj
pbm_obj = Human_5("pbm", 26)
pbm2_obj = Human_5("ly", 24)
pbm_obj.info()
pbm2_obj.info()