OOP编程思想概述
面向对象编程是在面向过程编程的基础上发展来的,它比面向过程编程具有更强的灵活性和扩展性。面向对象编程是程序员发展的分水岭,很多初学者会因无法理解面向对象而放弃学习编程。
面向对象编程(Object-oriented Programming,简称 OOP),是一种封装代码的方法。
其实,在前面章节的学习中,我们已经接触了封装,比如说:
将数据放进列表和字典中中,这就是一种简单的封装,是数据层面的封装;
把常用的代码块打包成一个函数,这也是一种封装,是语句层面的封装。
------------------->>>>>
代码封装,其实就是隐藏实现功能的具体代码,仅留给用户使用的接口,就好像使用计算机,用户只需要使用键盘、鼠标就可以实现一些功能,而根本不需要知道其内部是如何工作的。
接下来所讲的面向对象编程,也是一种封装的思想,不过显然比以上两种封装更先进,它可以更好地模拟真实世界里的事物,并把描述特征的数据和代码块(函数)封装到一起。
类与实例对象
类和对象的概念
类是人们抽象出来的一个概念,所有拥有相同属性和功能的事物称为一个类;
而拥有相同属性和功能的具体事物则成为这个类的实例对象。
比如:
电脑可以看成是一个类
电脑有不同的品牌、颜色、配置参数等是它的属性
使用电脑进行软件安装、办公、游戏等等是它的功能
再比如:
教师也是一个类,属于人类,职业是教师,属于人类系统下的子类;就是说类具有包含关系
----------->>>>
所以,简单来说:
类是人们抽象出来的某个范围,对象是这个范围下真实的、看的见摸得着的实例
声明类和实例化对象
面向对象最重要的概念就是类(Class)和实例(Instance):
● 必须牢记类是抽象的模板,比如Person类
● 而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。
----------->>>>
类与模块、函数的关系:
一个模块(一个py文件)包裹函数和类、
类里面封装着函数(python中函数放在类里面,就是一个方法)
先聊聊为什么代码中要用到类,看个栗子
prop = [] #道具
vitality = 100 #血值
def attacked():
global vitality
print("被攻击")
vitality -= 1 #被攻击血值减1
def buy_prop():
print("购买道具")
上面的代码的问题:
虽然也可以实现在方法里调用数据(prop、vitality这两个变量),但是数据与方法是脱离的
数据和方法都在全局的位置,完全是隔离开的,是一种不理想的方式
所以:类解决的问题,就是把方法与之相关的数据组织一起
----------->>>>
具体如下代码:
定义一个英雄的类
把"道具"和"血值"这样的变量数据、以及“攻击”和“购买道具”的方法都放在类里面
当然这还不是一份合格的类的代码,这里只是通过代码演示说明类都干了些啥
class Hero():
prop = [] # 道具
vitality = 100 # 血值
def attacked():
global vitality
print("被攻击")
vitality -= 1 # 被攻击血值减1
def buy_prop():
print("购买道具")
开始正经的写一个类:
● class顶格写:声明一个类;
类名自己命名,类名首字母尽量大写,小写虽不会报错,但是不符合规范;
● 变量名 = 类名():实例化对象
● .类属性:实例化对象调用属性和方法
#声明一个类
class Person():
#类属性:这个类全部拥有的共同属性
isMotion = True
legs = 2
isLanguage = True
#def是这个类下每一个对象都能拥有的实例化方法:
def eat(self):
print("eating...")
def sleep(self):
print("sleep...")
def run(self):
print("run....")
#实例化对象:
p1 = Person()
#实例化对象:调用属性和方法,通过句点符
print(p1.isLanguage)
print(p1.legs)
p1.eat()
p1.run()
实例化对象敲重点:
● 一个类可以被多次实例化对象,只要每次的实例化对象命名不同即可,所以不同的实例化对象并不等于彼此(所以下面代码中,"print(p1 == p2)"的结果是false)
● 当实例化对象通过.调用属性和方法时,会先从类里面找有没有其属性和方法,如果没有会往上一级找父类(下面再具体讲父类和子类),如果还是没有则会报错,可以理解为如果.没有出来就是没找到(比如下面代码中,如果直接"p2.name()"就会直接报错)
● 示例话对象时,可以直接新增变量属性并赋值(如下面代码中p1.name = "小明" )
#声明一个类
#类名的首字母尽量大写,小写虽然不会报错,但是不符合规范
class Person():
#类属性:这个类全部拥有的共同属性
isMotion = True
legs = 2
isLanguage = True
#def是这个类下每一个对象都能拥有的实例化方法:
def eat(self):
print("eating...")
def sleep(self):
print("sleep...")
def run(self):
print("run....")
#实例化对象:
p1 = Person()
#实例化对象:调用属性和方法,通过句点符
print(p1.isLanguage)
print(p1.legs)
p1.eat()
p1.run()
p1.name = "小明" #对p1创建name变量并赋值,会存在p1这个实例化对象中
print(p1.name) #所以可以正常打印
print("*" * 50)
#再实例化一个对象
p2 = Person()
print(p2.isLanguage)
print(p2.legs)
p2.eat()
p2.run()
print(p1 == p2)
对象的属性初始化
在创建类时,我们可以手动添加一个 `__init__()` 方法,该方法是一个特殊的类实例方法,称为构造方法(或构造函数)。
`__init__() `方法可以包含多个参数,但必须包含一个名为 self 的参数,且必须作为第一个参数。
在 `__init__() `构造方法中,除了 self 参数外,还可以自定义一些参数,参数之间使用逗号“,”进行分割,从而完成初始化的工作。
----------->>>>
例如:
上面代码中定义的Person类,需要给每个实例对象都添加实例属性(冗余版)
#声明一个类
#类名的首字母尽量大写,小写虽然不会报错,但是不符合规范
class Person():
#类属性:这个类全部拥有的共同属性
isMotion = True
legs = 2
isLanguage = True
#def是这个类下每一个对象都能拥有的实例化方法:
def eat(self):
print("eating...")
def sleep(self):
print("sleep...")
def run(self):
print("run....")
#实例化对象:
p1 = Person()
#实例化对象:调用属性和方法,通过句点符
p1.name = "小明"
p1.age = 23
p1.weight = 60
print(p1.name,p1.age,p1.weight)
#再实例化一个对象
p2 = Person()
p2.name = "小红"
p2.age = 22
p2.weight = 45
print(p2.name,p2.age,p2.weight)
像上面这样,每次对实例对象先进行属性赋值,再打印,比较的重复、麻烦
这时候就可以加上魔法`__init__()` 方法:在进行实例化对象时就会触发执行,为什么这么说呢?
先来一个栗子:
我们先不进行实例对象的具体属性添加,先做一个打印
声明一个类
#类名的首字母尽量大写,小写虽然不会报错,但是不符合规范
class Person():
#类属性:这个类全部拥有的共同属性
isMotion = True
legs = 2
isLanguage = True
#初始化方法:
def __init__(self):
print("self:",id(self))
#def是这个类下每一个对象都能拥有的实例化方法:
def eat(self):
print("eating...")
def sleep(self):
print("sleep...")
def run(self):
print("run....")
#实例化对象:
p1 = Person()
print("p1:",id(p1))
p2 = Person()
print("p2",id(p2))
可以看出:
p1和p2调用实例对象方法打印,一共打印了4次:
其中p1的两次地址是一样的,p2的两次也是一样
地址即调用实例对象方法时触发,python内部为我们开辟的空间,可以用来接收变量及存储
所以当p1先调用,拿到空间地址,再执行p1打印,就是将拿到的变量付给p1
'def __init__(self)'是一个函数,是函数就可以接受参数,它已经由解释器默认分配一个self参数,
self就是调用时用来开辟空间的,但是我们同时可以在self后面,再加上我们需要的变量名
同时在初始化函数中先进行形参的变量赋值,通过初始化方法调用时执行传入实参即可
简化版:
#声明一个类
#类名的首字母尽量大写,小写虽然不会报错,但是不符合规范
class Person():
#初始化方法:实例对象的变量、示例对象的属性
isMotion = True
legs = 2
isLanguage = True
#初始化方法:实例对象的变量、示例对象的属性
def __init__(self,name,age):
# print("self:",id(self))
self.name = name
self.age = age
#def是这个类下每一个对象都能拥有的实例化方法:
def eat(self):
print("eating...")
def sleep(self):
print("sleep...")
def run(self):
print("run....")
#实例化对象:
p1 = Person("小明",23)
print(p1.name,p1.age)
p2 = Person("小红",20)
print(p2.name,p2.age)
p3 = Person("小蓝",24)
print(p3.name,p3.age)
为了避免犯模糊,这里强调一下区别:
● 刚开始定义类的公共属性和变量:叫类属性/类变量,属于这个类下面的所有实例对象都有的属性,通过.的方式进行调用
● 后面定义的'def __init__(self)'的属性和变量:叫实例化对象属性/实例化对象变量,需要通过实例化对象调用时先传入实参,才能够输出打印
● 也可以在`__init__()`括号里可以加上我们需要的变量名:叫实例变量
实例方法
在上面一开始“声明类和实例化对象”的代码中略有展示,这里再专门进行说明与理解:
------------->>>>
实例方法是类中定义的一种特殊类型的方法:
函数里带着self放在类里面,就是实例方法
通过实例对象.进行调用
只有实例化对象之后才可以使用的方法,该方法的第一个形参接收的一定是对象本身。
代码示例如下:
class Person():
#类属性、类变量:这个类全部拥有的共同属性
isMotion = True
legs = 2
isLanguage = True
#初始化方法:实例对象的变量、示例对象的属性
def __init__(self,name,age):
# print("self:",id(self))
self.name = name
self.age = age
#def是这个类下每一个对象都能拥有的实例化方法:
def eat(self):
print("eating...")
def sleep(self):
print("sleep...")
def run(self):
#实例方法
print(f"{self.name}run....")
#实例化对象:
p1 = Person("小明",23)
print(p1.name,p1.age)
p1.run()
print("*" * 50)
p2 = Person("小红",20)
print(p2.name,p2.age)
p2.run()
实例化方法存储在类空间里
实例化方法在实例化对象调用时,可以重新对实例化方法进行变量赋值,赋值的变量值存储在实例化空间中,因此重新赋值后拿到是最新的变量值
但重新赋值后直接调用会报错,因为类空间里找不到
class Person():
#类属性、类变量:这个类全部拥有的共同属性
isMotion = True
legs = 2
isLanguage = True
#初始化方法:实例对象的变量、示例对象的属性
def __init__(self,name,age):
# print("self:",id(self))
self.name = name
self.age = age
#def是这个类下每一个对象都能拥有的实例化方法:
def eat(self):
print("eating...")
def sleep(self):
print("sleep...")
def run(self):
#实例方法
print(f"{self.name}run....")
#实例化对象:
p2 = Person("小红",20)
print(p2.name,p2.age)
p2.run = 100 #重新对实例方法进行赋值
print(p2.run) #打印后就是新的值
p2.run() #会报错
类对象和类属性
在python语言中,一切皆对象!
我们之前学习过的字符串,列表,字典等等数据都是一个个的类,我们用的所有数据都是一个个具体的实例对象。
区别就是,那些类是在解释器级别注册好的,而现在我们学习的是自定义类,但语法使用都是相同的。
所以,我们自定义的类实例对象也可以和其他数据对象一样可以进行传参、赋值等操作。
------------------>>>>
在类中:
class声明一个类:叫类对象
class类最后进行实例化调用及传参:叫实例对象
所以关系是:
声明一个类时开辟了一个类的空间,进行实例化对象时又在类里面开辟了一个实例化的空间,也就是类里面包裹了实例对象
#类对象:
class Person():
#类属性、类变量:这个类全部拥有的共同属性
isMotion = True
legs = 2
isLanguage = True
#初始化方法:实例对象的变量、示例对象的属性
def __init__(self,name,age):
# print("self:",id(self))
self.name = name
self.age = age
#def是这个类下每一个对象都能拥有的实例化方法:
def eat(self):
print("eating...")
def sleep(self):
print("sleep...")
def run(self):
#实例方法
print(f"{self.name}run....")
#实例化对象:
print(Person)
p1 = Person("小红",20)
print(p1)
打印的结果就是两个空间,p1属于__main__.Person下的object
其次,类对象、实例化对象的区分:
在实例化对象时,对类属性/类变量进行重新赋值,只会将新的值存在实例化对象的空间里,不会改变类的属性/类变量值。
--------------------->>>>>
如下:
在实例化对象时,'st1.class_num = 600 '重新对class_num进行赋值,只有st1的打印结果是最新的变量值,打印'st2.class_num'的值仍然是一开始定义的类属性的500
#类声明:
class Student(object):
class_name = '爬虫学习'
class_num = 500
def __init__(self,name):
self.name = name
def listen(self):
print(f"{self.name}听课")
#实例化对象:
st1 = Student("张三")
st2 = Student("李四")
st1.class_num = 600 #对类属性class_num重新赋值
print(st1.class_num)
print(st2.class_num)
那么问题来了:有时候就是需要临时修改整个类属性的值,怎么办?
既然上面了解到,类是一个对象、示例化对象也是一个对象,那么修改类对象下的属性值,
只需要直接进行类属性重新赋值即可哦~
#声明一个类:
class Student(object):
class_name = '爬虫学习'
class_num = 500
def __init__(self,name):
self.name = name
def listen(self):
print(f"{self.name}听课")
Student.class_num = 600 #针对类对象重新变量赋值
#实例化对象:
st1 = Student("张三")
st2 = Student("李四")
print(st1.class_num)
print(st2.class_num)
类对象属性变量重新赋值后,这下不论st1、还是st2打印都是最新的600:
公有属性、私有属性
● 实例属性: 实例属性是指属于类实例的属性,每个类实例都有自己的实例属性。
实例属性通常在类的
__init__
方法中进行初始化。在类的方法中可以通过
self
来访问和修改实例属性;案例如上--------------->>>>
● 公有属性: 公有属性是指可以被类的实例和类本身访问的属性。
在Python中,没有特殊的关键字来定义公有属性,一般情况下,类的属性都是公有属性。
代码示例如下:
class Person:
nationality = "Chinese" # 公有属性
person1 = Person()
print(person1.nationality) # 访问公有属性
print(Person.nationality) # 也可以通过类本身来访问公有属性
● 私有属性: 私有属性是指只能在类的内部访问的属性,外部无法直接访问。
在Python中,可以通过在属性名前面加上双下划线
__
来定义私有属性。代码示例如下:
class Person():
def __init__(self, name, age, sex, job):
#公有属性:
self.name = name
self.age = age
#私有属性:
self.__sex = sex
self.__job = job
xm = Person("小明", 20, "man", "QA")
xh = Person("小红", 21, "man", "PG")
print(xm.name)
print(xh.name)
类直接调用私有属性,会报错:
封装
封装是面向对象编程中的一个重要概念,它指的是将数据和对数据的操作(方法)封装在一个类中,并对外部隐藏对象的内部细节。封装可以通过访问控制来实现,例如使用公有属性、私有属性和公有方法、私有方法等。
----------->>>>
封装的主要目的:是保护数据,防止外部直接访问和修改对象的内部状态,从而确保数据的安全性和一致性。封装还可以隐藏对象的内部实现细节,使得对象的使用者不需要关心对象内部是如何实现的,只需通过对象提供的公有接口来访问对象的功能。
----------->>>>
在Python中,封装可以通过以下方式实现:
- 使用私有属性:在属性名前面加上双下划线
__
,可以将属性定义为私有属性,外部无法直接访问。- 使用公有方法和私有方法:将对象的操作封装在方法中,通过公有方法来访问对象的功能,而将一些内部实现细节封装在私有方法中,外部无法直接调用。
上面私有属性的代码例子中,类无法直接访问它内部的私有属性
但是可以进行封装,封装后就可以访问私有属性:
class Person():
def __init__(self, name, age, sex, job):
#公有属性:
self.name = name
self.age = age
#私有属性:
self.__sex = sex
self.__job = job
def get_job(self): #获取私有属性
return self.__job
def set_job(self,NewJob): #修改私有job属性
self.__job = NewJob
xm = Person("小明", 20, "man", "QA")
xh = Person("小红", 21, "man", "PG")
xm.set_job("打工仔")
xh.set_job("老板")
print(xm.get_job()) #通过封装的方法访问私有属性
print(xh.get_job())
静态方法和类方法
静态方法
上面学习到,带着self的就是实例方法,但有时候一些实例方法,我们只想让它完成一个简单的操作,用不到实例变量,它不需要形参,也是完全有可能的。
那么,直接把self去掉,会怎样呢?
上面的栗子中,'st1. foo()'进行调用时会传1个实参,但是由于实例方法foo中没有形参
所以直接报错了~~
-------------------->>>
所以,当我们的实例方法不需要传参、也不需要用到类信息时,就需要用到装饰器
定义:使用装饰器`@staticmethod`,就是告诉解释器,当前实例方法只是一个普通函数
参数随意,没有`self`和`class`参数,但是方法体中不能使用类或实例的任何属性和方法;
调用:类对象或实例对象都可以调用。
class Student:
#类属性
cls_number = 50
def __init__(self,name):
self.name = name
@staticmethod #加上装饰器
def show_class_num():
print(f"班级人数",Student.cls_number)
@staticmethod #加上装饰器
def add_calss_num():
Student.cls_number += 1
#调用是换成类对象
Student.add_calss_num()
Student.show_class_num()
类方法
上面静态方法中,存在一个问题:最后是类对象进行调用的,如果哪天修改了类对象的变量名,则实例方法、调用时都需要同步修改,就比较麻烦。
所以有了更灵活的的类方法。
定义:使用装饰器`@classmethod`。
第一个参数必须是当前类对象,该参数名一般约定为`cls`,通过它来传递类的属性和方法(不能传实例的属性和方法);
调用:主要是由类对象调用。
class newStudent:
#类属性
cls_number = 50
def __init__(self,name):
self.name = name
@classmethod #加上类方法
def show_class_num(cls):
print(f"班级人数",cls.cls_number)
@classmethod #加上类方法
def add_class_num(cls):
cls.cls_number += 1
#调用是换成类对象
s = newStudent.add_class_num()
newStudent.show_class_num()
面向对象之继承
面向对象的编程的主要好处之一是代码的重用,实现这种重用的方法之一是通过继承机制。
● 通过继承创建的新类称为:子类或派生类
● 被继承的类称为:基类、父类或超类
继承的基本使用
继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。
通过使用继承我们能够非常方便地复用以前的代码,能够大大的提高开发的效率。
------------>>>>
诚然,继承定义了类如何相互关联,共享特性。对于若干个相同或者相识的类,我们可以抽象出他们共有的行为或者属相并将其定义成一个父类或者超类,然后用这些类继承该父类,他们不仅可以拥有父类的属性、方法还可以定义自己独有的属性或者方法。
------------>>>>
同时在使用继承时需要记住三句话:
① 子类拥有父类非私有化的属性和方法。
② 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
③ 子类可以用自己的方式实现父类的方法。(下面会介绍)。
#无继承关系的两个类:
class Dog:
def eat(self):
print("eating....")
def sleep(self):
print("sleep....")
def swimming(self):
print("swimming...")
class Cat:
def eat(self):
print("eating....")
def sleep(self):
print("sleep....")
def climb_tree(self):
print("climb_tree...")
实际上继承者是被继承者的特殊化,它除了拥有被继承者的特性外,还拥有自己独有得特性。
例如上面的代码中:猫有抓老鼠、爬树等其他动物没有的特性。同时在继承关系中,继承者完全可以替换被继承者,反之则不可以,例如我们可以说猫是动物,但不能说动物是猫就是这个道理,其实对于这个我们将其称之为“向上转型”:
#继承方式:
#新增一个Animal的父类:
class Animal(object):
#把eat、sleep这两个相同的方法放在父类中:
def eat(self):
print("eating....")
def sleep(self):
print("sleep....")
#子类继承:类名的括号里加上父类的类名
class Dog(Animal):
def swimming(self):
print("swimming...")
#子类继承:类名的括号里加上父类的类名
class Cat(Animal):
def climb_tree(self):
print("climb_tree...")
#实例化对象:
cls1 = Dog()
cls1.eat()
#dog子类中也有swimming方法,继承了父类,相当于是cls1自己再调用swimming
cls1.swimming()
cls2 = Cat()
cls2.sleep()
如果父类、子类的方法名相同,子类调用该方法时,调用的子类的方法:
class Animal():
type_name = "动物类"
__num = 100
def eat(self):
print("动物能吃")
def sleep(self):
print("动物的睡觉方式")
class Cat(Animal):
def shout(self):
print("喵喵~")
def sleep(self):
print("猫咪的睡觉方式")
class Tom(Cat):
def speak(self):
print("Tom会说话")
def sleep(self):
print("Tom的睡觉方式")
tom = Tom()
tom.eat()
tom.sleep()
可以看到,在.方法时,后面就会显示方法来自哪个类名
重写父类方法和调用父类方法
重写父类方法
为什么要重写父类?
有时候调用别人写好的类,我们引用时只需要修改其中某个实例化方法,又不想完全自己重写一个新的类时(重写可能几千行代码):就可以引用别人的类,新写一个自己的子类并重写某个方法
#Animal的父类:
class Animal(object):
#把eat、sleep这两个相同的方法放在父类中:
def eat(self):
print("eating....")
def sleep(self):
print("sleep....")
def foo(self):
print("foo")
#创建新的子类组件:
class MyAnimal(Animal):
#重写foo方法
def foo(self):
print("new foo")
ma = MyAnimal()
ma.sleep()
ma.eat()
ma.foo()
由于子类中重写了foo方法,所以实例化调用时,优先找的是子类中的foo方法:
调用父类方法
上面重新父类方法是,直接继承父类,并针对其中一个方法直接进行重写。
实际编程中,完全又可能说,我们需要对某个实例方法进行扩展,相当于说在调用父类方法的前提下,并重写某个实例方法。
● 方式1 :父类对象调用 父类对象.方法(self,其他参数)
● 方式2 :super关键字 super(子类对象,self).方法(参数)or super().方法(参数)
ps:如果方法改名了,维护成本较高,一般更推荐第二种
#Animal的父类:
class Animal(object):
#把eat、sleep这两个相同的方法放在父类中:
def eat(self):
print("eating....")
def sleep(self):
print("sleep....")
def foo(self):
print("foo")
#创建新的子类组件:
class MyAnimal(Animal):
# 重写foo方法
def foo(self):
#调用父类方法:
# Animal().foo() #父类调用方式1
super().foo() #父类调用方式2
print("foo end")
#实例化对象调用:
ma = MyAnimal()
ma.foo()
多重继承
如果在继承元组中列了一个以上的类,那么它就被称作"多重继承" 。
派生类的声明,与他们的父类类似,继承的基类列表跟在类名之后
-------------->>>>
如下所示:
一个Animal的父类,下面有多种动物的子类,由于Dog类不具备会飞的属性:
所以Fly子类不能放直接放在父类下面作为公共的实例化方法
所以Eagle、Bat的子类需要同时继承Animal、Fly这两个父类,它才同时具备这个这个类里所有的方法:
#多继承:
#声明一个Animal父类
class Animal:
def eat(self):
print("eating....")
def sleep(self):
print("sleep....")
#定义一个Fly的子类
class Fly(object):
def fiy(self):
print("fly...")
#定义一个dgo的子类
class Dog(Animal):
pass
#一个老鹰的子类
class Eagle(Animal,Fly):
pass
#一个蝙蝠的子类,括号里同时继承Animal,Fly者两个父类
class Bat(Animal,Fly):
pass
#实例化对象:
b = Bat()
b.fiy() #调用时会先去Animal的父类找,再去Fly的父类找
多重继承中的“就近原则”
如下示例:
定义三个类,第三个类继承前两个类,前两个类中都有“eat”这个方法
那么第三个子类实例化之后,调用“eat”方法时,调的Monkey里的方法,原因是第三个类继承的时候,括号里的Monkey的父类名称放在前面的。
class Monkey():
def eat(self):
print("猴子喜欢吃香蕉")
class God():
def fly(self):
print("神仙会飞")
def eat(self):
print("神仙喜欢吃蟠桃")
class SunWnKong(Monkey,God): #就近原则,取决于两个父类方的前后顺序
def qu_jing(self):
print("孙悟空要去取西经")
swk = SunWnKong()
swk.eat()
`dir()`方法和`__dict__`属性
`dir(obj)`可以获得对象的所有属性(包含方法)列表, 而`obj.__dict__`对象的自定义属性字典
注意事项:
1. `dir(obj)`获取的属性列表中,**方法也认为属性的一种**。带双下划线的都是内置方法属性
2. `obj.__dict__`只能获取自己自定义的属性,系统内置属性无法获取。返回是`dict`当我们需要要看有哪些方法可以使用,就可以使用上面的方式
例如:
class Student:
def __init__(self, name, score):
self.name = name
self.score = score
def test(self):
pass
hailey = Student("hailey", 100)
print("获取所有的属性列表")
print(dir(hailey)) #获取所有的内置属性、自定义变量属性名
print("获取自定义属性字段")
print(hailey.__dict__)
异常机制
首先我们要理解什么叫做"异常”?
> - 在程序运行过程中,总会遇到各种各样的问题和错误。
> - 有些错误是我们编写代码时自己造成的:比如语法错误、调用错误,甚至逻辑错误。
> - 还有一些错误,则是不可预料的错误,但是完全有可能发生的:比如文件不存在、磁盘空间不足、网络堵塞、系统错误等等。这些导致程序在运行过程中出现异常中断和退出的错误,我们统称为异常。大多数的异常都不会被程序处理,而是以错误信息的形式展现出来。
------------>>>>
异常的分类:
- 异常有很多种类型,Python内置了几十种常见的异常,无需特别导入,直接就可使用。
- 需要注意的是,所有的异常都是异常类,首字母是大写的!------------>>>>
异常的危害:
- 如果程序中一旦出现了异常的语句代码,则该异常就会立即中断程序的运行!
- 因此:为了保证程序的正常运行,提高程序健壮性和可用性。我们应当尽量考虑全面,将可能出现的异常进行处理,而不是留在那里,任由其发生。
异常基本语法
捕捉异常(tyr + except )
try:
#代码块A:可能存在报错的逻辑代码
except Exception:
#代码块B:出现异常之后,执行except模块的代码,用来防止报错终止程序
一个简单的代码示例:
例如需要先输入一个字符串 -> 输出字符串的第8个元素
程序会出现两种情况:
a、字符串输入≥8位,正常输出第8个元素
b、字符串输入≤8位,程序会报错
为了防止在输出的时候报错,导致后面的代码无法正常运行,所以加上异常:
import logging
logging.basicConfig(level=logging.INFO)
str1 = input("请输入字符串:")
try:
print("plan a:",str1[7])
except Exception:
print("plan b:输入的字符串长度不够8位数")
print("继续执行下面的代码")
当输入不足8位,抛出异常,并且继续执行后面的代码
但是:当我把plan a、plan b的代码写成一致,并且输入不足8位时,还是会报错
所以:添加异常时,通常plan a写可能存在问题的情况,plan b写一般不会存在问题的代码(防止代码报错)
当开启日志时:try模块中报错之前的代码一定会正常执行,产生报错处的代码,则不会执行
import logging
logging.basicConfig(level=logging.INFO)
str1 = input("请输入字符串:")
try:
logging.info("异常前面的代码,是否会执行")
print("plan a:",str1[7])
logging.info("异常之后的代码,是否会执行")
except Exception:
print("plan b:输入的长度不够8位")
print("继续执行下面的代码")
finally
#finally的基本语法:
finally:
try:
计划A
except Exception:
计划B
finally:
计划C(无论是否异常发生,计划C一定会执行;仅限于本身能够执行成功的代码)
try:
n = 1 / 0 #0不能用作被除数
print("plan a")
except:
print("plan b")
finally:
print("plan c")
因为plan a报错了,所以执行b和c
但是:如果本来执行就会报错的代码,写在finally,会执行,但是依旧会报错
assert
assert就是断言
断言就是:预期结果和实际结果进行对比,输出:Pass、Falie
一个简单的示例如下:
s = "hello world"
assert s[0] == "o" #判断第一个字符串为"o"
print("该用例通过 -- Pass")
预期结果与实际结果不符,导致执行后报错:并且pychram没有给出报错信息
这时,我们可以自己手动加个报错信息:
s = "hello world"
assert s[0] == "o" ,f"字符串的第一个元素--预期结果是'o',但实际获取到的结果是:{s[0]}"
print("该用例通过 -- Pass")
再次执行报错后,就能看到报错信息了:
raise语句
很多时候,我们需要主动抛出一个异常。
Python内置了一个关键字`raise`,可以主动触发异常。
`raise`可以抛出自定义异常,我们已将在前面看到了python内置的一些常见的异常类型。
大多数情况下,内置异常已经够用了。但是有时候你还是需要自定义一些异常:自定义异常应该继承`Exception`类,直接继承或者间接继承都可以,例如:
def divide(x, y):
if y == 0:
raise ValueError("除数不能为0")
return x / y
try:
result = divide(10, 0)
print(result)
except ValueError as e:
print(f"发生异常: {e}"