2. 一文入门Python核心教程

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()就能返回一个这个类型的变量。

由于这是一个类,它内部是绑定了函数的,所以你就可以直接用点的方式使用这些函数:

image-20240309124110159

这个时候,你就可以看到IDE的提示信息了,蓝色图标代指这是一个变量,紫色图标代指这是一个函数。

其中还有大量的__*__的变量、函数,这是python为每个类自动生成的东西,也可以调用(具体来说涉及到了类的继承,所有类都默认继承自Object,这些多出来的函数、变量也是这个类身上的)。

所以下面两句代码其实是等价的:

s.age=10
s.set_age(100)

当你调用set_age这个函数时,它的第一个参数self现在就是前面的s,然后进入这个函数内,将self替换为s,你就会发现这两句代码完全一样。

这就是类最简单、最基础的用法了:封装方法函数

而且你应该是能感受得到这种方法的优点的,如果感受不到,你可以试试不用类来实现这个试一试。

比如我需要五个学生变量,使用类的话,我只需要调用Student()五次就行了,不使用类,你就得分别声明五个nameage变量,这还只是两个变量的情况下,如果更多,那就更加复杂了。

1.2 初始化函数

其实如果你稍微思索一下就会发现,像上面那样写是有问题的,比如如果我不调用set_name这些函数、也不初始化这两个变量,直接调用fmt函数怎么办呢?

s=Student()
print(s.fmt())

这时候肯定就会报错了!因为你还没有为其绑定对应的变量,而fmt函数中却要使用这两个变量,没有,那肯定就报错了!

image-20240309124528124

为了解决这个问题,就出现了初始化函数(也称为构造函数):

    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()

报错信息为:

image-20240309124734004

也就是实例化的对象无法调用它了,但你却可以在其它方法内部调用这个函数,也就实现了私有化函数的目的:

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已经提示出来了有哪些特殊函数:

image-20240309125544380

特殊函数非常之多!所以这里只挑几个可能比较常用的说明一下就好了。

首先是__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差不多。

为了更好理解异常,首先我们来看看低级语言是如何处理错误的: