假装用某米赛尔号的角度看Python面向对象编程

33次阅读

共计 8043 个字符,预计需要花费 21 分钟才能阅读完成。

类和对象
下面我们正式创建自己的类, 这里我们使用 Python 自定义某米赛尔号的精灵, 代码如下:
class Elf:
def setName(self, name):
self.name = name
def getName(self):
return self.name
def getInfo(self):
return self
类的定义就像函数定义, 用 class 语句替代了 def 语句, 同样需要执行 class 的整段代码这个类才会生效。进入类定义部分后, 会创建出一个新的局部作用域, 后面定义的类的数据属性和方法都是属于此作用域的局部变量。上面创建的类很简单, 只有一些简单的方法。当捕捉精灵后, 首先要为其起名字, 所以我们先编写函数 setName() 和 getName()。似乎函数中 self 参数有点奇怪, 我们尝试建立具体的对象来探究该参数的作用。
>>> x = Elf()
>>> y = Elf()
>>> x.setName(‘ 小火猴 ’)
>>> y.setName(‘ 皮皮 ’)

>>> x.getName()
小火猴
>>> y.getName()
皮皮

>>> x.getInfo()
<__main__.Elf instance at 0xXXXXXXXX>

>>> y.getInfo()
<__main__.Elf instance at 0xXXXXXXXX>
创建对象和调用一个函数很相似, 使用类名作为关键字创建一个类的对象, 实际上, Elf() 的括号里是可以有参数的, 后面我们会讨论到。我们有两只精灵, 一只是小火猴, 一只是皮皮, 并且对他们执行 getName() , 名字正确返回。观察 getInfo() 的输出, 返回的是包含地址的具体对象信息, 可以看到两个对象的地址, 是不一样的。Python 中的 self 作用和 C++ 中的 *this 指针类似, 在调用 精灵对象 的 setName() 和 getName() 函数时, 函数都会自动把该对象的地址作为第一个参数传入(该信息包含在参数 self 中), 这就是为什么我们调用函数时不需要写 self , 而在函数定义时需要把参数作为第一个参数。传入对象地址是相当必要的, 如果不传入地址, 程序就不知道要访问类的哪一个对象。
类的每个对象都会有各自的数据属性, Elf 类中有数据属性 name, 这是通过 setName() 函数中的语句 self.name = name 创建的。这个语句中的两个 name 是不一样的, 它们的作用域不一样。第一个 name 通过 self 语句声明的作用域是类 Elf() 的作用域, 将其作为对象 x 的数据属性进行存储, 而后面的 name 的作用域是函数的局部作用域, 与参数中的 name 相同。而后面 getName() 函数返回的是对象中的 name。
__init__()方法
从更深层逻辑去说, 我们捕捉到精灵的那一刻应该就有名字, 而并非捕捉后去设置。所以这里我们需要的是一个初始化手段。Python 中的__init__() 方法用于初始化类的实例对象。__init__() 函数的作用一定程度上与 C ++ 的构造函数相似, 但并不等于。C++ 的构造函数是使用该函数去创建一个类的实例对象, 而 Python 执行__init__() 方法时实例对象已被构造出来。__init__()方法会在对象构造出来后自动执行, 所以可以用于初始化我们所需要的数据属性。修改 Elf 类的代码, 代码如下:
class Elf:
def __init__(self, name, gender, level):
self.type = (‘fire’, None)
self.gender = gender
self.name = name
self.level = level
self.status = [10+2*level, 5+1*level, 5+1*level, 5+1*level, 5+1*level, 5+1*level]

# 精灵体力, 攻击, 防御, 特攻, 特防, 速度
def getName(self):
return self.name
def getGender(self):
return self.gender
def getType(self):
return self.type
def getStatus(self):
return self.status
在此处我们增加了几个数据的属性: 性别、等级、能力和精灵属性。连同前面的名字, 都放在__init__()方法进行初始化。数据属性是可以使用任意数据类型的, 小火猴属性是火, 而精灵可能会有俩个属性, 假设小火猴经过两次进化称为烈焰猩猩属性为地面, 火系。为了保持数据类型的一致性, 所以我们使用元组存储, 并让小火猴的第二个属性为 None。由于小火猴的属性是固定的, 所以在__init__() 的输入参数不需要 type。而精灵的能力会随着等级的不同而不同, 所以在初始化中也需要实现这一点。我们创建实例对象测试代码:
>>> x = Elf(‘ 小火猴 ’, ‘male’, 5)
>>> y = Elf(‘ 皮皮 ’, ‘female’, 6) # 这里有错误, 皮皮不是火系, 可以接着往下看, 思考如何改正
>>> print(x.getName(), x.getGender(), x.getStatus() )
小火猴 male [20, 10, 10, 10, 10, 10]

>>> print(y.getName(), y.getGender(), y.getStatus() )
皮皮 female [22, 11, 11, 11, 11, 11]
这时候创建对象就需要参数了, 实际上这是__init__() 函数的参数。__init__() 自动将数据属性进行了初始化, 然后调用相关函数能够返回我们需要的对象的数据属性。
对象的方法
1. 方法引用类的方法和对象的方法是一样的, 我们在定义类的方时程序没有为类的方法分配内存, 而在创建具体实例对象的程序才会为对象的每个数据属性和方法分配内存, 我们已经知道定义类的方法是 def 定义的, 具体定义格式与普通函数相似, 只不过类的方法的第一个参数要为 self 参数。我们可以用普通函数实现对对象函数的引用:
>>> x = Elf(‘ 小火猴 ’, ‘male’, 5)
>>> getStatus1 = x.getStatus
>>> getStatus1()
[20, 10, 10, 10, 10, 10]
虽然看上去似乎是调用了一个普通函数, 但是 getStatus1() 这个函数是引用 x.getStatus() 的, 意味着程序还是隐性地加入了 self 参数。
2. 私有化先敲代码:
>>> x.type
(‘fire’, None)
>>> x.getType()
(‘fire’, None)
虽然这样似乎很方便, 但违反了类的封装原则。对象的状态对于类外部应该是不可以访问的。为何要这样做, 我们查看 Python 的模块源码时会发现源码里定义了很多类, 模块中算法通过使用类是很常见的, 如果我们使用算法时能随意访问对象中的数据属性, 那么很有可能在不经意间修改算法中已经调好的参数, 这是十分尴尬的。尽管我们不会可以那么去做, 但这种无意的改动是常有的事。一般封装好的类都会有足够的函数接口供程序员用, 程序员没有必要访问对象的具体数据类型。
为防止程序员无意间修改了对象的状态, 我们需要对类的数据属性和方法进行私有化。Python 不支持直接私有方式, 但可以使用一些小技巧达到私有特性的目的。为了让方法的数据属性或方法变为私有, 只需要在它的名字前面加上双下划线即可, 修改 Elf 类代码:
# 自定义类
class Elf:
def __init__(self, name, gender, level):
self.__type = (‘fire’, None)
self.__gender = gender
self.__name = name
self.__level = level
self.__status = [10+2*level, 5+1*level, 5+1*level, 5+1*level, 5+1*level, 5+1*level]

# 精灵体力, 攻击, 防御, 特攻, 特防, 速度
def getName(self):
return self.__name
def getGender(self):
return self.__gender
def getType(self):
return self.__type
def getStatus(self):
return self.__status
def level_up(self):
self.__status = [s+1 for s in self.__status]
self.__status[0] += 1 # HP 每级增加 2 点, 其余增加 1 点
def __test(self):
pass
>>> x = Elf(‘ 小火猴 ’, ‘male’, 5)
>>> print(x.type)
Traceback (most recent call last):
File “seer.py”, line 25, in <module>
print(x.type)
AttributeError: ‘Elf’ object has no attribute ‘type’
>>> print(x.getName())
小火猴
>>> x.test()
Traceback (most recent call last):
File “ser.py”, line 28, in <module>
x.test()
AttributeError: ‘Elf’ object has no attribute ‘test’
现在在程序外部直接访问私有数据是不允许的, 我们只能通过设定好的节后函数去调取对象信息。不过通过双下划綫实现的私有实际上是 ” 伪私有化 ”, 实际上我们还是可以做到从外部访问这些私有属性。
>>> print(x._Elf__type)
(‘fire’, None)
Python 使用的是一种 name_mangling 技术, 将__membername 替换成 _class__membername, 在外部使用原来的私有成员时, 会提示无法找到, 而上面执行的 x.Elf__type 是可以访问。简而言之, 确保其他人无法访问对象的方法和数据属性是不可能的, 但是使用这种 name_mangling 技术是一种程序员不应该从外部访问这些私有成员的强有力信号。
可以看到代码中还增加了一个函数 level_up(), 这个函数用于处理精灵升级是能力的提升, 我们不应该在外部修改 x 对象的 status , 所以应准备好接口去处理能力发生变化的情景, 函数 level_up() 仅仅是一个简单的例子, 而据说在工业代码中, 这样的函数接口是大量的, 程序需要对它们进行归类并附上相应的文档说明。
3. 迭代器 Python 容器对象 (列表、元组、字典和字符串等) 都可以可以用 for 遍历,
for element in [1, 2, 3]:
print(element)
这种风格十分简洁, for 语句在容器对象上调用了 iter(), 该函数返回一个定义了 next() 方法的迭代器对象, 它在容器中逐一访问元素。当容器遍历完毕, __next__() 找不到后续元素时, next() 找不到后续元素时, next()会引发一个 StopIteration 异常, 告知 for 循环终止。
>>> L = [1, 2, 3]
>>> it = iter(L)
>>> it
<list_iterator object at 0x0302C530>
>>> it.__next__()
1
>>> it.__next__()
2
>>> it.__next__()
2
>>> it.__next__()
3
>>> it.__next__()
Traceback (most recent call last):
File “<stdin>”, line 1, in <module>
StopIteration
当知道迭代器协议背后的机制后, 我们便可以吧迭代器加入到自己的类中。我们需要定义一个__iter__()方法, 它返回一个有 next() 方法的对象, 如果类定义了 next(), __iter__()可以只返回 self, 再次修改类 Elf 的代码, 通过迭代器能输出对象的全部信息。
class Elf:
def __init__(self, name, gender, level):
self.__type = (‘fire’, None)
self.__gender = gender
self.__name = name
self.__level = level
self.__status = [10+2*level, 5+1*level, 5+1*level, 5+1*level, 5+1*level, 5+1*level]
self.__info = [self.__name, self.__type, self.__gender, self.__level, self.__status]
self.__index = -1

# 精灵体力, 攻击, 防御, 特攻, 特防, 速度
def getName(self):
return self.__name

def getGender(self):
return self.__gender

def getType(self):
return self.__type

def getStatus(self):
return self.__status

def level_up(self):
self.__status = [s+1 for s in self.__status]
self.__status[0] += 1

def __iter__(self):
print(‘ 名字 属性 性别 等级 能力 ’)
return self

def next(self):
if self.__index == len(self.__info) – 1:
raise StopIteration
self.__index += 1
return self.__info[self.__index]
继承
面向对象编程的好处之一就是代码的复用, 实现这种重用的方法之一就是通过继承机制。继承是两个类或多个类之间的父子关系, 子类继承了基类的所有公有数据属性和方法, 并且可以通过编写子类的代码扩充子类的功能。可以说, 如果人类可以做到儿女继承了父母的所有才学并加以拓展, 那么人类的发展至少是现在的数万倍。继承实现了数据属性和方法的重用, 减少了代码的冗余度。
那么我们如何实现继承呢??如果我们需要的类中具有公共的成员, 且具有一定的递进关系, 那么就可以使用继承, 且让结构最简单的类作为基类。一般来说, 子类是父类的特殊化, 如下关系:
哺乳类动物 ————> 猫科动物 ————> 东北虎
东北虎类 继承 猫科动物类, 猫科动物类 继承 哺乳动物类, 猫科动物类编写了所有猫科动物公有的行为的方法而特定猫类则增加了该猫科动物特有的行为。不过继承也有一定弊端, 可能基类对于子类也有一定特殊的地方, 如果某种特定猫科动物不具有绝大多数猫科动物的行为, 当程序员吗,没有理清类之间的关系是, 可能使子类具有不该有的方法。另外, 如果继承链太长的话, 任何一点小的变化都会引起一连串变化, 我们使用的继承要注意控制继承链的规模。
继承语法: class 子类名(基类名 1, 基类名 2,…), 基类卸载括号里, 如果有多个基类, 则全部写在括号里, 这种情况称为多继承。在 Python 中继承有以下一些特点:
1) 在继承中积累初始化方法__init__()函数不会被自动调用。如果希望子类调用基类的__init__() 方法, 需要在子类的 __init__() 方法中显示调用它。这与 C ++ 差别很大。
2) 在调用基类的方法时, 需要加上基类的类名前缀, 且带上 self 参数变量, 注意在类中调用该类中定义的方法时不需要 self 参数。
3) Python 总是首先查找对应类的方法, 如果在子类中没有对应的方法, Python 才会在继承链的基类中按顺序查找。
4) 在 Python 继承中, 子类不能访问基类的私有成员。
这是最后一次修改类 Elf 的代码:
class pokemon:
def __init__(self, name, gender, level, type, status):
self.__type = type
self.__gender = gender
self.__name = name
self.__level = level
self.__status = status
self.__info = [self.__name, self.__type, self.__gender, self.__level, self.__status]
self.__index = -1

# 精灵体力, 攻击, 防御, 特攻, 特防, 速度
def getName(self):
return self.__name

def getGender(self):
return self.__gender

def getType(self):
return self.__type

def getStatus(self):
return self.__status

def level_up(self):
self.__status = [s+1 for s in self.__status]
self.__status[0] += 1

def __iter__(self):
print(‘ 名字 属性 性别 等级 能力 ’)
return self

def next(self):
if self.__index == len(self.__info) – 1:
raise StopIteration
self.__index += 1
return self.__info[self.__index]

class Elf(pokemon):
def __init__(self, name, gender, level):
self.__type = (‘fire’, None)
self.__gender = gender
self.__name = name
self.__level = level
self.__status = [10+2*level, 5+1*level, 5+1*level, 5+1*level, 5+1*level, 5+1*level]
pokemon.__init__(self, self.__name, self.__gender, self.__level, self.__type, self.__status)
>>> x = Elf(‘ 小火猴 ’, ‘male’, 5)
>>> print(x.getGender())
male
>>> for info in x:
print(info)
小火猴 (‘fire’, None) male 5 [20, 10, 10, 10, 10, 10]
我们定义了 Elf 类的基类 pokemon, 将精灵共有的行为都放到基类中, 子类仅仅需要向基类传输数据属性即可。这样就可以很轻松地定义其他基于 pokemon 类的子类。因为某米赛尔号精灵有数千只, 使用继承的方法可以大大减少代码量, 且当需要对全部精灵进行整体改变时仅需改变 pokemanl 类的__init__()即可, 并向基类传输数据, 这里注意要加 self 参数, Elf 类没有继承基类的私有数据属性, 因此在子类只有一个 self.__type, 不会出现因继承所造成的重名情况。为了能更加清晰地描述这个问题, 这里再举一个例子:
class animal:
def __init__(self):
self.__age = age
def print2(self):
pritn(self.__age)

class dog(animal):
def__init__(self, age):
animal.__init__(self, age)
def print2(self):
print(self.__age)
>>> a_animal = animal(10)
>>> a_animal.print2()
10
>>> a_dog = dog(10)
>>> a_dog.print2()
Traceback (most recent call last):
File “seer.py”, line 13, in <module>
a_dog.print2()
File “seer.py”, line 11, in print2
print(self.__age)
AttributeError: ‘dog’ object has no attribute ‘_dog__age’
That’s all !

正文完
 0