Post

【Python 进阶】面向对象编程,一定要知道的类变量、类方法、静态方法

让我们首先从一个简单的例子开始。想象我们现在需要开发一个学生管理系统,那么自然会有一个这样的 Student 类,输入姓名和性别,来实例化一个学生。

1
2
3
4
5
6
7
class Student:

    def __init__(self, name, sex):
        self.name = name
        self.sex = sex

stu = Student('Qiqi', 'Female')

实际的案例当然没有这么简单,我进行了简化,为了更好 focus 在本次的主题上。

通过这个类,我们可以轻松定义一名学生,包含这个学生的名字、性别或者其他和这名学生相关的信息。

但如果我想要知道和整个 Student 类有关的信息,比如一共有多少学生。实例方法或者实例变量无法帮助我们知道,因为它们仅与单个实例有关,也就是只和单个学生有关。

于是,这就来到了我们今天的主题,类变量、类方法、和静态方法

基本使用

类变量

现在,我在 Student 类下面新增一个变量 student_num ,用于记录学生人数。

1
2
class Student:
    student_num = 0

可以看到 student_num 不同于一般的实例变量,它不需要在 构造方法中定义,而是直接在类下面定义。

同时,我需要在构造方法中改变 student_num ,使得每定义一个学生,student_num 就往上加一。

1
2
3
4
def __init__(self, name, sex):
    self.name = name
		self.sex = sex
    student_num += 1

运行一下,噢报错了!那是因为我们需要通过类来访问 student_num

1
2
3
4
def __init__(self, name, sex):
    self.name = name
    self.sex = sex
    Student.student_num += 1

运行一下看看!

1
2
3
4
5
print(f'Student.student_num:{Student.student_num}')

'''
Student.student_num:1
'''

符合我们的预期,学生人数变成了 1!

访问类变量,通过类还是实例?

刚刚提到类变量需要通过类访问,实际上,通过实例访问也不会报错。

1
2
3
4
def __init__(self, name, sex):
    self.name = name
    self.sex = sex
    self.student_num += 1

但让我们打印一下……

1
2
3
4
5
6
7
print(f'Student.student_num:{Student.student_num}')
print(f'stu.student_num:{stu.student_num}')

'''
Student.student_num:0
stu.student_num:1
'''

两者的数量不一样!这里确实让人有点困惑。

其中的原因是,在执行 self.student_num += 1 时,Python 需要先取值,再写入。

实例中不存在 student_num 这一属性,因此取值时,从类里面获取了 student_num 的值 0;而写入时,给实例增加了这一属性写入。

因此,为了不造成混乱,我建议通过类访问。

类方法

类方法需要使用装饰器 @classmethod 进行修饰。

1
@classmethod

关于装饰器使用我会在之后专门讲解,现在让我们 focus 在类方法的使用上。

我要定义一个类方法用于一次添加多个学生,给 student_num 加上一个数。

1
2
3
@classmethod
def add_students(cls, add_num):
    cls.student_num += add_num

注意,类方法的第一个参数需要是类本身,本例中我们使用 cls 表示 class,这也是被广泛使用的表达方式。通过 cls 作为参数,可以访问到类变量类方法等类里面的东西。

使用类方法替代构造方法

使用类方法代替构造方法是一种常见的做法,尤其在输入的形式比较多的时候。以我们目前的代码,实例化一名学生的时候需要分别输入名字和性别,但有时候输入可能是一段字符串或者一个列表。

1
stu1 = Student('Qiqi Female')

这样就不满足构造方法的要求了,但在一个庞大的系统中,输入的形式五花八门是很常见的。一个常见的解决办法是定义几个类方法解析输入,再执行构造函数。

现在,我定义一个解析字符串的类方法。

1
2
3
4
5
6
7
8
9
10
11
12
@classmethod
def from_string(cls, info):
		name, sex = info.split(' ')
    return cls(name, sex)

stu1 = Student.from_string('Qiqi Male')
print(f'stu.name: {stu.name}\nstu.sex: {stu.sex}')

'''
stu.name:Qiqi
stu.sex:Male
'''

在类方法 from_string 中解析输入,再调用构造函数。这个思路同样可以用来解析列表,字典等等。

在官方库中,这种做法被广泛使用,让我们看看 datetime.date 是怎么做的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@classmethod
def fromtimestamp(cls, t):
    "Construct a date from a POSIX timestamp (like time.time())."
    y, m, d, hh, mm, ss, weekday, jday, dst = _time.localtime(t)
    return cls(y, m, d)

@classmethod
def fromordinal(cls, n):
    """Construct a date from a proleptic Gregorian ordinal.

    January 1 of year 1 is day 1.  Only the year, month and day are
    non-zero in the result.
    """
    y, m, d = _ord2ymd(n)
    return cls(y, m, d)

这两个方法从不同类型的事件对象中提取出年月日。我们不需要过于关注细节,只需要了解,使用类方法替代构造方法是一种重要且常见的方法。

静态方法

静态方法需要由 @staticmethod 装饰。

1
@staticmethod

但与类方法不同,不需要传入参数 clsself,自然也就不能访问类和实例里面的私有属性。简单来说,静态方法和类的关系就像寄宿家庭和寄宿学生的关系,没有血缘关系,只是住在一起。

静态方法的适用场景是,虽然不需要类里面的内容,但在逻辑上需要在类里面的方法;也适用于将一系列功能相关的函数封装在一起。

在这次的示例中,我定义一个获取名字长度的静态方法。

1
2
3
@staticmethod
def name_len(name):
    return len(name)

然后在外部调用 name_len 。注意,调用静态方法时,也需要通过类名调用。

1
print(f'stu name: {stu.name}, name len: {Student.name_len(stu.name)}')

运行一下,stu 的名字是 Qiqi,长度为 4,与我们想的一致。

总结

让我们迅速回顾一下这次的内容。

类变量定义在类里面,需要通过类名访问;类方法适用装饰器 @classmethod ,第一个参数是 cls;静态方法使用装饰器 @staticmethod ,不可以访问类内部。

以上就是本次分享的全部内容啦,欢迎在评论区留下你的意见,我会尽量回复。如果觉得内容有帮助可以点个赞,如果想继续收看这类内容可以点点关注,感谢你的阅读。

This post is licensed under CC BY 4.0 by the author.