python中是没有switch语法的,我在练习的时候想使用类似switch的功能,搜索相关内容知道了使用字典可以完成我想要的步骤。于是,开始动手。
我使用的是python3,并且在练习使用tkinter模块写个小游戏:乒乓球。测试阶段,首先我敲入:
from tkinter import *
从而加载tkinter模块,并使用 * 使得在之后的代码输入中可以稍打一些代码。在这之后,我构想在创建一个canvas类变量,并在上面画一个矩形,通过左右方向键控制矩形移动从而模拟球拍。测试的完整代码为:
1 from tkinter import *
2
3
4 def move2right(dis):
5 canvas.move(1, dis, 0)
6
7
8 def move2left(dis):
9 canvas.move(1, -dis, 0)
10
11 movement = {'Right': move2right, 'Left': move2left}
12
13
14 def move(event):
15 movement.get(event.keysym)(8)
16
17 tk = Tk()
18 canvas = Canvas(tk, width=500, height=500)
19 canvas.pack()
20 canvas.create_rectangle(100, 100, 200, 120)
21 canvas.bind_all('<KeyPress-Right>', move)
22 canvas.bind_all('<KeyPress-Left>', move)
23
24 tk.mainloop()
以上测试代码的结果很顺利:使用字典变量movement将两个函数的指针存入字典中。21,22行绑定左右建的触发事件给函数move(),因为bind_all()方法中传递的函数要有一个参数(event),在bind_all()方法内部会将其设置为一个event类,从而存储触发的事件内容,因为以上原因,我定义了move()函数来封装movement.get()方法。15行中,.get()方法可以通过索引key获取相应的value,event.keysym为bind_all()传递给move()函数的event类中的元素,是一个代表相应键盘按键的字符串(这里有效的是'Right' 和 'left')。15行最后的 (8) 是函数的参数。
在测试成功后,我将这种方法移到了测试乒乓球和球拍反弹的文件中,这里面有乒乓球的Ball类和球拍Paddle类。先贴上最后可以运行的代码:
1 # !/usr/bin/env python3
2 # -*-coding=utf-8-*-
3
4 from tkinter import *
5 import time
6
7
8 class Ball:
9 def __init__(self, canvas, color, paddle):
10 self.canvas = canvas
11 self.canvas_height = self.canvas.winfo_height()
12 self.canvas_width = self.canvas.winfo_width()
13 self.paddle = paddle
14 self.id = canvas.create_oval(10, 10, 25, 25, fill=color)
15 self.canvas.move(self.id, 245, 100)
16 self.hit_bottom = False
17 self.x_pixel_of_one_step = 0 # 小球横向速度
18 self.y_pixel_of_one_step = -3 # 小球纵向速度
19
20 def hit_paddle(self, pos):
21 paddle_pos = self.canvas.coords(self.paddle.id)
22 if paddle_pos[0] <= (pos[0] + pos[2])/2.0 <= paddle_pos[2]:
23 if paddle_pos[1] <= pos[3] < paddle_pos[3] and self.y_pixel_of_one_step > 0:
24 return True
25 if paddle_pos[1] < pos[1] <= paddle_pos[3] and self.y_pixel_of_one_step < 0:
26 return True
27 return False
28
29 def draw(self):
30 ball_pos = self.canvas.coords(self.id) # 提取目前小球的位置
31 # 碰撞到上下边反弹参数设置
32 if ball_pos[1] <= 0 or ball_pos[3] >= self.canvas_height:
33 self.y_pixel_of_one_step = -self.y_pixel_of_one_step
34 # 碰撞到左右边反弹参数设置
35 if ball_pos[0] <= 0 or ball_pos[3] >= self.canvas_width:
36 self.x_pixel_of_one_step = -self.x_pixel_of_one_step
37 # 碰撞到球拍反弹参数设置
38 if self.hit_paddle(ball_pos):
39 self.y_pixel_of_one_step = -self.y_pixel_of_one_step
40 # 移动小球
41 self.canvas.move(self.id, self.x_pixel_of_one_step, self.y_pixel_of_one_step)
42
43
44 class Paddle:
45 def __init__(self, canvas, color):
46 self.canvas = canvas
47 self.canvas_height = canvas.winfo_height()
48 self.canvas_width = canvas.winfo_width()
49 self.id = canvas.create_rectangle(0, 0, 100, 10, fill=color)
50 self.canvas.move(self.id, 250, 250)
51 self.canvas.bind_all('<KeyPress-Right>', self.move)
52 self.canvas.bind_all('<KeyPress-Left>', self.move)
53 self.x_pixel_of_one_step = 0
54 self.y_pixel_of_one_step = 0
55
56 def move2left(self):
57 paddle_pos = self.canvas.coords(self.id)
58 if paddle_pos[0] >= 0:
59 self.canvas.move(self.id, -5, 0)
60
61 def move2right(self):
62 paddle_pos = self.canvas.coords(self.id)
63 if paddle_pos[2] <= self.canvas_width:
64 self.canvas.move(self.id, 5, 0)
65
66 movement = {'Right': move2right, 'Left': move2left}
67
68 def move(self, event):
69 self.movement.get(event.keysym)(self)
70 # print(self.movement[event.keysym])
71
72 def draw(self):
73 pass
74
75
76 tk = Tk()
77 tk.title('Fuck The Ping-Pang')
78 tk.resizable(0, 0) # 限制画布不能伸缩
79 tk.wm_attributes('-topmost', 1)
80 canvas = Canvas(tk,width=500, height=500, bd=0, highlightthickness=0)
81 # canvas = Canvas(tk, width=500, height=500)
82 canvas.pack()
83 tk.update()
84 paddle = Paddle(canvas, 'black')
85 ball = Ball(canvas, 'black', paddle)
86
87 while True:
88 ball.draw()
89 tk.update_idletasks()
90 tk.update()
91 time.sleep(0.01)
一切正常,除了56~70行。这几行正是我加入的刚刚所示的方法。我纠结的地方在第69行。这一行的内容我百思不得其解。
对于类方法的定义,所有类方法的第一个参数必须是一个self(名称可变)参量,这个参量默认指向方法说在的类,便于方法内部相关内容的编写。而在使用方法时,这个self参量是隐藏的,也就是说这个参量并不会对外可见。例如Paddle类中的move2left()函数(56行),在类方法定义中使用move2left()函数时,只需要写self.move2left(),内部的self参数不需要写。但是在69行上,我必须要在参数中加入self才行。重温一下69行:
self.movement.get(event.keysym)(self)
这里的self参数是move()方法中的默认参数self,指向所在的Paddle类,是一个函数指针。为什么要显示地写入这个参数呢?
在字典movement中保存了两个内容,分别是 move2left 函数和 move2right 函数的指针。那么在使用 movement.get('Right') 时,相当于返回了 move2right 。那么,movement.get('Right')() 这个代码就相当于move2right() 这个函数的调用,最后的括号表示前面的内容是一个函数,不加的话运行肯定是过不去的(不能用编译儿,因为python是解释性语言)。对于非类方法的函数,在函数定义时形参中没有设定默认值的参数,在调用这个函数时,必须要在相应的形参位置上传入实参,如果没有传入足够多的实参,那么Python解释器就无法正确运行这个函数从而报错。对于类方法中的函数,因为Python解释器对类的 ”特别对待“,所以在调用类方法的时候,解释器会自动 “跨过” 类方法中第一个默认指向自身类的指针的变量。
但是纠结的地方来了。在使用字典方法get()时,get 方法仅仅返回字典中对应键(key)的值(value)。在这里返回的是move2left或者move2right函数的地址。解释器运行到这里并从get()方法中出来后,得到了一个函数指针。然而此时解释器已经不知道这个指针是否是类中的方法,它只知道这个函数指针所指向的函数需要一个参数self,所以在与后面的括号()结合并解释成一个函数时,它需要相应个数的实参,所以此时不能忽略那个self参数的输入。
问题又来了,python中是隐藏数据类型的。并且在这个例子中,move2left 和 move2right 的self参量都没有使用,这时我可不可以不传入self 而传入别的参数,例如: self.movement.get(event.keysym)(20)。虽然这样是无意义的,但是与上一段python解释器的行为并不相冲突。恩,结果倒是没有悬念:运行出错。运行出错,那么原因基本只有一个:在解释器将get()方法与后面的括号结合起来并解释为函数时,解释器将这个函数解释为非类方法,将其作为普通函数,检测它的参数传递个数与定义个数是否一致。在检测参数一致后,程序将由函数指针进入这个函数,然后发现这个函数竟然是一个类方法!那么它就再检查一次向函数传递的参数,这时候发现传递给函数的参数有一个—— 20,而这个类方法的定义的参数只有一个默认的self。解释器接受这个事实,并把20赋值给self。因为脚本运行,程序继续跑(例如之前按的是左键,那么跳入的是move2left()方法)。在move2left 方法中使用了self.canvas.move()方法,解释器这时候发现,self等于20!它不是一个类!好吧好吧,报错吧那就!
结案。