Python 系列学习之八:面向对象编程 - 类、实例、类方法以及私有属性

前言

打算写一系列文章来记录自己学习 Python 3 的点滴;本章主要介绍 Python 有关面向对象编程中的类和实例的相关内容;

本文为作者的原创作品,转载需注明出处;

类的定义

1
2
3
4
5
6
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
def print_score(self):
print('Student name is %s and sore is %s' % (self.name, self.score))
  1. object 是 Student 的父类,表示 Student 继承自 object 类
  2. __init__是 Student 的构造函数
    构造函数的第一个参数 self 表示 this
    self.name 和 self.score 分别表示 Student 的成员变量 name 和 score;
  3. print_score 表示类的实例方法;

类的类型

1
2
>>> Student
<class '__main__.Student'>

可见,Student 本身是一个 class 对象,其签名是 ‘__main__.Student’;这里可以对比 Java,Java 是将 Class 对象存放在其内存模型的方法区中,是一个一旦初始化后基本上就不再发生变化的固定内存区域;可以预见的是,Python 一定也会将相关的类的信息存储在内存中的某个区域中。

1
2
>>> type(Student)
<class 'type'>

class 本身也是一个对象,既然是对象就有其对应的类型,从上述的输出结果可以看到,class 的类型是type,有关type的相关内容将会在后续元类的章节中深入描述;

类变量和实例变量

变量分为两种,一种是类变量,一种是实例变量;看一个例子,

1
2
3
4
class Dog:
kind = 'canine' # class variable shared by all instances
def __init__(self, name):
self.name = name # instance variable unique to each instance

上面分别定义了两个变量,一个是类变量kind,一个是实例变量name

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind # shared by all dogs
'canine'
>>> e.kind # shared by all dogs
'canine'
>>> d.name # unique to d
'Fido'
>>> e.name # unique to e
'Buddy'
>>> Dog.kind
'canine'
>>> Dog.name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: type object 'Dog' has no attribute 'name'

类变量相当于 Java 类的静态变量,所有实例所共有的,也可以直接通过类进行调用;实例变量专属于实例本身;

再谈类变量

再谈类变量,类变量的确是所有实例所共有的,但是类变量在赋值过程中,是值拷贝,什么意思,你想告诉我什么东西?看下面这个例子,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> class User:
... name = 'zhangsan'
...
>>> u1 = User()
>>> u2 = User()
>>> u1.name
'zhangsan'
>>> u2.name
'zhangsan'
>>> u1.name = 'lisi'
>>> u1.name
'lisi'
>>> u2.name
'zhangsan'
>>> user.name
'zhangsan'

直接跳转到第 10 行和第 11 行,我们为实例 u1 进行赋值,将类变量 name 改为 ‘lisi’,但是,你会惊奇的发现,它并不影响 u2 和 User 中的类变量 name;这个和其它语言中的类变量(静态变量)的定义不同,只要一处修改,处处都会修改,因为类似 Java 语言,采用的是引用拷贝的方式,而这里采用的是值拷贝的方式,所以,导致 u1 对类变量的修改在其它地方并不会生效;这等价于什么?其实,当通过 User() 得到实例 _u1_ 和 _u2_ 以后,通过值拷贝,类变量 name 实际上已经变为实例 _u1_ 和 _u2_ 的实例变量了;OK,上面我们看到了类变量转变为实例变量的过程,但是,正规教程中教导我们,实例变量要通过 __init__() 方法来为实例变量赋值,为了彻底搞清楚两种实例变量的形成过程,我们来看看下面这个测试用例,

1
2
3
4
class User:
name = 'zhangsan'
def __init__(self, name):
self.name = name

执行,

1
2
3
4
5
>>> User.name
'zhangsan'
>>> u1 = User('lisi')
>>> u1.name
'lisi'

可见,通过 __init__() 方法所生成的实例变量 name 会覆盖通过类变量值拷贝生成的实例变量 name

最后,要补充的是,通过类变量构造实例变量的正确姿势是,

1
2
3
>>> u1 = User(name='lisi')
>>> u1.name
'lisi'

在构造实例的时候,直接通过关键字参数进行初始化;

静态方法、类方法和实例方法

类的方法定义总共分为三种,实例方法、静态方法以及类方法;看下面这个例子,

1
2
3
4
5
6
7
8
9
10
11
12
class Student(object):
def __init__(self, name, score):
self.name = name
self.score = score
def print_score(self):
print('Student name is %s and sore is %s' % (self.name, self.score))
@staticmethod
def print_school_name():
print('Southwest University')
@classmethod
def print_class_name(cls):
print(cls.__name__);

上述的 Student 类分别定义了上述三种类的方法,实例方法、静态方法以及类方法,它们分别是 print_score、print_school_name 以及 print_class_name 方法;实例方法比较好理解,就是必须通过实例来进行调用,并且方法内部通过关键字 self 获取对象本身;那么类方法和静态方法呢?它们的区别是什么?严格说来,类方法也是静态方法,同样可以通过类进行直接调用,但是,唯一的区别是,类方法所接收的第一个参数是当前类既 class 本身,这样,在调用类方法的时候,有个好处就是可以获取类的信息;看看下面执行的结果就一目了然了,

1
2
3
4
5
6
7
>>> linda = Student('Linda', 100)
>>> linda.print_score()
Student name is Linda and score is 100
>>> Student.print_school_name()
Southwest University
>>> Student.print_class_info()
Student

动态扩展属性和方法

新增

假设我们要为类的定义小节中的 Student 类动态出一个属性 gender 该如何做呢?

1
2
3
4
5
>>> linda = Student('Linda Boston', 100)
>>> linda.gender
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'gender'

可以看到,默认情况下,Student 的实例 linda 是没有属性 gender 的,那么下面,我们为 Student 动态扩展出这样一个属性,(其实,当笔者在写这个测试用例到这里的时候,经验告诉我,这里一定扩展出来的是一个静态的变量而不是一个实例变量,但是,当笔者执行到后面的时候,发现自己彻底错误了,它定义的是一个实例变量,这一点和其它动态语言是有本质区别的,其它动态语言,比如 Javascript,这样动态扩展出来的就是一个静态变量,由所有的实例所共有;)

1
>>> Student.gender = 'female'

再次使用 linda 实例进行调用,

1
2
>>> linda.gender
'female'

并且,该动态扩展的属性,适用于任何 Student 的实例当中;(另外需要特别注意的是,通过这种方式所动态扩展出来的变量,于实例而言,不是静态变量,而是实例变量,后续通过动态扩展实例方法的时候,大家可以看到这个特性,这就是 Python 的强大之处了)

1
2
3
>>> sam = Student('Sam Borge', 60)
>>> sam.gender
'female'

但是,Sam 明明是位男性呀,这样可不太好;Ok,下面笔者将带领大家动态的为 Student 添加一个实例方法;实例方法?你说的是实例方法,是的,Python 厉害的地方就是,可以直接在 class 上动态扩展出实例方法;

首先,定义方法 set_gender

1
2
3
>>> def set_gender(self, gender):
... self.gender = gender
...

注意,定义方法的是时候,记得将第一个参数设置为 self;

再次,将该方法直接赋值给 Student,实现方法的动态扩展;

1
>>> Student.set_gender = set_gender

好了,这里我们来思考一下 Javascript 方法级别的动态扩展,Javascript 通过在 prototype 上动态扩展出方法和属性,但是这些方法和属性都是静态的,是所有 Javascript 实例所共享的;那么笔者下面将要演示的是,Python 的强大之处,它可以动态扩展出实例的方法和变量;

1
2
3
>>> sam.set_gender('male')
>>> sam.gender
'male'

如果读者读懂了上述的代码,一定会有所惊讶,我们就这样扩展出了 sam 的实例方法 set_gender,并且,成功的通过语句self.gender = gendermale赋值给了实例 sam 的成员变量 gender;是的,看到这里,我仍将半信半疑,你能保证 sam.gender 中的变量真的是成员变量(实例变量)而不是全局变量(静态变量)?是的,可以保证,看看下面这行简单的调用,便一目了然了….

1
2
>>> linda.gender
'female'

What… linda 的 gender 仍然是 female,可见在类级别上扩展出来的 gender 变量的确是实例属性;

由此,我们可以总结得到,直接在类上面所动态扩展出来的属性和方法,统统都是实例方法实例属性;这点,尤其需要注意;

删除

可以动态新增变量和方法,同样也可以动态的删除变量和方法,紧接新增小节的内容,让我们来看看如何动态删除属性和方法,

1
>>> del Student.gender

这样,我们就删除了 Student 的属性 gender;

1
2
3
4
>>> linda.gender
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'gender'

可见属性 gender 已经从 Student 以及其相关的实例中删除了;

实例

初始化

以小节类定定义小节中的 Student 为例,

1
2
3
>>> linda = Student('Linda Boston', 100)
>>> linda.print_score()
Student name is Linda Boston and sore is 100

如果为类定义了构造函数,那么初始化的时候,必须输入构造函数中的参数;否则会提示错误;

1
2
3
4
linda = Student()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: __init__() missing 2 required positional arguments: 'name' and 'score'

实例的类型

1
2
>>> linda
<__main__.Student object at 0x10b78c880>

可以看到,linda是一个 Student 类的一个实例(object),0x10b78c880 是该实例对象所在的内存地址;

1
2
>>> type(linda)
<class '__main__.Student'>

一个实例本身也是一个对象,对象有自己的类型,从上述输出中可以看到,linda 实例的类型是 Student class;

动态扩展属性和方法

新增

Python 的初始化比较好玩,它可以动态的给一个实例追加属性和方法,首先,我们定义一个空的类,

1
2
class Student(object):
pass

然后,我们初始化得到一个没有任何属性的实例 linda,

1
>>> linda = Student()

然后,我们动态的为 linda 实例扩展出相应的属性,

1
2
3
4
5
6
>>> linda.name = 'Linda Boston'
>>> linda.score = 100
>>> linda.name
'Linda Boston'
>>> linda.score
100

看到这里,如果学过 Javascript 的同学,一定会惊呼,简直和 Javascript 一模一样;其实也没什么好奇怪的,这个就是动态语言的特性和共性;

最后,我们看看一个最好玩的东西,就是动态扩展出一个方法,首先参照类定定义定义一个方法,

1
2
>>> def print_score(self):
... print('Student name is %s and sore is %s' % (self.name, self.score))

然后,我们有两种方式可以对其进行扩展,

  1. 直接赋值的方式,扩展出一个静态方法
    我们将这个方法直接赋值给该实例 linda,并将其当做实例方法一样去调用

    1
    2
    3
    4
    5
    >>> linda.print_score = print_score
    >>> linda.print_score()
    Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    TypeError: print_score() missing 1 required positional argument: 'self'

    可以看到,如果把 print_score 方法当做是 linda 的实例方法去调用,会报错,提示没有输入位置参数 self;正确的调用方式如下,

    1
    2
    >>> linda.print_score(linda)
    Student name is Linda Boston and sore is 100

    可以看到,通过直接赋值的方式动态扩展出来的方法是无法成为实例方法(或称作成员方法)的,这里,也就等价于为 linda 实例添加了一个静态方法 print_score;

  2. 借助types某块的MethodType方法,扩展出一个实例方法
    那么,如果我们的确需要扩展出一个 linda 的实例方法呢?该怎么操作呢?这里就需要借助types模块的MethodType方法了;将上述直接赋值的方式改为如下的方式,

    1
    2
    3
    4
    >>> from types import MethodType
    >>> linda.print_score = MethodType(print_score, linda)
    >>> linda.print_score()
    Student name is Linda Boston and sore is 100

    这样,我们就为实例 linda 扩展出了一个实例方法 print_score

    最后,要特别注意的是,为实例动态扩展出的实例属性和静态方法只属于该实例本身;如果想要为所有的实例动态扩展属性和方法,参考类的动态扩展属性和方法

删除

此部分参考有关类相关的删除小节的内容,两者的操作是一样的;

私有属性

Python 中如何为一个类定义私有属性呢,我们修改类的定义中所使用到的类 Student,

1
2
3
4
5
6
class Student(object):
def __init__(self, name, score):
self.__name = name
self.__score = score
def print_score(self):
print('Student name is %s and sore is %s' % (self.__name, self.__score))

我们将成员变量的定义前面加上了一个前缀____name__score,这样呢,就无法通过其实例直接访问到该实例变量了;

1
2
3
4
5
6
7
8
9
>>> linda = Student('Linda Boston', 100)
>>> linda.name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'name'
>>> linda.__name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute '__name'

可以看到,无论是通过 linda.__name 或者是通过 linda.name 都不能直接访问其私有属性了;不过,读者不要以为 Python 在编译器中做了什么强制性的约束,对私有变量的直接访问做了万无一失的约束,对,没有,Python 只是非常简单的将 __name 做了一下重命名而已,将其重命名为了 _Student__name,下面我们来验证一下,

1
2
>>> linda._Student__name
'Linda Boston'

是吧,我们通过这个方式,就访问到了实例 linda 的私有变量;笔者写到这里,突然想到,Python 的确是一门伟大的语言,现在也被用到大量的大数据分析领域,比如 TensorFlow,但是,为什么 Python 始终不为企业级应用开发所青睐呢,比如 IBM、Oracle 和 Alibaba 等等.. 或许就是因为 Python 过于灵活,而为了灵活而丧失了相关的约束性所导致的吧,正如上面的这个私有变量的实现方式,显得是那么的随意;那么,读者会问了,这样定义私有的成员变量不是形同虚设吗?是的,Python 给出的唯一解释就是,Python 程序员应该养成良好的习惯,当遇到 __ 前缀的变量的时候,你需要做的,就是“自律”,要把它当做一个私有变量来看待,不要随便的访问它;

Reference

https://docs.python.org/3/tutorial/classes.html