跳转至

2.Python语法巩固

​ 虽然本教程已经默认读者已经掌握了基本的Python语法,但磨刀不误砍柴工,有必要对于常用的Python语法进行回顾,特别是加强面向对象程序设计的意识和思维。

​ 在本章中的例子可能会用到一些Spigot API,但是不了解也无伤大雅,只需理解其中的Python语法即可。

2.1 编码问题

​ 在1.1.3中,我们在PySpigot尝试运行了程序。但是如果我们现在把内容换为中文会如何?

1
2
3
4
5
6
7
import pyspigot as ps

def onCommand(sender, label, args):
    sender.sendMessage('悲惨世界')
    return True

ps.command.registerCommand(onCommand, 'hello')

​ 加载脚本/pyspigot load hello.py后报错:

1
2
[16:23:26 ERROR]: [PySpigot/test.py] SyntaxError: Non-ASCII character in file 'test.py', but no encoding declared; see http://www.python.org/peps/pep-0263.html for details
[16:23:26 ERROR]: [PySpigot/test.py] Script unloaded due to a runtime error.

​ 在Python 2.7中默认的编码格式是ASCII,所以在未特别说明编码格式时无法正确的使用汉字和其他字符。而Python 3采用UTF-8,则没有这个困扰。所以请开发时特别注意要按照以下形式编写:

  1. 开头指定编码#coding: utf-8
  2. 含或可能含中文时,需要写成形如u'string'的形式;
  3. print()等Python打印方法中含中文时,一般还需要对字符串进行.encode('utf-8'),完整为:print(u'你真棒'.encode('utf-8'))

​ 修改我们先前的脚本,得到:

1
2
3
4
5
6
7
8
#coding: utf-8
import pyspigot as ps

def onCommand(sender, label, args):
    sender.sendMessage(u'悲惨世界') # 字符串前加u代表unicode
    return True

ps.command.registerCommand(onCommand, 'hello')

​ 重载脚本/pyspigot reload hello.py后正常使用,问题解决。

2.2 变量和运算

​ 在变量类型和运算部分,着重提醒学习Python 3的读者几点与Python 2.7的不同。

  1. Python 2.7中有Python 3没有的长整型变量,在isinstance等判断时需要注意不要忘记可能的"long"类型;
  2. Python 2.7中str不包括刚才编码问题所提到的加“u”的内容,加“u”后变量类型为unicode,可用它们的基类basestring来判断(但编辑器可能不识别,可用#type: ignore忽略);
  3. 除法整数除整数只能得到整数,想正常得到结果必须确保其中一个数为浮点数float,但为了以后版本的兼容请不要刻意使用本“特性”。
1
2
3
4
5
6
>>> 1/2
0
>>> 1.0/2
0.5
>>> 1/float(2)
0.5

2.3 条件和循环语句

2.3.1 if条件语句

​ 在if条件语句中,很多初学者喜欢嵌套if结构。设计一个示例程序:检测是否有玩家在world空手右键在(114, 51, 4)的方块,如果有则打印其名字。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#coding: utf-8
import pyspigot as ps
from org.bukkit import Bukkit, Location
from org.bukkit.event.player import PlayerInteractEvent

# 玩家交互事件
def onPlayerInteract(e):
    if not e.isCancelled(): # 判断是否被取消
        if not e.hasItem(): # 判断是否空手
            if e.hasBlock() and e.getClickedBlock().getLocation() == Location(Bukkit.getWorld(), 114, 5, 14): # 判断是否有方块,且在world的(114, 51, 4)
                print(u'有,{}点击了'.format(e.getPlayer().getName()).encode('utf-8'))
            else:
                print(u'位置不对,跳出'.encode('utf-8'))
        else:
            print(u'没空手,跳出'.encode('utf-8'))
    else:
        print(u'被取消了'.encode('utf-8'))

ps.listener.registerListener(onPlayerInteract, PlayerInteractEvent)

​ 嵌套if在功能上完全不存在任何问题,但是在处理起来仍然不够清晰,阅读起来也不够直观。我们不妨改写为这样的形式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#coding: utf-8
import pyspigot as ps
from org.bukkit import Bukkit, Location
from org.bukkit.event.player import PlayerInteractEvent

# 玩家交互事件
def onPlayerInteract(e):
    if e.isCancelled(): # 判断是否被取消
        print(u'被取消了'.encode('utf-8'))
        return
    if e.hasItem(): # 判断是否空手
        print(u'没空手,跳出'.encode('utf-8'))
        return
    if not e.hasBlock() or e.getClickedBlock().getLocation() != Location(Bukkit.getWorld(), 114, 5, 14): # 没方块,或者方块位置不对
        print(u'位置不对,跳出'.encode('utf-8'))
    print(u'有,{}点击了'.format(e.getPlayer().getName()).encode('utf-8'))

ps.listener.registerListener(onPlayerInteract, PlayerInteractEvent)

​ 实际上在很多情况下,if就是一步步排除不合条件的情况的过程,显然后者利用返回来结束执行的写法更加清晰,且更加具有“排除”意味。这种编程模式叫做卫语句(Guard Clause)。在程序设计中,建议读者广泛运用这种编程方式。

2.3.2 for循环语句

在for循环使用range()

​ for循环语句常用于遍历可循环对象。Python中的for循环更像MATLAB而非C/C++,无需for(i=0; i<arr.length;i++)通过索引来访问数组等,更加适合其他的可迭代对象,如字典、集合。但有时我们又需要展示序号,这就凸显了range函数的重要性。

​ 比如我们有一个地点名单:['主城', '沃克城', '东篱'],如果单独用for我们只能让其依次打印:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#coding: utf-8
for name in [u'主城', u'沃克城', u'东篱']:
    print(u'现在是 {}'.format(name).encode('utf-8'))

'''
运行结果:
现在是 主城
现在是 沃克城
现在是 东篱
'''

​ 但有时我们想按序号排列,除了单独定义一个number变量并在每次循环最后加法外,还可以借助Python的range()函数,创建一个整数列表用于循环(Python 3时这里为一个可迭代对象)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#coding: utf-8
arenas = [u'主城', u'沃克城', u'东篱']
for number in range(arenas):
    print(u'{}. {}'.format(number + 1, arenas[number]).encode('utf-8'))

'''
运行结果:
1. 主城
2. 沃克城
3. 东篱
'''

​ 下面我们来具体介绍range()的用法:

1
2
range(length) # 只输入一个数字时是从0到length个
range(start, stop[, step]) # 从start至stop,步长为step,步长默认为1

​ 在Python 2.7中还存在“xrange()”函数,与Python 3中的range()一致,都是生成迭代器,相较于当前版本的range()可能更节省内存。如有需要请注意未来Python 3版本升级后需要进行的修改。

for循环中的卫语句

​ 在for循环中也可以利用continue和break实现卫语句。比如我们设计一个程序,查找当前所有在线玩家中具有“dcr.trust”权限玩家,并返回为列表。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#coding: utf-8
import pyspigot as ps
from org.bukkit import Bukkit

players = Bukkit.getOnlinePlayers() # 获取所有在线玩家

def getPlayers():
    permit = [] # 用于存储有权限的玩家
    for player in players:
        if player.hasMetadata('NPC'): # 过滤掉NPC
            continue
        if not player.hasPermission('dcr.trust'): # 过滤掉无权限玩家
            continue
        permit.append(player) # 未过滤即符合条件的,加入列表
    return permit

2.4 数据结构

2.4.1 四种常用数据结构

​ 在Python中常用的数据结构有:列表(list)、元组(tuple)、字典(dict)和集合(set)。这些数据类型用于存储和组织多个元素,是 Python 编程中处理数据的基础。在这些数据结构中,又可根据其数据组织形式分为三类:

  • 序列类型(Sequence Types):如 listtuple,用于存储有序的元素集合。
  • 集合类型(Set Types):如 set,用于存储无序且不重复的元素集合。
  • 映射类型(Mapping Types):如 dict,用于存储键值对(key-value)的集合。

​ 这四种数据结构的用途也各不相同,往往根据需求来确定:

  • 使用 list:当你需要一个可变的、有序的元素集合时。类似ArrayList。
  • 使用 tuple:当你需要一个不可变的、有序的元素集合时,常用于函数的返回值。
  • 使用 set:当你需要一个无序的、不包含重复元素的集合时,适用于去重操作。
  • 使用 dict:当你需要通过键快速查找对应值时,适用于构建映射关系。类似HashMap。

​ 这些数据结构是 Python 编程的基石,熟练掌握它们将有助于你更高效地处理和组织数据。其中重点是掌握各种方法,如字典中“in”、“.get()”、“.keys()”、“.items()”的用法。同时也需要掌握“.split()”、“.join()”等字符串与可迭代对象互转方法。

2.4.2 有序字典 OrderedDict

​ 如果你有一个键值对想要存储,即需要字典的快速检索,又需要像列表那样有序,那么可以考虑使用OrderedDict。请学习Python 3.7及以上的读者注意,低版本的字典并不有序。

1
2
3
4
5
6
7
8
from collections import OrderedDict

od = OrderedDict()
od['apple'] = 1
od['banana'] = 2
od['cherry'] = 3

print(str(od))  # 输出: OrderedDict([('apple', 1), ('banana', 2), ('cherry', 3)])

2.4.3 默认值字典 defaultDict

defaultdict 是 dict 的子类,它接受一个工厂函数作为参数,用于为缺失的键提供默认值。它最大的作用是可以自动初始化,从而避免在访问前检查键是否存在的需要,进而简化代码。

1
2
3
4
5
6
7
from collections import defaultdict

dd = defaultdict(list)
dd['fruits'].append('apple')
dd['fruits'].append('banana')

print(str(dd))  # 输出: defaultdict(<class 'list'>, {'fruits': ['apple', 'banana']})

​ 在上面的示例中,dd['fruits'] 在第一次访问时自动创建了一个空列表。这对于需要为每个键维护一个集合或列表的场景非常有用

2.5 函数

​ 对于没有完全理解传参的开发者在前期难免会出现问题,比如对传入的参数使用方法,但发现原值也一同改变。因此,有必要再次强调参数传递的内容。

2.5.1 可变默认参数的陷阱

​ 在 Python 中,函数的默认参数值是在函数定义时计算的,而不是在每次调用函数时重新计算。这意味着如果你使用了可变对象(如列表或字典)作为默认参数,并在函数中修改了它,那么这个修改会影响到后续的函数调用。

1
2
3
4
5
6
def append_to_list(value, my_list=[]):
    my_list.append(value)
    return my_list

print(append_to_list(1))  # 输出: [1]
print(append_to_list(2))  # 输出: [1, 2],不是预期的 [2]

​ 所以,建议初始值使用None,然后在函数内检测和处理。

1
2
3
4
5
def append_to_list(value, my_list=None):
    if my_list is None:
        my_list = []
    my_list.append(value)
    return my_list

​ 这种做法可以避免默认参数值被共享的问题。

2.5.2 传对象引用的传参机制

​ Python 的参数传递机制是“传对象引用”(也称为“共享传参”)。这意味着函数参数是通过对象的引用传递的,而不是对象的副本。因此:

  • 如果传入的是可变对象(如列表、字典),并在函数内部修改了该对象的内容,那么这些修改在函数外部也是可见的。
  • 如果在函数内部重新赋值参数名(例如,将列表参数重新赋值为一个新的列表),那么这种重新赋值不会影响到函数外部的变量。
1
2
3
4
5
6
7
def modify_list(lst):
    lst.append(1)  # 修改原始列表
    lst = [2, 3]   # 重新赋值为新列表

my_list = []
modify_list(my_list)
print(my_list)  # 输出: [1]

​ 在这个例子中,lst.append(1) 修改了原始列表的内容,而 lst = [2, 3] 只是将 lst 重新绑定到一个新的列表对象,不影响 my_list

2.5.3 默认参数不能依赖于其他参数

​ 在前边我们已经提过,在Python中,函数的默认参数值是在函数定义时计算的,而不是在函数调用时。因此,不能在一个参数的默认值中引用另一个参数。同样应该在初始值使用None,然后在函数内检测和处理。

​ 错误示例:

1
2
def func(a, b=a):  # 错误:b 的默认值依赖于 a
    pass

​ 正确方法:

1
2
3
4
def func(a, b=None):
    if b is None:
        b = a
    # 继续处理

2.5.4 参数类型不受限制

​ 与Java等语言不同,Python 是动态类型语言,函数参数不需要声明类型,也不会进行类型检查。这意味着你可以将任何类型的对象作为参数传递给函数。在编程中可以灵活的处理和利用变量。

1
2
3
4
5
def add(a, b):
    return a + b

print(add(2, 3))       # 输出: 5
print(add("a", "b"))   # 输出: 'ab'

​ 但为了使用编辑器的自动补全,建议在编程时尽量将可能的类型注释出来,也能够帮助其他协作者理解。比如我们对上边的函数进行注释:

1
2
3
def add(a, b):
    # type: (int|float|str, int|float|str)
    return a + b

​ 虽然这种注释在Python 2.7还未引入,但注释只需要给编辑器阅读即可,无需程序执行。请不要使用形如from_: str, to: str的格式,因为Jython还不能识别这种新格式。

​ 在先前我们讲过Python 2.7除了str外还有unicode和basestring,但实际上这种注释是在Python 3下才支持的,因此我们只需要使用Python 3的等位类型即可获得自动补全。

​ 参数类型不受限制的特点,可以配合isinstance()来实现参数转化为我们希望的类型,这在PySpigot程序设计当中十分常用。

1
2
3
4
5
6
7
8
9
from collections import Iterable # 可迭代对象

def append_to_list(value, my_list=None):
    if not isinstance(my_list, Iterable): # 卫语句排除不可迭代对象,返回空列表
        return []
    if not isinstance(my_list, list) # 其他可迭代对象先转换为数组
        my_list = list(my_list) 
    my_list.append(value)
    return my_list

2.5.5 善用*args和**kwargs传参

​ 在 Python 中,可以使用 *args**kwargs 来接收可变数量的位置参数和关键字参数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def func(arg, *args, **kwargs):
    print("arg:", arg)
    print("args:", args)
    print("kwargs:", kwargs)

func("a", 1, 2, 3, a=4, b=5)
# 输出:
# arg: a
# args: (1, 2, 3)
# kwargs: {'a': 4, 'b': 5}

​ 在传参时,Python会根据参数个数判断传入的是"arg"还是"*args"。当出现形如"key=value"时,传入kwargs。实际上,直接访问"args"就是一个元组,而"kwargs"就是一个字典。

​ 其中“*”称为“解包运算符”,它可将可迭代对象(如列表、元组、字符串等)解包为单独的元素。这在函数调用、赋值和数据结构合并等场景中非常有用。"**"同样也有类似的用法。

​ 函数调用时解包,将可迭代对象解包为数个参数(最常用)

1
2
3
4
5
def greet(name, age):
    print("Hello, {}. You are {} years old.".format(name, age))

info = ("Alice", 30) # 一个元组
greet(*info)  # 等同于 greet("Alice", 30)

​ 变量赋值中的解包

1
2
3
4
5
numbers = [1, 2, 3, 4, 5]
first, *middle, last = numbers
print(first)   # 输出: 1
print(middle)  # 输出: [2, 3, 4]
print(last)    # 输出: 5

​ 合并可迭代对象

1
2
3
4
5
list1 = [1, 2]
list2 = [3, 4]
combined = [*list1, *list2]
print(combined)  # 输出: [1, 2, 3, 4]
# 当然 [1, 2] + [3, 4] 也是可行的

2.6 异常处理

​ 使用异常处理可以在程序抛出异常时使程序继续运行。捕捉异常可以用“try except”语法。比如在捕捉玩家输入后,判断是否为一个可转为浮点数的字符串(即判断字符串是否是可用的数字)。

1
2
3
4
5
6
7
def isNumber(input):
    # type: (str) -> bool
    try:
        float(input)
    except:
        return False
    return True

​ 如果要打印遇到了何种错误,可以进行如下修改:

1
2
3
4
5
6
7
8
def isNumber(input):
    # type: (str) -> bool
    try:
        float(input)
    except Exception as e:
        print('Encounter error: {}'.format(e))
        return False
    return True

​ 在编写程序时,还可以使用“raise”来自主触发异常,从而提醒开发者和终止程序运行,这对于开发库等具有很大的作用。例如某个函数只允许传入字符串:

1
2
3
4
def toNumber(input):
    if not isinstance(input, basestring):
        raise TypeError('type {} not support'.format(type(input)))
    # 剩余逻辑

​ 异常处理中还有更多的用法,这里只回顾一些基本的使用场景,读者可参看菜鸟教程或其他资料。

2.7 推导式

​ 推导式是一种用简洁语法快速创建可迭代对象(列表、集合、字典)的方式,即“for + 条件判断”自动生成新的可迭代对象。

名称 结果数据类型 基本结构示例
列表推导式 list comprehension list [表达式 for 变量 in 可迭代对象 if 条件]
集合推导式 set comprehension set {表达式 for 变量 in 可迭代对象 if 条件}
字典推导式 dict comprehension dict {键表达式:值表达式 for 变量 in 可迭代对象 if 条件}
生成器表达式 generator expression generator (表达式 for 变量 in 可迭代对象 if 条件)

​ 其中“if 条件”不需要时可以省略。

2.7.1 列表推导式

​ 下面我们来写一个获取全服在线玩家名的推导式

1
2
3
4
5
from org.bukkit import Bukkit
# 所有在线玩家
[player.getName() for player in Bukkit.getOnlinePlayers()]
# 所有在线OP
[player.getName() for player in Bukkit.getOnlinePlayers() if player.isOP()]

​ 推导式一行就完成了一般结构需要多行的内容,我们以所有在线OP为例:

1
2
3
4
5
6
from org.bukkit import Bukkit
lst = []
for player in Bukkit.getOnlinePlayers():
    if not player.isOP():
        continue
    lst.append(player.getName())

2.7.2 字典推导式

​ 字典推导式也比较常用,特别是进行一些筛选。比如筛选所有热度超过16的地区:

1
2
hot = {u'主城': 10, u'沃克城': 50, u'东篱': 55}
meets = {k: v for k, v in hot if v > 16}

​ 对于其中的“k: v”也可以灵活的修改和运用,比如获得一个“地区名: 地区名字数”的字典:

1
2
hot = {u'主城': 10, u'沃克城': 50, u'东篱': 55}
meets = {k: len(k) for k in hot}

2.7.3 集合推导式

​ 集合推导式与字典推导式长相类似,都使用“{}”,但是集合推导式没有“key: value”的形式。比如对列表内的元素去重并平方:

1
2
s = {x**2 for x in [1, 2, 2, 3]}
print(s)  # {1, 4, 9}

​ 有时也会对字典使用集合推导式,但根据目的可能会采用“.items()”或“.keys()”方法。比如获取热度超过16的地区名集合:

1
2
hot = {u'主城': 10, u'沃克城': 50, u'东篱': 55}
meets = {k for k in hot.keys() if v > 16}

内容补充:

​ 类似列表推导式这样一行解决问题的还有“条件表达式”,也叫“三元表达式”,其格式为“A if condition else B”。若条件成立返回A,不成立则B。

2.8 常用内置函数

​ 内置函数在Python 2.7中具有重大作用,这里只挑选几个常用且关键的函数,更多内容可参看菜鸟教程或其他资料。

2.8.1 all()

​ all()函数用于判断可迭代对象中所有元素是否都满足条件。比如在开发DC邮电寄递系统时,物品输入是以元素为ItemStack的列表来实现的。当实现检查是否为空的功能时,不仅要考虑到列表是否无元素,还要考虑到getContents可能返回的元素均为None的空列表。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 等待发送者 用以填写邮寄信息使用
class AwaitSender(object):
    def __init__(self, senderFacility, items, senderName):
        # type: (SenderFacility, list, str) -> None
        # 构造时传入
        self.facility = senderFacility
        self.items = items
        self.senderName = senderName
        # 需用户后续设置
        self.method = None
        self.receiver = None
        self.additionChooser = None
    # 是否为空
    def isEmpty(self):
        return not self.items or all(x is None for x in self.items) # 列表不为空且并非全为None

​ all()接受传入一个可迭代对象,判断其中是否所有元素都为True,即判断是否所有元素都并非0、None、False。上边的例子就是传入了一个生成器表达式,依次取出每个元素x去判断“x is None”,返回True和False,整体来看就当x全为None时则返回True。

​ 与之对立的是any(),用以判断可迭代对象是否全部为False,可类似的进行使用。

2.8.2 isinstance()

​ isinstance()用以判断一个对象是否是一个(些)类或这个(些)类的继承类。

1
isinstance(object, class or tuple) # 对象, 类 或 元素为类的元组

​ 在2.5.4中我们已经示范了一般用法,如果面对判断是否为一种数字的情况,就可以在第二个参数传入一个元组:

1
2
def isNumber(obj):
    return isinstance(obj, (int, long, float)):

2.8.3 max()和min()

​ man()和min()顾名思义,返回给定参数的最大和最小值。这里以max()举例。

​ 最简单的用法就是判断一些数的最大值:

1
print(str(max(1, 14, 51, 4))) # 结果为 51

​ max()同样支持输入一个生成器表达式。下边的例子用于获取列表lst中最长的字符串:

1
2
lst = ['ni', 'hao', 'a']
man_length = max(len(item) for item in lst) # 结果为 3

​ 还可以找到列表中元素满足key表达式的最大值,比如处理下边两个字典“arrTime”中最大值:

1
2
3
train1 = {'name': 'Z1', 'arrTime': 17000}
train2 = {'name': 'D7', 'arrTime': 14000}
max([train1, train2], key=lambda d: d['arrTime']) # 返回train1的字典

2.8.4 sorted()

​ sorted()的作用是对可迭代对象排序,返回一个新的排序后的列表。与列表的.sort()方法不同,sorted()是新生成一个列表,不会修改原列表的数据。

1
2
3
4
sorted(iterable, key=None, reverse=False)
# iterable: 需排序的可迭代对象
# key: 指定排序规则的函数,可选
# 是否反转排序,默认(False)为升序,指定后为降序

​ 还是写一个类似2.8.3的字符串的例子:

1
2
3
words = ['banana', 'apple', 'cherry', 'date']
sorted_words = sorted(words, key=len) # key=len 指用len()得到的数字来排序
print(sorted_words)  # ['date', 'apple', 'banana', 'cherry']

​ 很多熟悉、了解数据结构的编程能手喜欢自己写一个“高端”的排序算法,但是绝大多数情况下直接用Python为我们提供的sorted()才是最高效率的,因为sorted()函数的背后是直接用解释器的语言编写的高效算法,自己在Python实现的算法不可能比它还快。除非编程者在打算法比赛,或者确有不常见需求,否则请不要自行实现其他方法。

2.9 面向对象的程序设计

​ Python是一门面向对象的语言,鉴于PySpigot是运行在Java和Spigot的基础上,掌握面向对象的编程方法就更为重要。如果先前已经具体学习过面向对象的语言,那么Python在编程思路上基本上与它一致,只需要注意语言之间表达的不同形式。

2.9.1 类与对象

定义类

​ 类(Class)是一种抽象,是同一类对象的共同属性和行为进行概括。将抽象出的数据、代码封装在一起,形成类。可以理解为类是一种模板。

​ 定义类的基本格式:

1
2
3
4
5
6
7
8
class ClassName(object): # 类名(基类)
    def __init__(self, 参数1, 参数2):
        self.属性1 = 参数1
        self.属性2 = 参数2
    # 方法
    def 方法1(self):
        # 访问属性或执行操作
        print(self.属性1)

​ 其中“object”代表是“新类”,只有“新类”才具有继承的功能。而Python 3中默认是新类。建议在编程时都使用新类。

​ 需要特别注意,类中的“函数”都是“方法”(method)。在PySpigot内建的功能中,二者具有严格的区分,在要求函数(function)时输入方法的话会无法运行,因此请使用lambda来修饰。

内容补充:

​ lambda是用来创建匿名函数的语法,其格式为:lambda 参数列表 : 表达式,常用于这类小函数。在2.8.3中就有这样的例子。

类成员的访问控制

  • 公有类型成员:类与外部的接口,任何外部函数都可以访问公有类型数据和函数
  • 私有类型成员:只允许本类中访问,外部任何函数都不能访问
  • 保护类型成员:只允许本类和其派生类允许访问
 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
29
30
31
class ClassName(object): # 类名(基类)
    def __init__(self, 参数1, 参数2):
        self.公有 = 参数1 # 公有属性(Public Attributes)
        self._保护 = 参数2 # 受保护属性(Protected Attributes)
        self.__私有 = 参数3 # 私有属性(Private Attributes)
    def 公有方法(self):
        # 访问属性或执行操作
        print(self.属性1)
    def _保护方法(self):
        # 访问属性或执行操作
        print(self._保护)
    def __私有方法(self):
        # 访问属性或执行操作
        print(self.__私有)

class SonClass(ClassName):
    self.公有 = 新参数
    self.公有方法()
    self._保护 = 新参数
    self._保护方法()
    self.__私有 = 新参数 # 不允许!
    self.__私有方法() # 不允许!

class AnotherClass(object):
    className = ClassName()
    className.公有 = 新参数
    className.公有方法()
    className._保护 = 新参数 # 不允许!
    className._保护方法() # 不允许!
    className.__私有 = 新参数 # 不允许!
    className.__私有方法() # 不允许!

构造函数

​ 类在初始化成对象时,是通过构造函数进行的。构造函数即为“__init__”,其传参等与函数一致。比如设计了一个个人信息类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Profile(object):
    # 构造函数
    def __init__(self, firstName, lastName, gender, birth, homeTown):
        self._firstName
        self._lastName
        self._gender
        self._birth
        self._homeTown
    # 示例方法
    def getFullName(self):
        return '{} {}'.format(self._firstName, self._lastName)

​ 不定义任何构造函数时则使用默认构造函数,是没有参数的构造函数。

对象

​ 对象是类的实例(Instance),由类通过构造函数变为对象是完成了实例化。比如对Profile进行构造,并赋给profile变量:

1
2
profile = Profile('Yoshino', 'Nanjo', 'female', '7/12', 'Shizuoka')
profile.getFullName() # 结果: Yoshino Nanjo

2.9.2 类的继承

​ 类的派生和继承体现了事物普遍联系的观点。现实世界的事物既作为个体存在,同时也作为联系中的事物存在。人们在认识世界的过程中,根据事物所具有的普遍性和特殊性、共性和个性,利用分类等科学方法对其分析和描述。比如,汽车、火车、飞机、轮船等都属于载具,而汽车、火车、飞机、轮船又各有不同的种类。为了减少代码重复程度,便于功能的复用和扩展,我们使用类的继承,使派生类(子类)拥有基类(父类)的属性和方法。

基类和派生类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 基类:动物
class Animal(object):
    def eat(self):
        pass
    def sleep(self):
        pass

# 子类:狗
class Dog(Animal):
    def __init__(self):
        super(Dog, self).__init__() # 利用基类的构造函数
        Animal.__init__(self) # 另一种利用基类构造函数的写法,在多继承中必须使用此类进行明确
    def bark(self): # 狗的方法:吠叫
        pass

​ 在Paper API中,也具备这样的开发思路。我们以Inventory为例:在Minecraft中很多的对象(包括生物、方块)都具有容器,但其大小、形式、功能也不同,并且容器的持有者又有不同的功能,如果每个都单独开发势必会使工作量陡增,同时降低其“互换性”。在Paper API中,通过这样的设计来解决这个问题:

​ 对于不同的容器,都是Inventory的派生,使用isinstance(obj, Inventory)全部返回True。

graph LR
A[Inventory]-->AA[AbstractHorseInventory]-->AAA[HorseInventory]
AA-->AAB[LlamaInventory]
A-->AB[AnvilInventory]
A-->AC[DoubleChestInventory]
A-->AD[CraftingInventory]
A-->AE[FurnaceInventory]
A-->AF[PlayerInventory]

​ 对于不同的容器持有者,都是InventoryHolder的派生。

graph LR
A[InventoryHolder]-->B[AbstractHorse]
A-->C[Container]
C-->Barrel
C-->Chest
C-->Furnace
C-->ShulkerBox
A-->Jukebox
A-->Player
A-->Piglin
A-->D[ChestBoat]
D-->OakChestBoat
D-->SpruceChestBoat

​ 这些InventoryHolder的派生类除了是容器以外,还各自分属于不同的类,即派生类可以继承多个基类。我们以大家最熟悉的Player为例:

graph LR
Attributable-->D
AnimalTamer-->D
Damageable-->D
C[Entity]-->D[HumanEntity]-->B[Player]
InventoryHolder-->D
C-->LivingEntity-->D
CommandSender-->C
Nameable-->C
Permissible-->C
ServerOperator-->C
OfflinePlayer-->B
ProjectileSource-->D

​ 但是在Python 2.7中,多继承可能会遇到除第一个外后边方法无法直接使用的问题,这时可能就需要手动在派生类添加一个方法。

2.9.3 多态性

​ 多态性即为方法名相同,但在不同类的表现不同,也可以叫做“方法重写”。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Animal(object):
    def speak(self):
        raise NotImplementedError("Method speak is not implemented in sub class")

class Dog(Animal):
    def speak(self):
        print("Woof!")

class Cat(Animal):
    def speak(self):
        print("Meow!")

def animal_speak(animal):
    animal.speak()

a = Animal()
d = Dog()
c = Cat()

animal_speak(d)  # 输出:Woof!
animal_speak(c)  # 输出:Meow!
animal_speak(a)  # 错误:NotImplementedError Method speak is not implemented in sub class

​ 这里的Animal类似就类似我们上边介绍的Abstruct类,由于speak功能及其依赖于动物的不同类型,只能预留好这个方法,在派生类开发。这里就用到了前边的异常处理,主动发起了一个NotImplementedError(未实现错误),来提醒继承你开发的基类的开发者有方法未实现。在各自派生类中,由于重写了speak方法,所以可以正确的运行,这就是多态性的体现。有了Abstruct类,就可以更方便的使用isinstance()来判断是否为某一类。

​ 当然,不继承Animal也是可以的,只要都有speak方法即可。这种基于接口的调用,就是典型的鸭子类型(Duck Typing):“如果它走路像鸭子,叫声像鸭子,那它就是鸭子。”

2.9.4 析构函数

​ 析构函数是在类的对象生命周期终止时所运行的函数。一般不需要编写析构函数,仅有特殊需求时使用。

​ 例如Util2中有一个ConfigUtil,是关于配置文件的工具。配置文件的MemorySection需要使用“save()”才能写入文档,那么我们可以使用析构函数来在生命周期结束前进行保存。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 配置文件工具
class ConfigUtil(object):
    def __init__(self, section):
        self._section = section
    # 保存配置文件
    def save(self):
        self._section.save()

# 自动保存的配置文件工具
class ConfigUtilAutoSave(ConfigUtil):
    # 运行析构函数时保存
    def __del__(self):
        self.save()

2.9.5 运算符重载

​ 运算符重载使自定义类与Python运算符(如+、-、*)进行交互,且表现出自定义行为。背后是通过一系列的特殊方法来实现的:

运算符 方法名
+ __add__
- __sub__
* __mul__
/ __truediv__
// __floordiv__
% __mod__
** __pow__
== __eq__
< __lt__
> __gt__

​ 例如我们编写一个二维向量类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Vector(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):  # 重载 +
        return Vector(self.x + other.x, self.y + other.y)

    def __sub__(self, other):  # 重载 -
        return Vector(self.x - other.x, self.y - other.y)

    def __mul__(self, scalar):  # 重载 *
        return Vector(self.x * scalar, self.y * scalar)

    def __str__(self):  # 用于打印 str()
        return "Vector(%s, %s)" % (self.x, self.y)

v1 = Vector(1, 2)
v2 = Vector(3, 4)

print(str(v1 + v2))     # 输出:Vector(4, 6)
print(str(v2 - v1))     # 输出:Vector(2, 2)
print(str(v1 * 10))     # 输出:Vector(10, 20)

​ 当然,这不是必须的,使用对象方法也依然优雅。

章节练习

请使用面向对象的程序设计方法,设计一个满足下述要求的查询列车时刻的程序,示例列车时刻表已给出。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
trains = (
    ("D15","1",u"三界北","21:47/53",u"滴水原","2:00/06",u"辽海","6:17"),
    ("D16","1",u"辽海","22:43",u"滴水原","2:53/3:00",u"三界北","7:05"),
    ("D175/8","1",u"辽海","22:25",u"沙城","5:12/18"),
    ("D176/7","1",u"沙城","23:08/14",u"辽海","5:54"),
    ("Z151/4","1",u"辽海","17:30",u"滴水原","22:34/40",u"三界北","2:46/52",u"东篱","6:52"),
    ("Z152/3","1",u"东篱","18:30",u"三界北","22:30/36",u"滴水原","2:43/49",u"辽海","7:53"),
    ("T25/8","1",u"沙城","22:23/29",u"三界北","3:21/27",u"东篱","7:57/8:03",u"北桓","14:36"),
    ("T26/7","1",u"北桓","14:30",u"东篱","20:48/54",u"三界北","1:24/30",u"沙城","6:22/28"),
    ("T41/4","1",u"通州","20:51/57",u"东篱","3:16/22",u"北桓","9:48"),
    ("T42/3","1",u"北桓","19:30",u"东篱","0:56/1:02",u"通州","8:20/26"),
    ("T65/8","1",u"辽海","15:30",u"滴水原","20:57/21:03",u"三界北","1:34/40",u"通州","4:03/09",u"荃角西","11:42"),
    ("T66/7","1",u"荃角西","14:06",u"通州","21:29/35",u"三界北","23:57/0:03",u"滴水原","4:35/41",u"辽海","10:10"),
    ("T115/8","1",u"三界北","16:11/17",u"沙城","21:03"),
    ("T116/7","1",u"沙城","8:35",u"三界北","13:20/26")
)
# 车次, 开行频率(无需处理), 车站名称, 到发时, ……
  • 至少需要实现的功能:
    1. 能够正确将时刻表中时间整理为到点和发点,并计算站间历时和总历时;
    2. 查询并展示站到站的所有可用列车,并按历时进行升序排序,且需展示车站是否为始发、终到站;
    3. 展示指定车次的所有停站,显示总历时和各站到点、开点;
    4. 展示指定车站的所有列车,显示车次和在车站的开点;
    5. 展示时需尽量进行对齐。
  • 供参考的开发提示:
    1. 编写一个专门用于处理列车每个停靠车站信息(车站名称、到发时)的类,在这个类中实现将时刻分个处理为到发时等方法;
    2. 编写一个处理列车信息的类(车次、开行频率、所有车站),负责将给出的时刻表进行处理,并实现获取全程历时、某站是否为始发(终到)站判断等方法;
    3. 按需要编写其他代码。
  • 程序设计要求:
    1. 必须使用面向对象的程序设计方法;
    2. 对于字符串处理、排序等,已有内建方法的不要自行“造轮子”;
    3. 使用#type:标记函数和方法的参数类型、返回值类型等信息;
    4. 具有可读性较高的注释;
    5. 不要求一定在PySpigot上运行(即不要求现在就学会调用相关API),也可以在CPython上运行,由开发者自行决定环境,但必须使用Python 2.7的语法;
    6. 有余力者,可在“至少需要实现的功能”基础上,完成一些实用的自定义功能;
    7. 不得试图使用任何人工智能手段生成整段代码,不推荐使用内置激活的人工智能功能的编辑器或IDE;
    8. 满足其它提及的要求。
  • 练习完成后提交时,请将源代码、运行结果截图一并提交。