1 类
在前一篇文章的基础之上,我们继续深入学习python相关的知识点。
类同样是编程语言中非常常见的一个概念,如果你想要对类有更加宏观的认识,可以参考本站的另一篇文章:结构体、类、接口。
为了照顾新手,我们还是来简单说一下。
从前面的代码中我们可以发现,写代码无非就是写两个内容:变量、函数。
- 变量:用来存放数据
- 函数:一系列代码语句的集合,一般用来封装一系列常用的代码语句。
显然,这两部分是分开的,很多时候这种分离是合理的,比如前面的提到的len
函数,它就可以求许多类型的变量长度。
但有时候分开却并不太好,比如一个数字,我想要写一个函数对这个数字做一个非常复杂的运算,比如:
def js(n: int):
# 假设这里对数字做了大量复杂运算
return n
那么其实这个函数是只能给数字用的,其它类型的变量你就算想用也用不了!
这时候问题就来了:
- 首先就是这个函数名必须得放在全局,让其它想使用的地方随时都能使用不是?就像
len
函数一样。
然而这只是一个函数,实际开发中怎么都得有几十上百个函数,先不说这些函数会不会有重名的风险,只是让你记函数名都很困难吧!
- 其次,这只是对于一个
int
类型变量,可能会很简单,但实际开发过程中,一般都是一系列基本数据类型组成的一个新类型,填写参数就会很麻烦不是吗?
比如一个学生信息,就得有学号、姓名、性别等等很多变量,如果你想要处理这个学生信息,可能就得每次都挨个将其传入函数中,是不是非常的麻烦?
这样的麻烦事有很多,而这些麻烦事都可以用类这个概念来解决!
类的基本概念就是:将所有相关的数据、函数都包装成一个抽象的集合!
比如上面提到的一个学生,他可能有很多身份数据信息,那就把这些身份数据信息全都放在一个学生类中就可以了,这样一个新的自定义数据类型就产生了:学生
对于学生而言,它可能有许多相关的操作,比如:设置姓名、年龄、学号、并按照一定的格式标准输出这些相关信息。
那这些操作就可以对应一个又一个函数,并且将其绑定到(封装到)这个类中,那么之后这个函数就只有属于这个类的对象可以调用,其他类型是无法调用的(因为绑定了)。
而且由于封装在了类中,你就不再需要记住这些函数名,因为IDE可以给我们相关的提示信息,你只需要大概记得有这么个函数、知道它的前两个字母就行了。
1.1 简单使用
上面是理解,但只是空说其实也不太好理解,所以这里先试着封装一个简单的类:
class Student:
def set_name(self, name: str):
self.name = name
def set_age(self, age: int):
self.age = age
def fmt(self):
return self.name+" "+str(self.age)
s=Student()
s.set_age(100)
s.set_name('yushi')
print(s.fmt())
上面的代码并不很难,缩进什么的应该不用我再多说什么了,简而言之就是只要你这段代码属于外面一层,那就需要一个tab
键。
要使用一个类,首先需要声明,使用关键字class
,其后跟着你想要的类名称,比如我这里的是Student
。
然后你就可以在里面定义一系列函数了!
是不是觉得有点奇怪?明明上面我说的是类封装函数、变量,怎么这里只有函数呢?
因为类是一个很抽象的概念,并没有分配具体的内存,加上python是一个弱类型的语言,所以它的变量是可以动态绑定的。
也就是set_name
等函数中,第一个参数self
,就指代当前这个类型,然后只要我给它赋值一个变量,那它就会自动生成这么一个变量,用的方式就是点.
。
类中函数第一个参数就是用来指代这个对象本身的,一般用的是单词:self
,但其实你使用其它单词也是可以的
使用方法就很简单了,就和前面调用函数差不多:直接用Student()
就能返回一个这个类型的变量。
由于这是一个类,它内部是绑定了函数的,所以你就可以直接用点的方式使用这些函数:
这个时候,你就可以看到IDE的提示信息了,蓝色图标代指这是一个变量,紫色图标代指这是一个函数。
其中还有大量的__*__
的变量、函数,这是python为每个类自动生成的东西,也可以调用(具体来说涉及到了类的继承,所有类都默认继承自Object
,这些多出来的函数、变量也是这个类身上的)。
所以下面两句代码其实是等价的:
s.age=10
s.set_age(100)
当你调用set_age
这个函数时,它的第一个参数self
现在就是前面的s
,然后进入这个函数内,将self
替换为s
,你就会发现这两句代码完全一样。
这就是类最简单、最基础的用法了:封装方法、函数
而且你应该是能感受得到这种方法的优点的,如果感受不到,你可以试试不用类来实现这个试一试。
比如我需要五个学生变量,使用类的话,我只需要调用Student()
五次就行了,不使用类,你就得分别声明五个name
、age
变量,这还只是两个变量的情况下,如果更多,那就更加复杂了。
1.2 初始化函数
其实如果你稍微思索一下就会发现,像上面那样写是有问题的,比如如果我不调用set_name
这些函数、也不初始化这两个变量,直接调用fmt
函数怎么办呢?
s=Student()
print(s.fmt())
这时候肯定就会报错了!因为你还没有为其绑定对应的变量,而fmt
函数中却要使用这两个变量,没有,那肯定就报错了!
为了解决这个问题,就出现了初始化函数(也称为构造函数):
def __init__(self):
self.name=""
self.age=0
这个函数名字是固定的,它的第一个参数同样是self
,写在对应的类中。
它与普通的成员函数唯一不同的一点是,它会默认调用。
也就是说,这个函数不需要你手动调用,只要你实例化了一个这个类的对象,那就会立马自动调用这个函数。
这就完成了初始化的工作!
class Student:
def __init__(self):
self.name = ""
self.age = 0
def set_name(self, name: str):
self.name = name
def set_age(self, age: int):
self.age = age
def fmt(self):
return self.name + " " + str(self.age)
s = Student()
print(s.fmt())
此时就不会出现任何问题了!
但既然能自动初始化,那是不是可以再简化一点呢?比如在实例化一个对象时,就立即初始化好一个我们想要的对象!
这当然是可以的!
def __init__(self, name: str, age: int):
self.name = name
self.age = age
你只需要像普通函数那样,为其写上参数就好了。
然后在实例化一个对象时,就可以像下面这样做:
s = Student(name='yushi',age=100)
print(s.fmt())
是不是感觉和函数调用特别像!
1.3 类的特性
前面为了好理解,说的可能比较笼统,所以这里规范一下他们的名称:
- 类:封装属性、方法的抽象化类型(可以理解为一个自定义的类型,比如int、str等)
- 对象:为类的具体表现形式(就和人类与你的区别,一个大而抽象、一个小而具体),拥有具体的内存,一般称为实例化一个对象
- 属性:也就是绑定到类上面的那些变量,称为属性
- 方法:也就是绑定到类上面的那些函数
对于类这个概念来说,一般有三个特性:封装、继承、多态
其中的封装,前面已经提到了,就是将关联的变量与函数统一到一个类中。
但那会暴露全部方法,并不完全封装。
有些方法可能是我们这个类自己用的,并不想要暴露出去让对象直接调用,所以就需要将其私有化,这在python中非常简单,就是在方法名前添加两个下划线_
:
def __test(self):
self.name ='test'
这时如果你想要调用它,就会直接报错:
s = Student()
s.__test()
报错信息为:
也就是实例化的对象无法调用它了,但你却可以在其它方法内部调用这个函数,也就实现了私有化函数的目的:
class Student:
def __test(self):
self.name ='test'
def set_name(self, name: str):
self.__test()
def fmt(self):
return self.name + " " + str(self.age)
s = Student()
s.set_name("111")
print(s.fmt())
不仅仅是函数,变量也是同理,因为你会发现,你在类中所有的变量,都是可以直接在外部直接使用的!
class Student:
def __init__(self):
self.name = 'test'
s = Student()
s.name # 可以直接使用
这显然有些违背封装的初衷:内部实现细节只需要我自己知道,你只管调用函数使用就行了。
当你不想要让外部直接使用,就可以私有化这个变量,方式同样为在变量名前面添加两个下划线:
class Student:
def __init__(self):
self.__name = 'test'
s = Student()
s.__name # 不可以直接使用私有化数据
至于多态……由于python是弱类型语言,这个特性其实并不明显,所以本文不会过多介绍。
所谓弱类型,就是变量的类型是不定的、任意改变的,比如下面这段代码:
a=10
a='string'
print(a)
这没有任何问题,最终a的值为最后赋值的’string‘
,而如果是在c/c++等强类型语言中,这种代码肯定是无法编译的。
所以我们重点来关注一下继承,所谓继承,其实就是字面意思,得到父辈的东西。
比较好理解的一个例子就是动物与鸟、狗,很明显,这三个都属于抽象的类,但你又能明显感觉得到,鸟与狗都是属于动物的。
这就是继承的概念,无论是鸟还是狗,它们都属于动物,也就有动物的共同特性,比如会吃饭、会叫等等。
这个时候,如果不使用继承,你就必须在鸟、狗类里面各写一份完全相同的代码:
class Bird:
def eat(self, foot: str):
self.foot= foot
class Dog:
def eat(self, foot: str):
self.foot= foot
这就造成了代码重复,并且如果后面我想要改这个函数,还得改两个地方,很麻烦。
这时候就可以使用继承属性了!
class Animal:
def __init__(self):
self.food = None
def eat(self, foot: str):
self.food = foot
class Bird(Animal):
def __init__(self):
super().__init__()
class Dog(Animal):
def __init__(self):
super().__init__()
因为python语言的特性,必须得写一点代码你才能写这个类,所以我就给每个类都写了一个初始化函数。
不看初始化函数,你就发现,这次我们只在一个Animal
类中写了一个eat
函数。
如何继承呢?非常的简单!直接在类名后面添加一个小括号,写上你想要继承的父类就行了!
class Bird(Animal):
这个时候,子类也就有了父类的相关方法:
b = Bird()
b.eat("1111")
d = Dog()
d.eat('2222')
另外一个值得注意的点是在子类的初始化函数中调用了这么个函数:
super().__init__()
这是因为继承的关系,你得先初始化父类、再初始自己吧!
所以就会有super()
这个函数,代表你继承的父类,然后再调用父类的__init__
初始化方法就行了!
一般初始化函数就是用来声明当前这个类所拥有的全部属性变量。
当然,除了单继承,其实你还可以多继承(即一个子类继承多个父类):
class child(p1,p2):
也是直接写在小括号中,中间用逗号分隔。
但多继承很容易引发混乱,比如两个父类有一个同名函数,那子类应该继承谁的呢?所以多继承一般来说用的都比较少。
继承下来的方法并非是不可变的,比如一个父类有十多种方法,但其中某个方法这个子类需要特殊化。
比如动物有很多特性,其中一个方法为该动物的具体类,比如是鸟、还是狗。
这个时候,你就可以通过在子类重写这个方法达成这个目的:
class Bird(Animal):
#省略初始化函数
def eat(self, foot: str):
self.food = 'bird:' + foot
比如以前面这个鸟继承动物类为例,你就可以重写这个继承下来的方法
这个时候,当Bird这个类实例化的对象调用eat时,调用的就会是重写后的方法,而不是父类的方法。
同时需要注意,所有python的类,都默认继承了python中内置的Object
类,所以你会发现,即使你自定义的类中没有写任何东西,但依旧会有很多函数、方法可以使用。
1.4 静态方法
前面的方法我们可以看到,所有的方法第一个参数都是self
:
class class_name:
def fun(self):
#.....
这就是属于具体实例化对象的方法,因为这个self
本质上来说,相比于普通函数,就是可以让你少写一个参数而已,即这个对象本身:
c=class_name()
c.fun()
当你用点的方式去调用这个函数,实际上就是把前面的对象c
也传递进去了,作为函数的第一个参数self
。
但有些时候我可能并不需要将类中的函数绑定在具体的对象上,比如一个日期类,我想要实现一个方法来获取当前的时间。
这个方法显然是非常固定的,如果绑定在具体的对象上,用起来就会感觉很麻烦:
class Date:
def cur_date(self):
return '2023-7-29'
d = Date()
cur_date = d.cur_date() # 获取当前日期
也就是你每次想要调用这个函数,你都得先实例化一个对象。
当然,这个时候你可能就会说:我直接将其写为一个普通函数不就好了?
这样当然可以,但如果这样做另一个问题就出现了:这只是一个函数,如果函数一多,比如成百上千个,你怎么记呢?
而且这样做很容易出现同名的情况。如果将其写在日期里面就很简单了,我想要获取当前日期,第一时间自然是想看看日期类里面有没有相关的函数嘛,这样就有了一种树形的层级关系。
在这种情况下,我们就可以用到静态函数了,它的作用就是:不绑定到具体的某个对象上,但又属于这个类。
使用方法并不难,直接在函数的上面添加一个标注:@staticmethod
即可,其实就是两个单词:static
method
:静态方法,然后去除掉函数第一个参数self
就行了:
class Date:
@staticmethod
def cur_date():
return '2023-7-29'
cur_date=Date.cur_date() # 没有实例化对象,直接使用
这个时候就非常舒服了!因为没有self
参数,所以你无需实例化一个对象,可以直接通过类名来调用这个函数!
其中这种语法@staticmethod
叫做装饰器,这只是其中一种,当你了解到后面,会遇到其它很多的装饰器。
1.5 特殊方法
最后一个要介绍就是特殊方法,这在python 类中也很常用。
比如前面用到的初始化函数:__init__
,这就是一个特殊方法,专门用来初始化我们成员变量的、或者其它初始化操作的,实际上都是继承自内置的Object
获得的。
除此之外,还有需要其它的特殊方法,而这些特殊方法都有一个共性,那就是用两个下划线作为开头和结尾:__fun__
。
当你输入两个下滑线时,就能看到vscode已经提示出来了有哪些特殊函数:
特殊函数非常之多!所以这里只挑几个可能比较常用的说明一下就好了。
首先是__str__
,这个函数用于你打印这个对象时调用:
class Test:
def __str__(self):
return '123-456-789'
t = Test()
print(t) # 在打印时会调用__str__函数,并打印它的返回值
所以很多时候你看到print
能打印很多奇奇怪怪的类型,并不是print
本身很强大,它只能打印字符串而已。
只不过不同之处在于它在打印的时候会去调用这个对象的__str__
函数,让其自定义打印规则。
还有__len__
,为什么len
函数可以求很多类型的长度?就因为它们都实现了这个__len__
方法:
class Test:
def __len__(self):
return 120
t = Test()
print(len(t))
当你使用len
函数的时候,它就会去调用这个对象的__len__
函数,返回这个函数的返回值。
还有以后你可能会常常看到两个对象进行比较的,其实也有对应的特殊函数:__eq__
class Test:
def __eq__(self, other):
return True
t = Test()
t1 = Test()
b = (t == t1) # 比较时会调用__eq__函数,并返回这个函数的返回值
print(b)
看了上面几个特殊函数,你应该也差不多能明白它的含义了:特殊函数只在某个特殊情况下会被自动调用,比如上面的求长度、打印、比较等等操作。
所以以后当你想要让自己的类实现某个看起来很奇怪的操作,那就可以去找找有没有对应的特殊函数可以使用就行了。
2 异常
我们首先来聊聊异常,这是很多高级语言都拥有的一个特性,它并非什么高大上的东西,其仅仅只是一种程序错误的处理方式。
较低级的语言不使用它的原因很简单:对程序的性能影响较大,所以一般会更倾向于通过返回值来判断。
但对于像python
这种高级语言,那就无所谓了,异常几乎随处可见,与java
差不多。
为了更好理解异常,首先我们来看看低级语言是如何处理错误的: