首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >盘一盘 Python 系列特别篇 - 面向对象编程

盘一盘 Python 系列特别篇 - 面向对象编程

作者头像
用户5753894
发布2019-10-08 15:00:07
8090
发布2019-10-08 15:00:07
举报
文章被收录于专栏:王的机器王的机器

本文含 14123 字,53 图表截屏

建议阅读 72 分钟

0

引言

在写 Keras (下) 时,发现很多内容都要用到 (class) 和对象 (object),因此本文作为 Python 系列的特别篇,主要介绍面向对象编程 (Object-Oriented Programming, OOP)。

如果只用一句话来区分 Python 和其它编程语言,那就是

万物皆对象。

那我再加唐突的加一句废话。

万物可分为若干类。

抖了这么多包袱,请注意上面两句话提到了两个词,对象

类 (class) 是对某一类事物的描述,是抽象的;而对象 (object) 是类的一个实例,是具体的。比如:

  • 「人」是类,而「运动员」则是「人」的一个实例。
  • 「车」是类,而「轿车」则是「车」的一个实例。
  • 「金融产品」是类,而「外汇期权」则是「金融产品」的一个实例。

还记得之前介绍的变量 (variables) 和函数 (functions) 吗?它们是零散的,而对象将它们集合起来,

  • 在对象里也有变量,用来存储数据,这时变量又称字段 (fields)
  • 在对象里也有函数,用来操作数据,这时函数又称方法 (methods)

字段方法统称为类的属性 (attributes)。

很抽象对不对?我故意的,这样你们才想往下看,或者点右上角的叉。

本帖的讲述逻辑如下:

  • 第一章先用 Python 里面内置的 int, list, ndarray 和 dataframe 变量举例,感受一下 Python 中万物皆对象,体会一下对象里的属性 (字段和方法)
  • 第二章详细介绍面向对象编程的细节,内容包括:实例变量、类变量、实例方法、类方法、静态方法、继承、多态、魔法方法、属性装饰器等。

本帖目录如下:

目录

第一章 - 对象初体验

1.1 整型 int

1.2 列表 list

1.3 NumPy 数组 - ndarray

1.4 Pandas 数据帧 - dataframe

第二章 - 面向对象编程

2.1 极简类和对象

2.2 __init__() 和 self

2.3 类变量 (千人千面)

2.4 类变量 (千人一面)

2.5 类方法 + 静态方法

2.6 其他构建函数

2.7 继承和多态

2.8 魔法方法

2.9 属性装饰器

总结

1

对象初体验

读本小节你不需要有什么心理负担,担心不懂对象和类怎么办?本节不需要懂,只需要跟着我节奏走。当然你需要大概知道整型变量、列表变量、numpy 数组变量和 pandas 数据帧变量。

回想一下,原来你是不是称它们都是变量?但其实上它们有更「高级」的叫法:类或对象。

1.1

整型 - int

整数类和对象

首先定义一个整数,并赋值。

# int 对象
i = 1031

在 C++ 和 Java 里,整数只是一个基础 (primitive) 类型,而在 Python 里,整数是一个,可以用来创建很多整数型对象

用 dir(i) 可以浏览到整数下的属性,侧面证明了 i 是对象 (注意基础变量下面是没有属性的)。

注意到属性以两种类型呈现

  • 一种就是普通的字符串,比如 numerator
  • 一种是字符串前带两个下划线后带两个下划线 (dunder),比如 __init__

我先不讲为什么,先注意到这个区别就可以了。

此外用 dir(int) 也能得到同样结果,侧面也证明了 int 是类。

再来看看 i 的类型,很显然应该是 int。

# int 的类型
type(i)
int

这样我们脑海里应该复现这样的类比:

类 : 对象

int : i

字段和方法

打印出对象 i 一个字段 numerator,注意字符串后面没有括号

# int 的 fields
i.numerator
1031

打印出对象 i 一个方法 bit_length(),注意字符串后面有括号

# int 的 methods
i.bit_length()
11

我们知道用 dir() 可以帮助我们该怎么写具体的属性名称,但在使用它们时,我们怎么知道后面要不要加括号呢?一看 numerator 就不加,而 bit_length() 就需要加。

方法 1:试着不加括号和加括号,总有一个或报错,那么就用另外一个。

方法 2:把鼠标放在属性名称上,按 shift + tab 键,就会用提示出来。

注意 Signature 后面写的带括号呢,因此使用 bit_length 的时候要加括号。

魔法方法

首先看看整数对象的加法乘法

# int 的 +
i + i
2062
# int 的 *
i * 2
2062

对于 Python 使用者,用普通二进制操作符 (binary operator) + 和 * 就能实现加法和乘法运算。

但对于 Python 开发者来说,他们是用魔法方法 (magic methods) __add__ 和 __mul__ 来实现加法和乘法运算的。看下面两个例子。

i.__add__(i)
2062
i.__mul__(2)
2062

这种更改 Python中运算符的含义的操作,被称为运算符重载 (overload operator)。整数对象之间的加法和乘法有明确的定义,试想你如何相加两辆轿车 (按质量相加还是按价格相加)?如何相加两个期权 (按现值相加还是按敏感度相加)?这里面有很多选择,具体要怎么加你都需要在 __add__ 里面实现逻辑。


再看魔法函数 __le__,le 是 less and equal than 的缩写,那么就是 ≤ 符号的重载。下例比较 i 是否小于等于 1,显然结果是 False。

i.__le__(1)
False

可能又有读者说了,我怎么知道括号里要放参数?还记得 shift + tab 这个骚操作吗?按了你就知道了,如下图所示。


最后看一个开发者用的最多的魔法函数,__repr__,它是 representation 的缩写。它会返回对象的一个编码字符串,可以用来重新创建对象,或者给开发者详细的显示。

i.__repr__()
'1031'

此外还有个 __str__ 魔法函数,它是 string 的缩写,作用和 __repr__ 差不多。它返回的编码字符串更加易读,因此是面向用户的,而不像 __repr__ 是面向开发者的。

i.__str__()
'1031'

整数对象的内容太简单,因此看不出 __repr__ 和 __str__ 的区别,对于自定义的类,那它俩可玩的名堂就多了。

1.2

列表 - list

关于类、对象和属性的一些基本知识点都在上节讲得差不多了,本节就体会列表类的属性。

列表类和对象

# list 对象
l = [1, 2, 3]

用 dir(l) 或 dir(list) 可以浏览到列表下的属性。

检查 l 的类型。

# list 的类型
type(l)
list

字段和方法

列表对象下没有字段 (fields),只有方法 (methods)。

# list 的 methods
l.append(4)
l
[1, 2, 3, 4]

魔法方法

首先介绍 __getitem__ 魔法方法,它的功能就是索引,对比下面两段代码。

l.__getitem__(2)
3
l[2]
3

和整数类一样,两个魔法函数 __add__ 和 __mul__ 重载二元运算符 + 和 *,在列表上进行操作。

# list 的 +
print(l + l)
print(l.__add__(l))
[1, 2, 3, 4, 1, 2, 3, 4]
[1, 2, 3, 4, 1, 2, 3, 4]
# list 的 *
print(l * 2)
print(l.__mul__(2))
[1, 2, 3, 4, 1, 2, 3, 4]
[1, 2, 3, 4, 1, 2, 3, 4]

最后看看面向用户和开发者的 __repr__ 和 __str__ 作用在列表对象 l 上。

l.__repr__()
'[1, 2, 3, 4]'
l.__str__()
'[1, 2, 3, 4]'

得到的都是同样的字符串 '[1, 2, 3, 4]'。

1.3

Numpy 数组 - ndarray

数组类和对象

本节来体会数组 numpy array 类的属性。

# ndarray 对象
import numpy as np
arr = np.array( [[1,2,3],[4,5,6],[7,8,9]] )
arr
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

用 dir(arr) 或 dir(np.array) 可以浏览到数组下的属性。

检查 arr 的类型。

# ndarray 的类型
type(arr)
numpy.ndarray

字段和方法

# ndarray 的 fields
arr.ndim
2

arr 是一个 2 维数组,因此用字段 ndim 返回 2。

# ndarray 的 methods
arr.sum()
45

arr 所有元素加起来等于 45,因此用方法 sum 返回 45。对于 numpy ndarray,还有一种语法来调用其方法。

np.sum(arr)
45

两种调用是等价的,我们大概可以摸出一些规律。你看 np 是,arr 是对象,那么可以抽象成一下两种等价调用。

对象.方法()

.方法(对象)

魔法方法

# ndarray 的 +
print( arr + arr )
print( arr.__add__(arr) )
[[ 2  4  6]
 [ 8 10 12]
 [14 16 18]]

[[ 2  4  6]
 [ 8 10 12]
 [14 16 18]]
# ndarray 的 *
print( arr * arr )
print( arr.__mul__(arr) )
[[ 1  4  9]
 [16 25 36]
 [49 64 81]]

[[ 1  4  9]
 [16 25 36]
 [49 64 81]]

最后看看面向用户和开发者的 __repr__ 和 __str__ 作用在数组对象 arr 上。

arr.__repr__()
'array([[1, 2, 3],\n [4, 5, 6],\n [7, 8, 9]])'
arr.__str__()
'[[1 2 3]\n [4 5 6]\n [7 8 9]]'

这时 __repr__ 和 __str__ 两种方法的输出有些区别了。前者比后者多了个 array(),如果把它们打印出来,得到

print( arr.__repr__() )
print( arr.__str__() )
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

[[1 2 3]
 [4 5 6]
 [7 8 9]]

很明显,前者输出非常精确没有歧义,就是指的 array 类型;而后者输出看起来像是列表类型,但无所谓,这只是 ndarray 的打印形式,可读性强就够了。

现在大概了解 __repr__ 和 __str__ 的区别了吧,前者准确无歧义,后者可读性强。

1.4

Pandas 数据帧 - dataframe

数据帧类和对象

本节来体会数据帧 pandas dataframe 类的属性。

# dataframe 对象
import pandas as pd
df = pd.DataFrame( np.arange(6).reshape(2,3), 
                  columns=list('abc') )
df

用 dir(df) 或 dir(pd.DataFrame) 可以浏览到列表下的属性。

检查 df 的类型。

# dataframe 的类型
type(df)
pandas.core.frame.DataFrame

字段和方法

# dataframe 的 fields
df.columns
Index(['a', 'b', 'c'], dtype='object')

用字段 columns 返回 df 的栏标签,['a', 'b', 'c']

# dataframe 的 methods
df.cumsum()

用方法 cumsum 沿着行累加,因为该方法默认 axis = 0。

魔法方法

# dataframe 的 +
print( df + df )
print( '\n' )
print( df.__add__(df) )
   a  b   c
0  0  2   4
1  6  8  10

   a  b   c
0  0  2   4
1  6  8  10
print( df * df )
print( '\n' )
print( df.__mul__(df) )
   a   b   c
0  0   1   4
1  9  16  25

   a   b   c
0  0   1   4
1  9  16  25

最后看看面向用户和开发者的 __repr__ 和 __str__ 作用在数据帧对象 df 上。

df.__repr__()
' a b c\n0 0 1 2\n1 3 4 5'
df.__str__()
' a b c\n0 0 1 2\n1 3 4 5'

两者输出一样,都把数据帧的特点 (有行标签和列标签) 表现出来了。

print( df.__repr__() )
print( df.__str__() )
   a  b  c
0  0  1  2
1  3  4  5
   a  b  c
0  0  1  2
1  3  4  5

2

面向对象编程

上节体验了四个 Python 内置类别:int, list, ndarray 和 dataframe。本节来以「雇员」为场景,来学习如何构建类、并弄清类的四大特征:封装抽象继承多态

2.1

极简的类和对象

定义类必须要用关键词 class,后面跟着类的名字,最后以冒号结尾。

下面是一段定义 Employee 类的代码,如果在该类里什么都不定义的话,用关键词 pass

class Employee:
    pass

emp_1 = Employee()
emp_2 = Employee()

print(emp_1)
print(emp_2)
<__main__.Employee object at 0x00000296C4337B38>
<__main__.Employee object at 0x00000296C43377F0>

接着创建两个 Employee 类的对象 emp_1 和 emp_2,打印它们的信息包含对象储存的位置,但也证实对象创建成功,只不过可读性极差 (后面会改进)。

2.2

__init__() 和 self

上节的类里是空的,实际的类是将属性聚集的,用的就是 __init__ 方法。这种将属性聚在一起称作封装 (encapsulation),这是类的第一个特征。

调用 __init__ 方法就是在构建类的实例,即对象。

注意到 __init__ 方法第一个参数永远是 self,表示在创建对象本身;后面的参数都是 Employee 字段 (fields),常见的赋值方式就是

self.fld_name = fld_name

在上例要定义 Employee 时,我们只用到名 (first)、姓 (last) 和薪水 (pay),而邮件 (email) 可以由姓和名来生成。

first.last@gmail.com

如何用 __init__ 方法 + self 来常见对象的语法总结如下:

接着创建两个 Employee 的对象,分别将 first, last 和 pay 传入类中,注意它们就是 __init__ 方法中跟着 self 的三个参数。

最后打印出两个雇员的全名,Steven Wang 和 Sherry Zhang。但是每次打印全名都要写重复代码,我们其实可以把这个操作定义在 Employee 类里面,作为一个 fullname 的方法 (见下图第 9-10 行),同样第一个参数是 self,因为该函数也需要用自身的 first 和 last 字段。

这时只用调用对象里 fullname 方法就可以打印全名了,语法是

对象.方法()

此外,还可以用类来调用 fullname 方法,语法是

.方法( 对象 )

综上两种调用方法的语法是等价的

对象.方法()

.方法( 对象 )

回想一下小节 1.3 里调用 sum 方法的语法

arr.sum()

np.sum( arr )

虽然第二种语法更符合类中的方法定义,但第一种语法更简洁些,因此用的比较多。

2.3

类变量 (千人千面)

雇员每年要加薪,假设增幅为 5%。我们可以定义一个函数 apply_raise 专门计算增完之后的薪水 (见下图第 12-13 行)。

测试一下,Steven 加薪前年薪是 200000,加薪后年薪是 210000,5% 的涨幅。

更好的方法是定义一个类变量 (class variable),命名为 raise_rate,见下图第 3 行。

在上图第 15 行用 self.raise_rate 来替代 1.05 。测试结果和上面一样。

类变量可以用类 (Employee) 和对象 (emp_1, emp_2) 来访问,结果都是 1.05。

print( Employee.raise_rate )
print( emp_1.raise_rate )
print( emp_2.raise_rate )
1.05
1.05
1.05

如果你想查看一个类或一个对象的详细信息,可以用 __dict__ 方法。注意下图蓝色高亮处,在 Employee 类下有 raise_rate 类变量,其值为 1.05。

再看看 emp_1 对象的详细信息,奇怪怎么没有 raise_rate 字段呢?而上面的确可以用 emp_1.raise_rate 来访问它啊。原因是当 emp_1 找不到类变量的字段,就会继续向其对应的类里找。

如果通过类访问来改变类变量 raise_rate ,那么类和对象下的 raise_rate 值都会变。

Employee.raise_rate = 1.1

print( Employee.raise_rate )
print( emp_1.raise_rate )
print( emp_2.raise_rate )
1.1
1.1
1.1

如果通过对象 emp_1 访问来改变类变量 raise_rate ,那么只会是对象 emp_1下的 raise_rate 值会变,而类下的和对象 emp_2 的 raise_rate 值不会变。

emp_1.raise_rate = 1.05

print( Employee.raise_rate )
print( emp_1.raise_rate )
print( emp_2.raise_rate )
1.1
1.05
1.1

在改变 emp_1.raise_rate 后,这时 emp_1 下有 raise_rate 字段,而 emp_2 下没有,因此当用 emp_2 来访问 raise_rate 时,其实是用类 Employee 来访问的。

总结:如果想让类变量千人千面,用

self.类变量

2.4

类变量 (千人一面)

类变量 - 薪水增幅 - 对于不同对象有不同的值,有没有一种类变量,对于所有对象都有相同的值?有的,比如雇员总数 (见下面第 3 行),因为用任何对象来访问雇员总数,得到的肯定是相同的值。

重点在第 12 行,当用 __init__ 方法创建一个对象时,雇员总数就要增加一个,即

Employ.num_of_emps += 1

在没创建任何之前,雇员总数为 0;加了 Steven Wang 后,雇员总数为 1;加了 Sherry Zhang 后,雇员总数为 2。

总结:如果想让类变量千人一面,用

类名.类变量

2.5

类方法 + 静态方法

到目前为止,类里的方法都是实例方法 (instance method),它们都适用于对象。本节介绍类方法 (class method) 和静态方法 (static method)。先看下图,第 20 -22 行是类方法,用来统一增长薪水;第 24 -28 行是静态方法,用来判断某一天是否是工作日。

类方法

类方法适用于该类,即对该类下的所有对象的作用的相同的。类方法有两个特点:

  1. 第一行要有装饰器 @classmethod (记住就行了)
  2. 函数第一个参数必须是关键词 clf (对象一个参数必须是关键词 self

创建两个雇员,查看它们的 raise_rate 都是 1.05。

emp_1 = Employee( 'Steven', 'Wang', 200000 )
emp_2 = Employee( 'Sherry', 'Zhang', 100000 )

print( Employee.raise_rate )
print( emp_1.raise_rate )
print( emp_2.raise_rate )
1.05
1.05
1.05

用类 Employee 来调用类方法,将薪水涨幅调成 1.1,所有用类和对象来访问的 raise_rate 都变成了 1.1。

Employee.set_raise_rate( 1.1 )

print( Employee.raise_rate )
print( emp_1.raise_rate )
print( emp_2.raise_rate )
1.1
1.1
1.1

用对象 emp_1来调用类方法,将薪水涨幅调成 1.2,所有用类和对象来访问的 raise_rate 都变成了 1.2。

emp_1.set_raise_rate( 1.2 )

print( Employee.raise_rate )
print( emp_1.raise_rate )
print( emp_2.raise_rate )
1.2
1.2
1.2

总结:类方法是所有对象和类都能调用,而且产生的效果是一样。

静态方法

一个类还会有些效用函数 (utility function),它们不随对象和类的属性而改变,因此我们称它们为静态方法

静态方法也有两个特点:

  1. 第一行要有装饰器 @staticmethod (记住就行了)
  2. 函数参数绝对不能有关键词 clf self

运行下上面定义的静态方法 is_workday。

import datetime

my_date = datetime.date(2019, 10, 2)
print( Employee.is_workday(my_date) )
True

2.6

其他构建函数

回顾一下构建雇员对象的代码。

emp_1 = Employee( 'Steven', 'Wang', 200000 )

每次都需要传三个参数,first, last 和 pay。如果我们拿到的数据是一个完整的字符串呢?比如是

'Steven-Wang-200000'

我们需要重新再定义 __init__ 方法,为了接受这样的字符串来创建对象吗?不用,我们可以聪明的利用一下类方法来实现上述功能 (见下图 24-27 行)。

类方法 from_string 的代码很简单,第一步将字符串按分隔符 '-' 拆分,然后用 clf 来创建对象。想想 clf 代表 Employee 类就容易了。

测试一下,没毛病。

2.7

继承和多态

继承 (inheritance) 是类的一大特征。有继承就必有父类 (parent class) 和子类 (child class)。

首先看父类 Employee。

下面构建雇员 Employee 的两个子类,开发者 Developer 和经理 Manager。由父类衍生出来多个子类称为多态 (polymorphism)。

Poly 和 many 意思相近,多的意思;morphism 和 form 意思相近,形态的意思。多态也是类的一大特征。

先看看开发者的例子。

子类 Developer

class Developer(Employee):
    pass

继承写子类的语法如下:

构建两个开发者,虽然在 Develop 类里没有定义任何操作,但是他们继承了父类 Employee 里面的邮箱地址的字段。

再叫一招,如果你想查看一个类里面详细内容的话,可以用 help(class)。如下图所示,注意灰色高亮处的 Method resolution order。意思就是

  • Object 是 Employee 的父类
  • Employee 是 Developer 的父类

Developer 类下的对象可以调用所有父类里的方法或访问所有父类里的字段。

验证一下,Developer 类里啥都没做,但 Employee 类里确实用 pay 字段和 apply_raise() 方法。

print(dev_1.pay)
dev_1.apply_raise()
print(dev_1.pay)
200000
210000

Developer 类里啥都没做简直太过分了,至少也设置一个薪水涨幅嘛。开发者薪水涨幅都高,设 raise_rate 为 1.1,这样 Developer 就再也不会用父类 Employee 里的 1.05 了。

class Developer(Employee):
    
    raise_rate = 1.1

验证一下果真如此,涨幅是 1.1 不是 1.05。

dev_1 = Developer( 'Steven', 'Wang', 200000 )

print(dev_1.pay)
dev_1.apply_raise()
print(dev_1.pay)
200000
220000

现在完善子类 Developer,但是 __init__ 方法里面的代码重复。

怎么精简?用 super 方法。见图第 6 行的 super().__init__() 就是在利用父类里的对象构造函数,而我们只用处理对于 Developer 对象的新字段 - 编程语言 (prog_lang)。

测试一下,没问题。

子类 Manager

开发者是雇员,经理也是雇员。但经理可以管理开发者,因此它的构造函数 __init__ 有一个参数是 employee,初始值为 None。为什么不用空列表 [] 当初始值呢?原因就是列表是可变的 (mutable)。

除此之外,经理还可以增加 (add_emp) 和减少 (remove_emp) 自己管理开发者的名单。

首先创建经理 Jack Black,自带管理 Steven Wang。

经理新管理 Sherry Zhang。

mgr_1.add_emp( dev_2 )
mgr_1.print_emp()
--> Steven Wang
--> Sherry Zhang

经理不再管理 Steven Wang。

mgr_1.remove_emp( dev_1 )
mgr_1.print_emp()
--> Sherry Zhang

两种检查函数

函数 isinstance(a, A) 是检查 a 是不是 A 的一个实例。

print( isinstance(mgr_1, Manager) )
print( isinstance(mgr_1, Employee) )
print( isinstance(mgr_1, Developer) )
True
True
False

很明显,mgr_1 对象是 Manager 类的一个实例,也是父类 Employee 的一个实例,但是不是 Developer 类的一个实例,即便 Developer 的父类也是 Employee。

print( isinstance(dev_1, Developer) )
print( isinstance(dev_1, Employee) )
print( isinstance(dev_1, Manager) )
True
True
False

同样的,dev_1 对象是 Developer 类的一个实例,也是父类 Employee 的一个实例,但是不是 Manager 类的一个实例,即便 Manager 的父类也是 Employee。

函数 issubclass(A, B) 是检查 A 是不是 B 的一个子类。

print( issubclass(Manager, Employee) )
print( issubclass(Developer, Employee) )
print( issubclass(Employee, Developer) )
print( issubclass(Employee, Manager) )
print( issubclass(Manager, Developer) )
print( issubclass(Developer, Manager) )
True
True
False
False
False
False

太简单了,不解释了。

2.8

魔法方法

魔法方法 (magic method) 的方法名前后被双下划线 (dunder) 所包围,构造函数 __init__ 就是最常见的魔法方法。

打印 emp_1 能得到一些难懂的信息,但是用 print() 到底调用的是 Employee 类里哪一个方法呢?

emp_1 = Employee( 'Steven', 'Wang', 200000 )
print( emp_1 )
<__main__.Employee object at 0x00000296C4F8EF60>

其实所有类都是 object 类的子类,而 object 类里有两个重要的魔法方法,__repr__ 和 __str__,任何 object 的子类都会继承这两个方法。

  1. 如果 Employee 中实现了 __str__,那么 print() 函数打印出来的是 __str__ 方法里的内容。
  2. 如果 Employee 中没实现 __str__ 但实现了 __repr__,那么 print() 函数打印出来的是 __repr__方法里的内容。

那么为了让 Employee 对象的打印出来信息更有用或者可读性更强,我们需要「用心」实现 __repr__ 和 __str__ 这两种方法。

__repr__()

该方法是给开发者用的,因此输出应该是准确而无歧义的。见下图第 17-18 行代码。我们希望能打印出来的字符串就能直接用于构建 Employee 对象。

这时我们已经实现了 __repr__ 方法,因此用 print() 可打印出第 18 行的内容。

emp_1 = Employee( 'Steven', 'Wang', 200000 )
print( emp_1 )
Employee('Steven', 'Wang', 200000)

这样开发者可以直接复制结果来构建对象了。试想某开发者敲入代码 emp_1 = 后,然后再复制 + 粘贴,这不就构建了 emp_1 对象了吗?

__str__()

该方法是给用户用的,因此输出应该是可读性强的。见下图第 20-21 行代码。我们希望能打印出来的字符串是人话。

我们希望输出信息包含雇员的全名和邮箱地址。

emp_1 = Employee( 'Steven', 'Wang', 200000 )
print( emp_1 )
Steven Wang - Steven.Wang@gmail.com

最后在对比一下开发者用的 __repr__ 和用户用的 __str__ 的输出。

print( emp_1.__repr__() )
print( emp_1.__str__() )
Employee('Steven', 'Wang', 200000)
Steven Wang - Steven.Wang@gmail.com

__add__()

魔法方法 __add__ 重载了二元运算符 +。

我们知道如何去相加两个整数,甚至两个字符串,但是怎么相加两个 Employee 呢?这个需要我们自己来定义,一个实际的加法操作是将两个雇员你的薪水相加,如下图第 23-24 行。

在定义 __add__ 方法之前。程序报错,正常,因此确实不知道如何将 emp_1 和 emp_2 相加。

在定义 __add__ 方法之后。相加的结果就是薪水相加的结果,200000 + 100000 = 300000.

print( emp_1 + emp_2 )
300000

2.9

属性装饰器

首先看个简单例子,定义 Employee 只有字段 first, last, pay,只有方法 fullname()。

打印两个雇员 Steven Wang 和 Sherry Zhang 的信息,没问题。

emp_1 = Employee( 'Steven', 'Wang', 200000 )
emp_2 = Employee( 'Sherry', 'Zhang', 100000 )

print( emp_1.first )
print( emp_1.email )
print( emp_1.fullname() )
Steven
Steven.Wang@gmail.com
Steven Wang

现在如果把 Steven Wang 的名改成 Tracy,也没问题,除了邮箱地址中的名还是 Steven。这是怎么回事呢?

emp_1.first = 'Tracy'

print( emp_1.first )
print( emp_1.email )
print( emp_1.fullname() )
Tracy
Steven.Wang@gmail.com
Tracy Wang

原因就是 email 已经在构建 emp_1 是就已经定好了,即在运行 __init__ 方法时就已经定好了。而 fullname() 里面的 self.first 和 self.last 每次都可以获取更新后的 first 和 last。

首先用笨方法,像 fullname() 那样定义 email(),见下图第 8-9 行。

问题解决了,但是改过之后别人需要改代码。原来获取 email 用的语法是emp_1.email,现在获取 email 用的语法是 emp_1.email()。注意多了个括号 ()。

emp_1 = Employee( 'Steven', 'Wang', 200000 )

print( emp_1.first )
print( emp_1.email() )
print( emp_1.fullname() )

emp_1.first = 'Tracy'

print( emp_1.first )
print( emp_1.email() )
print( emp_1.fullname() )
Steven
Steven.Wang@gmail.com
Steven Wang
Tracy
Tracy.Wang@gmail.com
Tracy Wang

接下来我们用聪明方法,即属性装饰器 (property decorator)。

见上图第 8 和 12 行,只用加一句 @property。这样所有方法都可以当成属性用,即调用它们时不用打括号了 (注意下面第 4-5 行的代码)。

emp_1 = Employee( 'Steven', 'Wang', 200000 )

print( emp_1.first )
print( emp_1.email )
print( emp_1.fullname )

emp_1.first = 'Tracy'

print( emp_1.first )
print( emp_1.email )
print( emp_1.fullname )
Steven
Steven.Wang@gmail.com
Steven Wang
Tracy
Tracy.Wang@gmail.com
Tracy Wang

现在如果突发奇想改全名 (fullname),把 Steven Wang 改成 Tracy Mcgrady。结果报错,因为改全名会同时影响名 (first) 和姓 (last)。

这时我们需要用装饰器来定义 setter 方法,语法为 @fullname.setter。实现逻辑很简单,讲全名分拆成名和姓,然后再赋值给 self.first 和 self.last。

测试一下,没问题。

emp_1 = Employee( 'Steven', 'Wang', 200000 )

print( emp_1.first )
print( emp_1.email )
print( emp_1.fullname )

emp_1.fullname = 'Tracy Mcgrady'

print( emp_1.first )
print( emp_1.email )
print( emp_1.fullname )
Steven
Steven.Wang@gmail.com
Steven Wang
Tracy
Tracy.Mcgrady@gmail.com
Tracy Mcgrady

按同样的思路,用 @fullname.delete 来实现删除方法。

测试一下,成功删除 Steven Wang。

emp_1 = Employee( 'Steven', 'Wang', 200000 )
print( emp_1.fullname )
del emp_1.fullname
print( emp_1.fullname )
Steven Wang
Delete Name: Steven Wang
None None

3

总结

一言以蔽之,类是描述,对象是实例。先有类,才有类的实例 - 对象。当你在创建某个类的实例(对象)之前,这个类必须被定义。

在学习 OOP 之前,我们通过整数、列表、数组和数据帧这些“变量”,来看看它们下面属性,即字段和方法。先从思维上把“变量”转成“对象”。

在学习 OOP 时,我们用雇员为例,学习如何定义类、构建对象、定义类方法和静态方法、继承父类雇员多态出开发者和经理、使用魔法方法、使用属性装饰器。并在中间穿插介绍了类的四大特征:封装抽象继承多态

Stay Tuned!

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-10-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 王的机器 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档