前言

什么是类?

类(Class):可以片面的理解为一类相似事物的抽象。

以圆为例,圆具有半径这样一个相似的属性,并且都可以执行面积、周长等运算

为什么用类?

为了是代码更加抽象,封装。事实上,我们以前写的每个 python 代码里都创建了各种各样的类。

一些例子比如 DataFrame 类。

你可能会用 read_csv 创建一个 DataFrame 类,可能也会用下标运算符,来获取它的某一列,也可能会用它的 drop_duplicates 方法。事实上,你并不关心 DataFrame 是怎么把数据排在一起的,你也不关心为什么 drop_duplicates 就能把所有重复的去除,但是,通过这一个抽象的类,你可以方便的调用它的功能。

DataFrame类,将整个表的数据,仅仅用一个变量,就全部包含起来,就像是封装在了一个小小的纸板箱中📦

需要注意的是,pandas 库并不是魔法,而是正常的 python 语法。一切 pandas 库的功能都是我们也可以实现出一模一样的。
比如说,为什么 df[(df['a']>5) & (df['b']<8)] 中用的是 &,而不是 and,更不是 && 呢?这是因为,python 只能重载按位与运算符(&),而不能重载逻辑与运算符(and)。(这句话可以看不懂,其中有一些概念:重载,按位与,逻辑与)

类的使用

定义

对比函数,类的定义是 class 关键字加上类名组成的一个代码块。

1
2
class Circle:
pass

pass 语句可以使代码块留空,以后再写。(比如 if, else, for, def 的冒号后面内容,不写会报错,用 pass 占位)

一般来说,我们习惯上类名使用大驼峰命名法,变量名及函数名使用小驼峰命名法。

小驼峰命名法:第一个单词首字母小写,后面其他单词首字母大写。
大驼峰命名法:每个单词的第一个字母都要大写。

创建实例

使用 类名(参数*可选*) 可以创建一个实例(Instance)。和函数差不多。

你已经使用过这个语法了。

1
2
3
4
from bs4 import BeautifulSoup
import requests

soup = BeautifulSoup(requests.get("https://linlexiao.com").text)

这段代码就构建了一个 BeautifulSoup 类实例。

实例和类的关系:还是拿圆来举例子,类相当于所有圆,而实例可能是一个半径为 1,半径为 5,半径为 1.4 的圆。

这说明,一个类可以有多个实例。(很好理解,就拿 DataFrame 类来说,你可以有不同的数据表格,就相当于不同的实例)

1
c1 = Circle()

这样创建一个 Circle 类的实例,并将其附在变量 c1 上。

类属性与实例属性

在 python 中,属性 (Attribute) 可以理解为类/实例的一些数值。(比如说,圆的半径)

属性通过 实例.属性名 获取,还可以进行修改。例如:

1
2
3
4
import requests
req = requests.get("https://linlexiao.com")
req.encoding = "utf-8" # 修改属性
print(req.text) # 获取属性

类属性

类属性在每个实例中值都相同。比如说在圆的例子里,可能是这个图形的名字(“Circle”)。

直接在类中使用 变量名=值 创建一个类属性。类属性用的机会并不多,因此你可以不太关注它。在实际过程中,大部分的属性都是实例属性

1
2
3
4
5
6
7
class Circle:
name = "circle"

c1 = Circle()
c2 = Circle()
print(c1.name) # circle
print(c2.name) # circle

实例属性

实例属性的值随着实例的变化而变化。比如说圆的半径(显然,每个圆的半径都不同)。

实例属性通常情况下会在 __init__ 方法中初始化。(下一章节介绍)

1
2
3
4
5
6
7
8
class Circle:
def __init__(self, r):
self.r = r

c1 = Circle(5)
c2 = Circle(8)
print(c1.r) # 5
print(c2.r) # 8

方法

方法也放在类中定义,和函数一样,使用 def 关键字。

魔术方法

魔术方法 (Magic Method):一些以两个下划线为开头和结尾,固定名字的 python 特殊方法(用于执行特定任务)。

在所有魔术方法中,你最需要知道的一定是 __init__() 方法。
当创建实例时,__init__() 方法被自动调用为创建的实例增加实例属性。__init__() 方法第一个参数是 self(这句话是不准确的,但是这么理解是 100% OK的),后面的参数为你要的实例属性。在进行实例化的时候传入后面的参数(第一个参数不用传)。使用 self.变量名=值 来定义一个实例对象。

正如魔术方法所有名字加了两个下划线告诉你的,在正常情况下,你绝不应该手动调用任何一个魔术方法。python 会在合适的时机自动帮你调用。(比如 __init__() 在实例化时被自动调用)

方法

方法 (Method):在类中定义的函数被称为方法(在某些语言中,我们还习惯称其为成员函数)。

方法还可以用来执行某些通用的操作(对圆来说,求周长、面积。对 DataFrame 来说,drop_duplicatesdrop_na)。

普通的方法也要有第一个参数 self。可以把这个 self 理解为当前这个类的实例。

1
2
def area(self):
return 3.1415 * self.r**2

调用方法通过 对象名.方法名(参数*可选*) 来实现。

1
2
c1 = Circle(5)
print(c1.area())

你见过这种表示方法

方法名 使用方法
socket.send(bytes) 发送二进制数据给套接字。本套接字必须已连接到远程套接字。
socket.recv(bufsize) 从套接字接收数据。返回值是一个字节对象,表示接收到的数据。bufsize 指定一次接收的最大数据量。

这里的 socket. 指的是这个类,在实际调用时要变成你的实例。

即调用方法使用 实例名.方法名而不是 类名.方法名

同理 DataFrame.drop_duplicates()

方法一定要有第一个 self 参数。这是由 python 底层对类方法的实现导致的。

如果你执行 object.method(args),其实等价于 MyClass.method(object, args)
具体来说,c1.area() 等价于 Circle.area(c1)

因此,方法的第一个参数即为用来接收实例对象。

实例

灵活运用上面的知识,我们写出了一个向量的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import math

class Vector:
def __init__(self, _x, _y):
self.x = _x
self.y = _y

def __mul__(self,other):
return self.x*other.x+self.y*other.y

def __abs__(self):
return math.sqrt(self.x**2 + self.y**2)

def show(self):
print(f"Vector({self.x},{self.y})")


def angle(v1, v2):
return math.acos((v1*v2)/(abs(v1)*abs(v2)))

a = Vector(5,3)
b = Vector(3,-5)

a.show()
b.show()

print("The Angle of A,B is {} radian(s).".format(angle(a,b)))

任务:
要求在源代码上进行修改,增加一个 Vector.rotate(x) 方法,将向量以原点为中心,逆时针旋转 x 弧度(就地修改)。

示例执行:

1
2
3
4
a = Vector(5,3)
a.rotate(math.pi/2)

a.show() # Vector(-3,5)

(x,y)(x,y) 绕原点逆时针旋转 θ\theta 后得到点的坐标为:

(xcosθysinθ,xsinθ+ycosθ)(x\cos\theta-y\sin\theta,x\sin\theta+y\cos\theta)

可以使用复数的三角形式自行推导。

鸭子类型

“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”

简单来说,一个接口,多种实现。

比如说 sum 函数。它可以计算一个列表、元组、甚至是 pandas.Series 的和。
也就是说,只要我们自己的类实现了相关接口,也可以被 sum 函数计算和。具体来说,我们要让我们的对象是一个 Iterable 的对象。

一个 Iterable 对象可以干很多事:被 for 循环迭代,在其他的一些接口里都可以调用。换句话说,如果一个对象迭代起来像列表,那么它就是一个列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyRange(object):
def __init__(self, end):
self.start = 0
self.end = end

def __iter__(self):
return self

def __next__(self):
if self.start < self.end:
ret = self.start
self.start += 1
return ret
else:
raise StopIteration

for i in MyRange(14):
print(i)

print(sum(MyRange(14)))