本篇博客是博主自己在这里:https://github.com/jackfrued/Python-100-Days 学习Python时做的一些笔记,由于我已经有了一些基础(因为学习过C语言、Java等,其中涉及到的有比如多线程、GUI、网络编程等内容),所以这里做的笔记可能对于新手来说比较有跳跃性。如果你确实有这样的体会,那建议去找其他路径来学习。买过一本关于Python爬虫的书,附了张学习Python的学习路线图,分享给大家下,仅供想要学习Python的同学参考:
本篇包括:
- 语言设计基础
- 面向对象编程
- 图形用户界面和游戏开发
一、语言设计基础
1、注释
单行注释 - 以#和空格开头的部分
多行注释 - 三个引号开头,三个引号结尾
2、运算符
a = 5
print("a = ", a)
flag1 = 3 > 2
print("flag1 = ", flag1)
#!/usr/bin/python
# -*- coding: latin-1 -*-
import os, sys
f = float(input('请输入华氏温度:'))
c = (f - 32) / 1.8
print('%.1f华氏度 = %.1f摄氏度' % (f, c))
#!/usr/bin/python
# -*- coding: latin-1 -*-
import os, sys, math
radius = float(input('Please input redius of the cicle: '))
perimeter = 2 * math.pi * radius
area = math.pi * radius * radius
print('Perimeter = %.2f' % perimeter)
print('Area = %.2f' % area)
# 输入年份 如果是闰年输出True 否则输出False
year = int(input('Please input a year: '))
# 如果代码太长写成一行不便于阅读 可以使用\或()折行
is_leap = (year % 4 == 0 and year % 100 != 0 or
year % 400 == 0)
print(is_leap)
3、分支结构
- 和C/C++、Java等语言不同,Python中if…else… 没有用花括号 来构造代码块,而是使用了缩进的方式来设置代码的层次结构。如果if条件成立的情况下需要执行多条语句,只要保持多条语句具有相同的缩进就可以了;换句话说连续的代码如果又保持了 相同的缩进 那么它们属于同一个代码块,相当于是一个执行的整体。
示例5-1
# 用户身份验证
username = input('Input Username: ')
password = input('Input Password: ')
# 如果希望输入Password时 终端中没有回显 可以使用getpass模块的getpass函数
# import getpass
# password = getpass.getpass('Input Password: ')
if username == 'admin' and password == '123456':
print('Success!')
else:
print('Fault!')
2. 如果要构造出更多的分支,则使用if…elif…else…结构。
示例5-2
"""
分段函数求值:
3x - 5 (x > 1)
f(x) = x + 2 (-1 <= x <= 1)
5x + 3 (x < -1)
"""
x = float(input('x = '))
if x > 1:
y = 3 * x - 5
elif x >= -1:
y = x + 2
else:
y = 5 * x + 3
print('f(%.2f) = %.2f' % (x, y))
示例6
# 掷骰子决定做什么事情
from random import randint
face = randint(1, 6) #使用random模块的randint函数生成指定范围的随机数来模拟掷骰子
if face == 1:
result = 'Sing'
elif face == 2:
result = 'Dance'
elif face == 3:
result = 'Paint'
elif face == 4:
result = 'Socccer'
elif face == 5:
result = 'Tennis'
else:
result = 'Cold Joke'
print(result)
4、循环结构
- Python中构造循环结构有两种做法,一种是for-in循环,一种是while循环:如果明确的知道循环执行的次数或者要对一个容器进行迭代(后面会讲到),那么我们推荐使用for-in循环;否则,推荐使用while循环。
示例7
# 用for循环实现1~100求和
sum = 0
for x in range(101):
sum += x
print(sum)
"""
range可以用来产生一个不变的数值序列,而且这个序列通常都是用在循环中的,例如:
range(101)可以产生一个0到100的整数序列。
range(1, 100)可以产生一个1到99的整数序列。
range(1, 100, 2)可以产生一个1到99的奇数序列,其中的2是步长,即数值序列的增量——
此结构可以实现功能:1~100之间的偶数求和——
只需将range(101)替换成range(2, 101, 2)即可。
"""
示例8
"""
猜数字游戏
计算机出一个1~100之间的随机数由人来猜
计算机根据人猜的数字分别给出提示大一点Larger/小一点Smaller/猜对了
"""
import random
answer = random.randint(1, 100)
counter = 0
while True:
counter += 1
number = int(input('Input a guess number: '))
if number < answer:
print('Larger')
elif number > answer:
print('Smaller')
else:
print('Guess Right!')
break
print('You guessed %d times.' % counter)
循环结构_练习
示例9
# 输入一个正整数判断它是不是素数
from math import sqrt
num = int(input('请输入一个正整数: '))
end = int(sqrt(num))
is_prime = True
for x in range(2, end + 1):
if num % x == 0:
is_prime = False
break
if is_prime and num != 1:
print('%d是素数' % num)
else:
print('%d不是素数' % num)
示例9
- 我的解法:
# 输入两个正整数,计算最大公约数和最小公倍数
# 注意最大公约数与最小公倍数的关系!!!
a = int(input("请输入一个正整数:"))
b = int(input("请再输入一个正整数:"))
if a >= b:
min = b
else:
min = a
while(min != 1):
if(a % min == 0 and b % min == 0):
print('%d和%d的最大公约数是:%d' % (a, b, min))
print('%d和%d的最小公倍数是:%d' % (a, b, a * b // min))
break
else:
min -= 1
- 老师解法:
# 输入两个正整数,计算最大公约数和最小公倍数
x = int(input('x = '))
y = int(input('y = '))
if x > y:
x, y = y, x # 啥意思???
for factor in range(x, 0, -1):
if x % factor == 0 and y % factor == 0:
print('%d和%d的最大公约数是%d' % (x, y, factor))
print('%d和%d的最小公倍数是%d' % (x, y, x * y // factor))
break
示例10
"""
Craps赌博游戏:
玩家摇两颗色子,如果第一次摇出的两筛子之和为7点或11点——玩家胜;
如果摇出2点 3点 12点——庄家胜;
其他情况时,则需要玩家再次摇这两颗色子:如果和为7点——庄家胜;
如果和与第一次相同——玩家胜。
玩家进入游戏时有1000元的资本,玩家每次选择赌注,直至全部输光,游戏结束。
"""
from random import randint
money = 1000 # 玩家的总资产...不需要在意庄家的,始终是玩家的总资产在改变着
while money > 0:
print('你的总资产为:%d' % money)
needs_go_on = False
while True:
debt = int(input('请下注:'))
if debt > 0 and debt <= money:
break
first = randint(1, 6) + randint(1, 6)
print('玩家摇出了%d点' % first)
if first == 7 or first == 11:
print('Player Success')
money += debt
elif first == 2 or first == 3 or first == 12:
print('Zhuangjia Success')
money -= debt
else: # 摇出的两筛子之和不是7/11/22/3/12的情况
needs_go_on = True
while needs_go_on:
current = randint(1, 6) + randint(1, 6)
print('玩家摇出了%d点' % current)
if current == 7:
print('Zhuangjia Success')
money -= debt
needs_go_on = False
elif current == first:
print('Player Success')
money += debt
needs_go_on = False
print('你破产了...Game Over! ')
示例11
# 输出斐波那契数列的前20个数
a = 0
b = 1
for _ in range(20):
a, b = b, a + b
print(a)
- 讲解:Python中的赋值语法——从右往左进行计算,然后再依次(从左往右)赋值
1、a, b = b, a + b 等价于:a = b, b = a + b。过程:
temp = b # 先保存b的原值
b = a + b # 赋b新值
a = temp # 将b的原值赋予a
2、a, b = 1, 3 等价于 a = 1, b = 3
示例12
# 判断输入的正整数是不是回文数
num = int(input('请输入一个正整数: '))
temp = num
num2 = 0
while temp > 0:
num2 *= 10 # 每次扩大十倍,为下面加上“个位”做准备
num2 += temp % 10 # 每次得到temp的最低位
temp //= 10 # 每次得到舍去temp最低位后的那个数
if num == num2:
print('%d是回文数' % num)
else:
print('%d不是回文数' % num)
示例13
"""
找出1~9999之间的所有完美数
完美数是除自身外其他所有因子的和正好等于这个数本身的数
例如: 6 = 1 + 2 + 3, 28 = 1 + 2 + 4 + 7 + 14
"""
for i in (1, 10000):
sum = 0
for factor in range(1, int(math.sqrt(num)) + 1):
if num % factor == 0:
sum += fator;
# 如28可以被2整除,那么其结果14也必定是28的因子。这里就是这样找出它的“另一半”的
if factor > 1 and num / factor != factor:
sum += num / factor
if sum == num:
print(num)
5、函数
函数的定义
Python中的函数与其他语言中的函数有很多不太相同的地方,其中一个显著的区别就是Python对函数参数的处理。在Python中,函数的参数可以有 默认值 ,也支持使用 可变参数 ,所以Python并不需要像其他语言一样支持函数的重载,因为我们在定义一个函数的时候可以让它有多种不同的使用方式,下面是两个小例子。
- 为参数设定默认值
from random import randint
"""
摇色子
:param n: 色子的个数
:return: n颗色子点数之和
"""
def roll_dice(n=2):
total = 0
for _ in range(n):
total += randint(1, 6)
return total
def add(a=0, b=0, c=0):
return a + b + c
# 如果没有指定参数那么使用默认值摇两颗色子
print(roll_dice())
# 摇三颗色子
print(roll_dice(3))
print(add())
print(add(1))
print(add(1, 2))
print(add(1, 2, 3))
# 传递参数时可以不按照设定的顺序进行传递
print(add(c=50, a=100, b=200))
- 设置可变参数
# 在参数名前面的*表示args是一个可变参数
# 即在调用add函数时可以传入0个或多个参数
def add(*args):
total = 0
for val in args:
total += val
return total
print(add())
print(add(1))
print(add(1, 2))
print(add(1, 2, 3))
print(add(1, 3, 5, 7, 9))
函数的管理——用模块管理函数
- 由于Python没有函数重载的概念,那么后面的函数定义会覆盖之前的函数定义,也就意味着两个函数同名函数实际上只有一个是存在的。那么如何使得两个同名函数都能够互不干扰的存在呢?答案其实很简单, Python中每个文件就代表了一个模块(module),在不同的模块中可以有同名的函数,在使用函数的时候我们通过 import 关键字导入指定的模块就可以区分到底要使用的是哪个模块中的foo函数 ,代码如下所示。
module1.py
def foo():
print('hello, world!')
module2.py
def foo():
print('goodbye, world!')
方式1
test.py
from module1 import foo
foo() # 输出hello, world!
from module2 import foo
foo() # 输出goodbye, world!
方式2
test.py
import module1 as m1
import module2 as m2
m1.foo()
m2.foo()
错误示范:后导入的foo覆盖了之前导入的foo
from module1 import foo
from module2 import foo
# 输出goodbye, world!
foo()
- 需要说明的是,如果我们导入的模块除了定义函数之外还中有可以执行代码,那么Python解释器在导入这个模块时就会执行这些代码,事实上我们可能并不希望如此,因此如果我们在模块中编写了可执行代码,最好是将这些执行代码放入如下所示的条件中,这样的话除非直接运行该模块,否则if条件下的这些代码(函数中的可执行代码)是不会执行的,因为只有 直接执行的模块 (或者说是“ 当前正在执行的模块 ”)的名字才是“ main ”。
module3.py
def foo():
pass
def bar():
pass
# __name__是Python中一个隐含的变量,它代表模块的名字
# 只有被Python解释器直接执行的模块的名字才是__main__
if __name__ == '__main__':
print('call foo()')
foo()
print('call bar()')
bar()
test.py
import module3
# 导入module3时,不会执行模块中if条件成立时的代码,因为if中模块的名字_name_是module3而不是__main__,所以不符合if执行的条件。所以我们导入的module3模块中的可执行代码不会执行。
- Python中有关变量作用域的问题的讨论具体请参考这里。
def foo():
b = 'hello'
def bar(): # Python中可以在函数内部再定义函数
c = True
print(a)
print(b)
print(c)
bar()
# print(c) # NameError: name 'c' is not defined
if __name__ == '__main__':
a = 100
# print(b) # NameError: name 'b' is not defined
foo()
说明:输出为100 hello True。原因请点击上面的链接进行查看(之所以没有解释,是因为自己会,只是为了以防万一又不会,所以还是简单的放在这里以下)。
- 从现在开始我们可以将Python代码按照下面的格式进行书写,这一点点的改进其实就是在我们理解了函数和作用域的基础上跨出的巨大的一步。
def main():
# Todo: Add your code here
pass # pass写在任何缩进的语句块部分,只是占位,什么事情都不做。为了满足python的语法要求。
if __name__ == '__main__':
main()
6、常用数据结构-练习题
Click Here.
二、面向对象编程
1、基础
类是对象的蓝图和模板,而对象是类的实例。可以看出,类是抽象的概念,而对象是具体的东西。在面向对象编程的世界中,一切皆为对象,对象都有属性和行为,每个对象都是独一无二的,而且对象一定属于某个类(型)。当我们把一大堆拥有共同特征的对象的静态特征(属性——通过类中的变量描述)和动态特征(行为——通过类中的方法描述【 写在类中的函数,我们通常称之为(对象的)方法】)都抽取出来后,就可以定义出一个叫做“类”的东西。
类的定义、创建、使用
务必仔细阅读下面的程序及注释,描述了相关的语法规则。更详细的Python中类的定义的解释,请看这里。
class Student(object):
# __init__是一个特殊方法用于在创建对象时进行初始化操作
# 通过这个方法我们可以为学生对象绑定name和age两个属性
def __init__(self, name, age): # self参数是类中的每个方法的参数都必须有的,用于指示本对象,相当于Java中的this
self.name = name
self.age = age
def study(self, course_name):
print('%s正在学习%s.' % (self.name, course_name))
# PEP 8要求标识符的名字用全小写多个单词用下划线连接
# 但是部分程序员和公司更倾向于使用驼峰命名法(驼峰标识)
def watch_movie(self):
if self.age < 18:
print('%s只能看《熊出没》.' % self.name)
else:
print('%s正在看喜欢的电影.' % self.name)
def main():
stu1 = Student('zjy', 18) # 自动调用__init__方法,使‘Student’赋值给self,‘zjy’赋值给name,'18'赋值给age
stu1.study('Python程序设计')
stu1.watch_movie()
if __name__ == '__main__':
main()
类中属性/方法的访问权限
在Python中,属性和方法的访问权限只有两种,也就是 公开 的和 私有 的,如果希望属性是私有的,在给属性命名时可以用 两个下划线作为开头 ,下面的代码可以验证这一点。
class Test:
def __init__(self, foo):
self.__foo = foo # 左侧的__foo是类中的属性,foo只是参数罢了
def __bar(self):
print(self.__foo)
print('__bar')
def main():
test = Test('hello') # 自动调用__init__方法,使‘Test’赋值给self,‘hello’赋值给foo
# (私有方法)AttributeError: 'Test' object has no attribute '__bar'
test.__bar()
# (私有属性)AttributeError: 'Test' object has no attribute '__foo'
print(test.__foo)
if __name__ == "__main__":
main()
但是,Python并没有从语法上严格保证私有属性或方法的私密性,它只是给私有的属性和方法换了一个名字来“妨碍”对它们的访问,事实上如果你知道更换名字的规则,那么你仍然可以访问到那些所谓的私有属性。下面的代码就可以验证这一点,并且你可以从中知道“ 更换名字的规则 ”。之所以这样设定,可以用这样一句名言加以解释,就是“We are all consenting adults here”。因为绝大多数程序员都认为开放比封闭要好,而且程序员要自己为自己的行为负责。
class Test:
def __init__(self, foo):
self.__foo = foo
def __bar(self):
print(self.__foo)
print('__bar')
def main():
test = Test('hello')
test._Test__bar() # 使私有的__bar()方法变成“公有”
print(test._Test__foo) # 使私有的__foo属性变成“公有”
if __name__ == "__main__":
main()
在实际开发中,我们并不建议将属性设置为私有的,因为这会导致子类无法访问(后面会讲到)。所以大多数Python程序员会遵循一种命名惯例就是让属性名以单下划线开头来表示属性是受保护的,本类之外的代码在访问这样的属性时应该要保持慎重。这种做法并不是语法上的规则,因为 单下划线开头 的属性和方法外界仍然是可以访问的,所以更多的时候它是一种暗示或隐喻:“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。
- 练习1:定义一个类描述数字时钟
from time import sleep
class Clock(object):
def __init__(self, hour=0, minute=0, second=0):
# 成员变量hour、minute、second是“受保护”的——名称以单个下划线开始
self._hour = hour
self._minute = minute
self._second = second
def run(self):
self._second += 1
if self._second == 60:
self._second = 0;
self._minute += 1
if self._minute == 60:
self._minute = 0
self._hour += 1
if self._hour == 24:
self._hour = 0
def show(self):
return '%02d:%02d:%02d' % (self._hour, self._minute, self._second)
def main():
clock = Clock(23, 59, 58)
while True:
print(clock.show())
sleep(1)
clock.run()
- 练习2:定义一个类描述平面上的点并提供移动点和计算到另一个点距离的方法
from math import sqrt
class Point(object):
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def show(self):
print('点在平面上的位置:x = %d, y = %d' % (self.x, self.y))
def distance(self, other):
dx = self.x - other.x
dy = self.y - other.y
return sqrt(dx ** 2 + dy ** 2)
def move_to(self, x, y):
"""移动到指定位置
:param x: 新的横坐标
"param y: 新的纵坐标
"""
self.x = x
self.y = y
def move_by(self, dx, dy):
"""移动指定的增量
:param dx: 横坐标的增量
"param dy: 纵坐标的增量
"""
self.x += dx
self.y += dy
def main():
p1 = Point(3, 5)
p2 = Point()
p1.show()
p2.show()
p2.move_by(-1, 2)
p2.show()
print(p1.distance(p2))
if __name__ == '__main__':
main()
面向对象的三大支柱 之 封装
- “隐藏一切可以隐藏的实现细节,只向外界暴露(提供)简单的编程接口”。我们在类中定义的方法其实就是把数据和对数据的操作封装起来了,在我们创建了对象之后,只需要给对象发送一个消息(调用方法)就可以执行方法中的代码,也就是说我们只需要知道方法的名字和传入的参数(方法的外部视图),而不需要知道方法内部的实现细节(方法的内部视图)。
2、进阶
@property装饰器
以上,我们知道了: 不建议将属性设置为私有的,但是如果直接将属性暴露给外界也是有问题的 ,比如我们没有办法检查赋给属性的值是否有效。我们之前的建议是将属性命名 以单下划线开头 ,通过这种方式来暗示属性是受保护的,不建议外界直接访问,那么如果想访问属性可以通过属性的 getter(访问器) 和 setter(修改器) 方法进行对应的操作。如果要做到这点,就可以考虑使用 @property包装器 来包装getter和setter方法,使得对属性的访问既安全又方便,代码如下所示。
class Person(object):
def __init__(self, name, age):
self._name = name
self._age = age
# 必须要先getter方法,然后再setter方法,否则会有编译错误
@property # 访问器:getter方法
def age(self):
return self._age
@property # 访问器:getter方法
def name(self):
return self._name
@age.setter # 修改器:setter方法
def age(self, age):
self._age = age
def play(self):
if self._age <= 16:
print('%s正在玩飞行棋...' % self._name)
else:
print('%s正在玩斗地主...' % self._name)
def main():
person = Person('zjy', 12)
person.play()
"""
为了重新为person._age赋值,系统根据此语句将自动调用相应的set方法,重新为_age赋值。
这里使用的是_age,实际上,使用person.age也ok,
但其他age的变形就不行了,也就是说,可以是_age或age,
看下一知识点就可以知道如何进行名称使用的限制了
"""
person._age = 22
"""
你以为下面也是为变量重新赋值的语句,但其实:TypeError: 'int' object is not callable.
所以,这是一种错误的重新为成员变量赋值的方法,与Java不太相同的地方。
Java要写出调用的set方法为变量赋值,
而这里的重新赋值应该写成上面那样,系统明白(@age.setter)你是想重新为变量赋值。
"""
# person.age(22)
person.play()
"""
下面的语句:AttributeError: can't set attribute.
因为没有提供name的set方法(@name.setter),所以不能修改此属性。
"""
# person.name = 'zyy'
"""
如何通过所谓的get方法调用属性值?这里有必要说一下:
你以为:print('姓名:%c' % person.name())
但实际:print('姓名:%c' % person.name)
又与Java不同,这里不把他们当做方法来使用,而是直接当作成员变量!
然后会自动调用相应的get方法~
"""
print('姓名:%s' % person.name)
if __name__ == '__main__':
main()
__slots__魔法
至此,你应该知道Python是一门动态语言。通常,动态语言允许我们在程序运行时给对象绑定新的属性或方法,当然也可以对已经绑定的属性和方法进行解绑定。但是如果我们需要限定自定义类型的 对象只能绑定指定的某些属性 ,可以通过在类中定义__slots__变量来进行限定。需要注意的是 __slots__的限定只对当前类的对象生效,对子类并不起任何作用 。
class Person(object):
# 限定Person对象只能绑定_name,_age和_gender属性
__slots__ = ('_name', '_age', '_gender')
def __init__(self, name, age):
self._name = name
self._age = age
@property
def name(self):
return self._name
@property
def age(self):
return self._age
@age.setter
def age(self, age):
self._age = age
def play(self):
if self._age <= 16:
print('%s正在玩飞行棋.' % self._name)
else:
print('%s正在玩斗地主.' % self._name)
def main():
person = Person('zjy', 22)
person.play()
person._gender = '男' # 这回就不可以用person.gender了!因为属性名已经被限制了!
if __name__ == '__main__':
main()
@staticmethod 与 @classmethod
在此之前,我们在类中定义的方法都称为对象方法,也就是说这些方法都是发送给 对象 的消息。下面,我们来学习两种非写给对象的方法,而是属于类的方法。
@staticmethod
使用@staticmethod定义出来的方法叫做 静态方法 。例如我们定义一个“三角形”类,通过传入三条边长来构造三角形,并提供计算周长和面积的方法,但是传入的三条边长未必能构造出三角形对象,因此我们可以先写一个方法来验证三条边长是否可以构成三角形,这个方法很显然就不是对象方法,因为在调用这个方法时三角形对象尚未创建出来(因为都不知道三条边能不能构成三角形),所以这个方法是属于三角形类而并不属于三角形对象的。
from math import sqrt
class Triangle(object):
def __init__(self, a, b, c):
self._a = a
self._b = b
self._c = c
@staticmethod
def is_valid(a, b, c):
return a + b > c and b + c > a and a + c > b
# 求三角形周长
def perimeter(self):
return self._a + self._b + self._c
# 求三角形面积
def area(self):
half = self.perimeter() / 2
return sqrt(half * (half - self._a) * (half - self._b) * (half - self._c))
def main():
a, b, c = 3, 4, 5
# 静态方法(以及下面要讲的类方法)都是通过给类发消息来调用的
if Triangle.is_valid(a, b, c): # 关键语句!!!
t = Triangle(a, b, c)
# 调用对象方法的第一种方式
print(t.perimeter())
# 调用对象方法的第二种方式:通过给类发消息,此时需要传入接收消息的类对象作为参数
# print(Triangle.perimeter(t))
print(t.area()) # 等价于:print(Triangle.area(t))
else:
print('无法构成三角形...')
if __name__ == '__main__':
main()
@classmethod
和静态方法比较类似,Python还可以在类中定义类方法, 类方法的第一个参数约定名为cls ,它代表的是与当前类相关的信息的对象(类本身也是一个对象,有的地方也称之为 类的元数据对象 ),通过这个参数我们可以获取和类相关的信息并且通过此创建出类的对象,代码如下所示。
from time import time, localtime, sleep
class Clock(object):
"""数字时钟"""
def __init__(self, hour=0, minute=0, second=0):
self._hour = hour
self._minute = minute
self._second = second
@classmethod
def now(cls):
ctime = localtime(time()) # 调用Python内置函数localtime()、time()
return cls(ctime.tm_hour, ctime.tm_min, ctime.tm_sec)
def run(self):
"""走字"""
self._second += 1
if self._second == 60:
self._second = 0
self._minute += 1
if self._minute == 60:
self._minute = 0
self._hour += 1
if self._hour == 24:
self._hour = 0
def show(self):
"""显示时间"""
return '%02d:%02d:%02d' % \
(self._hour, self._minute, self._second)
def main():
clock = Clock.now() # 关键语句——通过类方法创建对象并获取系统时间
while True:
print(clock.show())
sleep(1)
clock.run()
if __name__ == '__main__':
main()
类之间的关系
简单的说,类和类之间的关系有三种:is-a、has-a和use-a关系:
- is-a 关系也叫继承或泛化,比如学生和人的关系、手机和电子产品的关系都属于继承关系。
- has-a 关系通常称之为关联,比如部门和员工的关系,汽车和引擎的关系都属于关联关系;关联关系如果是整体和部分的关联,那么我们称之为聚合关系;如果整体进一步负责了部分的生命周期(整体和部分是不可分割的,同时同在也同时消亡),那么这种就是最强的关联关系,我们称之为合成关系。
- use-a 关系通常称之为依赖,比如司机有一个驾驶的行为(方法),其中(的参数)使用到了汽车,那么司机和汽车的关系就是依赖关系。
我们可以使用一种叫做UML(统一建模语言)的东西来进行面向对象建模,其中一项重要的工作就是把类和类之间的关系用标准化的图形符号描述出来。关于UML我们在这里不做详细的介绍,有兴趣的读者可以自行阅读《UML面向对象设计基础》一书。
利用类之间的这些关系,我们可以在已有类的基础上来完成某些操作,也可以在已有类的基础上创建新的类,这些都是实现
代码复用
的重要手段。复用现有的代码不仅可以减少开发的工作量,也有利于代码的管理和维护,这是我们在日常工作中都会使用到的技术手段。
面向对象的三大支柱 之 继承
上面已提到:可以在已有类的基础上创建新类。这其中的一种做法就是让一个类从另一个类那里将属性和方法直接继承下来,从而减少重复代码的编写。提供继承信息的我们称之为父类,也叫超类或基类;得到继承信息的我们称之为子类,也叫派生类或衍生类。子类除了继承父类提供的属性和方法,还可以定义自己特有的属性和方法,所以子类比父类拥有的更多的能力,在实际开发中,我们经常会用子类对象去替换掉一个父类对象,这是面向对象编程中一个常见的行为,对应的原则称之为里氏替换原则。下面我们先看一个继承的例子。
class Person(object):
def __init__(self, name, age):
self._name = name
self._age = age
@property
def name(self):
return self._name
@property
def age(self):
return self._age
@age.setter
def age(self, age):
self._age = age
def play(self):
print('%s正在愉快的玩耍.' % self._name)
def watch_tv(self):
if self._age >= 18:
print('%s正在观看泡沫剧.' % self._name)
else:
print('%s只能观看《熊出没》.' % self._name)
class Student(Person):
def __init__(self, name, age, grade):
super(Student, self).__init__(name, age)
# 下面是Python3中的用法
# super().__init__(name, age)
self._grade = grade
@property
def grade(self):
return self._grade
@grade.setter
def grade(self, grade):
self._grade = grade
def study(self, course):
print('%s的%s正在学习%s.' % (self._grade, self._name, course))
class Teacher(Person):
def __init__(self, name, age, title): # title表示老师的职位
super(Teacher, self).__init__(name, age)
# 下面是Python3中的用法
# super().__init__(name, age)
self._title = title
@property
def title(self):
return self._title
def teach(self, course):
print('%s%s正在讲%s' % (self._name, self._title, course))
def main():
stu = Student('zjy', 15, '初三')
stu.study('数学')
stu.watch_tv()
t = Teacher('zyy', 38, '教授')
t.teach('Python程序设计.')
t.watch_tv
if __name__ == '__main__':
main()
面向对象的三大支柱 之 多态
子类在继承了父类的方法后,可以对父类已有的方法给出新的实现版本,这个动作称之为方法重写(override)。通过方法重写我们可以让父类的同一个行为在子类中拥有不同的实现版本,当我们调用这个经过子类重写的方法时,不同的子类对象会表现出不同的行为,这一现象就被称作多态(poly-morphism)。
from abc import ABCMeta, abstractmethod
class Pet(object, metaclass=ABCMeta): # metaclass=ABCMeta必须要在3.5版本以上的Python环境下才可以啊
def __init__(self, nickname):
self._nickname = nickname
@abstractmethod # 在父类中定义抽象方法,当有子类继承时,就要写此抽象方法
def make_voice(self)
pass
class Dog(Pet):
def make_voice(self):
print('%s: 汪汪汪' % self._nickname)
class Cat(Pet):
def make_voice(self):
print('%s 喵喵喵' % self._nickname)
def main():
pets = [Dog('旺财'), Cat('凯蒂'), Dog('大黄')] # 一个宠物列表
for pet in pets:
pet.make_voice()
if __name__ == '__main__':
main()
在上面的代码中,我们将
Pet
类处理成了一个抽象类,所谓抽象类就是不能够创建对象的类,这种类的存在就是专门为了让其他类去继承它。
Python从语法层面并没有像Java或C#那样提供对抽象类的支持,但是我们可以通过
abc
模块的
ABCMeta
元类和
abstractmethod
包装器来达到抽象类的效果
,如果一个类中存在抽象方法那么这个类就不能够实例化(创建对象)。上面的代码中,
Dog
和
Cat
两个子类分别对
Pet
类中的
make_voice
抽象方法进行了重写并给出了不同的实现版本,当我们在main函数中调用该方法时,这个方法就表现出了多态行为(同样的方法做了不同的事情)。
- 练习1:奥特曼打小怪兽
简要描述:
- 奥特曼和小怪兽是敌对的两方,两者要进行战斗。其父类称之为“战斗者”,有属性战斗者名称、战斗者生命值、alive(该属性是判断生命值是否大于0得出的:alive为True表示生命值大于0,表示还存活在游戏中),此外,有一个待继承的表示攻击形式的抽象方法attack()。
- 奥特曼类:除了有名字、生命值属性外,又添加了魔法值属性。具有继承来的表示普通攻击形式的attack()方法以及魔法攻击形式的magic_attack()方法以及终级必杀技攻击形式的huge_attack()方法,由于魔法值会因为使用了普通攻击而增加,所以还具有增加魔法值的resume()方法。
- 小怪兽类:具有从父类继承来的名字、声明值属性,以及普通攻击形势的attack()方法。
-
三个全局函数:
1)is_any_alive():判断一群小怪兽里还有没有是活着的
2)select_alive_one():选中一只活着的小怪兽(用来与奥特曼战斗)
3)display_info():显示奥特曼和小怪兽的相关战斗信息
from abc import ABCMeta, abstractmethod
from random import randint, randrange
class Fighter(object, metaclass=ABCMeta):
"""战斗者(父类),将由奥特曼(子类)、小怪兽(子类)继承"""
# 通过__slots__魔法限定对象可以绑定的成员变量
__slots__ = ('_name', '_hp')
def __init__(self, name, hp):
"""
初始化方法
:param name: 名字
:param hp: 生命值
"""
self._name = name
self._hp = hp
@property
def name(self):
return self._name
@property
def hp(self):
return self._hp
@hp.setter
def hp(self, hp):
self._hp = hp if hp >= 0 else 0
@property
def alive(self):
return self._hp > 0
@abstractmethod
def attack(self, other):
"""
攻击
:param other: 被攻击的对象
"""
pass
class Ultraman(Fighter):
"""奥特曼"""
__slots__ = ('_name', '_hp', '_mp')
def __init__(self, name, hp, mp):
"""
初始化方法
:param name: 名字
:param hp: 生命值
:param mp: 魔法值
"""
super().__init__(name, hp)
self._mp = mp
def attack(self, other):
other.hp -= randint(15, 25)
def huge_attack(self, other):
"""
终级必杀技(打掉对方至少50点或四分之三的血)
:param other: 被攻击的某个对象
:return: 使用成功返回True否则返回False
"""
if self._mp >= 50:
self._mp -= 50
injury = other.hp * 3 // 4
injury = injury if injury >= 50 else 50
other.hp -= injury
return True
else: # 使用终级必杀技需要消耗50魔法值,此为魔法值不足的情况,无法使用终级必杀技,改为使用普通攻击
self.attack(other)
return False
def magic_attack(self, others):
"""
魔法攻击
:param others: 被攻击的群体(使用魔法攻击可以一次性攻击多个小怪兽)
:return: 使用魔法成功返回True否则返回False
"""
if self._mp >= 20:
self._mp -= 20
for temp in others:
if temp.alive:
temp.hp -= randint(10, 15)
return True
else: # 魔法值不足20时无法使用魔法攻击
return False
def resume(self):
"""
恢复随机点数的魔法值
:return: 返回新增的点数
"""
incr_point = randint(1, 10)
self._mp += incr_point
return incr_point
def __str__(self):
return '~~~%s奥特曼~~~\n' % self._name + \
'生命值:%d\n' % self._hp + \
'魔法值:%d\n' % self._mp
class Monster(Fighter):
"""小怪兽"""
__slots__ = ('_name', '_hp')
def attack(self, other):
other.hp -= randint(10, 20)
def __str__(self):
return '~~~%s小怪兽~~~\n' % self._name + \
'生命值:%d\n' % self._hp
def is_any_alive(monsters):
"""判断有没有小怪兽是活着的"""
for monster in monsters:
if monster.alive > 0:
return True
return False
def select_alive_one(monsters):
"""选中一只活着的小怪兽"""
monsters_num = len(monsters)
while True:
index = randrange(monsters_num)
monster = monsters[index]
if monster.alive > 0:
return monster
def display_info(ultraman, monsters):
"""显示奥特曼和小怪兽的相关战斗信息"""
print(ultraman) # 自动调用__str__方法
for monster in monsters:
print(monster, end='') # 自动调用__str__方法
def main():
u = Ultraman('zjy', 1000, 120)
m1 = Monster('aaa', 250)
m2 = Monster('bbb', 500)
m3 = Monster('ccc', 750)
ms = [m1, m2, m3]
fight_round = 1 # 大战回合数
while u.alive and is_any_alive(ms): # alive()是父级中的用来得到alive变量的get方法,该方法用来判断_hp 是否大于0 只有大于0才说明战斗者活着,只有活着才能进行下面的动作。活着的话就返回True,True == 1
print('=========第%02d回合========' % fight_round)
m = select_alive_one(ms) # 选中一只小怪兽参与战斗
skill = randint(1, 10) # 通过随机数选择使用哪种技能
if skill <= 6: # skill为1 ~ 6时,奥特曼使用普通攻击
print('%s使用普通攻击打了%s.' % (u.name, m.name))
u.attack(m)
print('%s的魔法值恢复了%d点.' % (u.name, u.resume())) # 每使用一次普通攻击,魔法值都会有一定补给
elif skill <= 9: # skill为6 ~ 9时,奥特曼使用魔法攻击
if u.magic_attack(ms):
print('%s成功使用魔法攻击了小怪兽群体.' % u.name)
else:
print('%s魔法值不足,魔法攻击失败.' % u.name)
else: # skill为10时,奥特曼使用终级必杀技
if u.huge_attack(m):
print('%s使用终级必杀技虐了%s.' % (u.name, m.name))
else:
print('%s使用普通攻击打了%s.' % (u.name, m.name))
print('%s的魔法值恢复了%d点.' % (u.name, u.resume())) # 每使用一次普通攻击,魔法值都会有一定补给
if m.alive > 0: # 如果与奥特曼战斗的小怪兽没有死,就回击奥特曼
# 不能替换成if m.alive() == True!因为alive其实是个get到的成员变量...
print('%s回击了%s.' % (m.name, u.name))
m.attack(u)
display_info(u, ms) # 每个回合结束后显示奥特曼和小怪兽的信息
fight_round += 1
print('\n========战斗结束!========\n')
if u.alive > 0:
print('%s奥特曼胜利!' % u.name)
else:
print('小怪兽胜利!')
if __name__ == '__main__':
main()
- 练习2:扑克游戏
简要说明
-
“一张牌”类
属性:花色、点数
方法:用来以统一的格式输出每张牌的__str__方法 -
“一副牌”类
属性:保存了一整副牌的cards属性、用来判断是否发了牌的current属性
方法:洗牌、发牌、判断牌是否已全部发完 -
“玩家”类
属性:玩家姓名name、 玩家手中的所有牌cards_on_hand
方法:摸牌、整理手中的牌 - 全局函数get_key(card)用来定义排序规则-先根据花色再根据点数排序。
import random
class Card(object):
"""一张牌"""
def __init__(self, suite, face):
"""
:suite: 花色
:face: 点数
"""
self._suite = suite
self._face = face
@property
def face(self):
return self._face
@property
def suite(self):
return self._suite
def __str__(self):
if self._face == 1:
face_str = 'A'
elif self._face == 11:
face_str = 'J'
elif self._face == 12:
face_str = 'Q'
elif self._face == 13:
face_str = 'K'
else:
face_str = str(self._face)
return '%s%s' % (self._suite, face_str)
def __repr__(self): # 类的一个专有方法,用来打印、转换
return self.__str__()
class Poker(object):
"""一副牌"""
def __init__(self):
"""
:cards: 保存了一整副牌
:current: 用来判断是否发了牌:current为0表示还没有发牌;每发一张拍current就会加1;同时也作为cards的索引,从而可以取到每一张牌
"""
self._cards = [Card(suite, face) # ???这个初始化看不懂????????????????????????????
for suite in '♠♥♣♦'
for face in range(1, 14)] # 没有大王小王...
self._current = 0 # _cards _current都是成员变量吗?参数里面没有啊?所以意思是只要出现在__init__函数中的就代表是成员变量??
@property
def cards(self):
return self._cards
def shuffle(self):
"""洗牌(随机乱序)"""
self._current = 0
random.shuffle(self._cards) # 这不是有吊用自己了吗?这怎么行。。。?????????????????????????????
@property
def next(self):
"""
发牌
:return: 返回发的那张牌
"""
card = self._cards[self._current] # current作为索引,取到将要发的牌self._cards[self._current]
self._current += 1 # 每发一张牌,surrent就变化
return card
@property
def has_next(self):
"""还有没有牌"""
return self._current < len(self._cards) # 判断是否发了牌,如果发完了牌,那么current的值应该是52(没有大王小王)
class Player(object):
"""玩家"""
def __init__(self, name):
"""
:name: 玩家姓名
:cards_on_hand: 玩家手中的牌
"""
self._name = name
self._cards_on_hand = []
@property
def name(self):
return self._name
@property
def cards_on_hand(self):
return self._cards_on_hand
def get(self, card):
"""摸牌"""
self._cards_on_hand.append(card)
def arrange(self, card_key):
"""玩家整理手上的牌"""
self._cards_on_hand.sort(key=card_key) # ?????????????????????????????????????????????
# 排序规则-先根据花色再根据点数排序
def get_key(card):
return (card.suite, card.face)
def main():
# 创建一副牌
p = Poker()
# 洗牌
p.shuffle()
# 建立玩家
players = [Player('东邪'), Player('西毒'), Player('南帝'), Player('北丐')]
for _ in range(13): # 牌的点数在1 ~ 13之间,所以这里进行限制
for player in players: # 玩家依次摸牌
player.get(p.next) # 玩家使用get()方法摸牌,摸到的牌是由next()发的
for player in players:
print(player.name + ':', end=' ')
player.arrange(get_key) # 玩家整理手中的牌?????????为什么参数是get_key,get_key是函数啊,不应该get_key()?
print(player.cards_on_hand) # cards_on_hand是个列表,保存了某个玩家的所有手中的牌
if __name__ == '__main__':
main()
- 练习3:工资结算系统
"""
某公司有三种类型的员工 分别是部门经理、程序员和销售员
需要设计一个工资结算系统 根据提供的员工信息来计算月薪
部门经理的月薪是每月固定15000元
程序员的月薪按本月工作时间计算 每小时150元
销售员的月薪是1200元的底薪加上销售额5%的提成
"""
from abc import ABCMeta, abstractmethod
class Employee(object, mataclass=ABCMeta):
"""员工"""
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
@abstractmethod
def get_salary(self):
"""
获得月薪
:return: 月薪
"""
pass
class Manager(Employee):
"""部门经理"""
def get_salary(self):
return 15000.0
class Programmer(Employee):
"""程序员"""
def __init__(self, name, working_hour=0):
super.__init__(name)
self._working_hour = working_hour
@property
def working_hour(self):
return self._working_hour
@working_hour.setter
def working_hour(self, working_hour):
self._working_hour = working_hour if working_hour > 0 else 0
def get_salary(self):
return 150.0 * self._working_hour
class Salesman(Employee):
"""销售员"""
def __init__(self, name, sale_num=0):
super.__init__(name)
self._sale_num = sale_num
@property
def sale_num(self):
return self._sale_num
@sale_num.setter
def sale_num(self, sale_num):
self._sale_num = sale_num if sale_num > 0 else 0
def get_salary(self):
return 1200.0 + self._sales * 0.05
def main():
emps = [
Manager('刘备'), Programmer('诸葛亮'),
Manager('曹操'), Salesman('荀彧'),
Salesman('吕布'), Programmer('张辽'),
Programmer('赵云')
]
for emp in emps:
if isinstance(emp, Programmer):
emp.working_hour = int(input('请输入%s本月工作时间: ' % emp.name))
elif isinstance(emp, Salesman):
emp.sales = float(input('请输入%s本月销售额: ' % emp.name))
# 同样是接收get_salary这个消息但是不同的员工表现出了不同的行为(多态)
print('%s本月工资为: ¥%s元' %
(emp.name, emp.get_salary()))
if __name__ == '__main__':
main()
三、图形用户界面和游戏开发
1、基于tkinter模块的GUI
Python默认的GUI开发模块是tkinter(在Python 3以前的版本中名为Tkinter),从这个名字就可以看出它是基于Tk的,Tk是一个工具包,最初是为Tcl设计的,后来被移植到很多其他的脚本语言中,它提供了跨平台的GUI控件。当然Tk并不是最新和最好的选择,也没有功能特别强大的GUI控件,事实上,开发GUI应用并不是Python最擅长的工作,如果真的需要使用Python开发GUI应用,wxPython、PyQt、PyGTK等模块都是不错的选择。
基本上使用tkinter来开发GUI应用需要以下5个步骤:
- 导入tkinter模块中我们需要的东西。
- 创建一个顶层窗口对象并用它来承载整个GUI应用。
- 在顶层窗口对象上添加GUI组件。
- 通过代码将这些GUI组件的功能组织起来。
- 进入主事件循环(main loop)。
下面的代码演示了如何使用tkinter做一个简单的GUI应用。
# Step1、导入tkinter模块中我们需要的东西
import tkinter
import tkinter.messagebox
def main():
flag = True
# 定义按钮触发的事件:修改标签上的文字?????????????
def change_label_text():
nonlocal flag
flag = not flag
color, msg = ('red', 'Hello, world!')\
if flag else ('blue', 'Goodbye, world!')
label.config(text=msg, fg=color)
# 定义按钮触发的事件:确认退出?????????????
def confirm_to_quit():
if tkinter.messagebox.askokcancel('温馨提示', '确定要退出吗?'):
top.quit() # 退出顶级会话窗口
# Step2、创建一个顶层窗口对象并用它来承载整个GUI应用
top = tkinter.Tk()
# 2.1 设置顶层窗口的大小
top.geometry('240x160') # 小写字母x
# 2.2 设置窗口标题
top.title('小游戏')
# Step3、在顶层窗口对象上添加GUI组件
# 3.1 创建标签对象并添加到顶层窗口
label = tkinter.Label(top, text='Hello, world!', font='Arial -32', fg='red')
label.pack(expand=1)
# 3.2 创建按钮对象
# 3.2.1 创建一个装按钮的容器
panel = tkinter.Frame(top)
# 3.2.2 创建按钮对象:指定添加到哪个容器中,通过command参数绑定事件回调函数
button1 = tkinter.Button(panel, text='修改', command=change_label_text)
button1.pack(side='left') # pack方法用来设置组件放置的位置
button2 = tkinter.Button(panel, text='退出', command=confirm_to_quit)
button2.pack(side='right')
panel.pack(side='bottom')
# Step4、开启主事件循环
tkinter.mainloop()
if __name__ == '__main__':
main()
点击“修改”:
点击“退出”:
需要说明的是,GUI应用通常是事件驱动式的,之所以要进入主事件循环就是要监听鼠标、键盘等各种事件的发生并执行对应的代码对事件进行处理,因为事件会持续的发生,所以需要这样的一个循环一直运行着等待下一个事件的发生。另一方面,Tk为控件的摆放提供了三种布局管理器,通过布局管理器可以对控件进行定位,这三种布局管理器分别是:Placer(开发者提供控件的大小和摆放位置)、Packer(自动将控件填充到合适的位置)和Grid(基于网格坐标来摆放控件),此处不进行赘述。
2、使用Pygame进行游戏开发
Pygame 是一个开源的Python模块,专门用于多媒体应用(如电子游戏)的开发,其中 包含对图像、声音、视频、事件、碰撞等的支持 。Pygame建立在SDL的基础上,SDL是一套跨平台的多媒体开发库,用C语言实现,被广泛的应用于游戏、模拟器、播放器等的开发。而 Pygame让游戏开发者不再被底层语言束缚,可以更多的关注游戏的功能和逻辑 。
下面我们来完成一个简单的小游戏,游戏的名字叫“大球吃小球”,当然完成这个游戏并不是重点,学会使用Pygame也不是重点,最重要的我们要在这个过程中 体会如何使用前面讲解的面向对象程序设计 , 学会用这种编程思想去解决现实中的问题 。
注意:开始之前,需要下载Pygame模块,下载步骤请点击这里。
制作游戏窗口
import pygame
def main():
# 初始化导入的pygame中的模块
pygame.init()
# 初始化用于显示的窗口并设置窗口尺寸
screen = pygame.display.set_mode((800, 600))
# 设置当前窗口的标题
pygame.display.set_caption('大球吃小球')
running = True
# 开启一个事件循环处理发生的事件
while running:
# 从消息队列中获取事件并对事件进行处理
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if __name__ == '__main__':
main()
在窗口中绘图
可以通过pygame中draw模块的函数在窗口上绘图,可以绘制的图形包括:线条、矩形、多边形、圆、椭圆、圆弧等。需要说明的是,屏幕坐标系是将屏幕左上角设置为坐标原点(0, 0),向右是x轴的正向,向下是y轴的正向,在表示位置或者设置尺寸的时候,我们默认的单位都是像素。所谓像素就是屏幕上的一个点,你可以用浏览图片的软件试着将一张图片放大若干倍,就可以看到这些点。pygame中表示颜色用的是色光三原色表示法,即通过一个元组或列表来指定颜色的RGB值,每个值都在0~255之间,因为是每种原色都用一个8位(bit)的值来表示,三种颜色相当于一共由24位构成,这也就是常说的“24位颜色表示法”。
import pygame
def main():
# 初始化导入的pygame中的模块
pygame.init()
# 初始化用于显示的窗口并设置窗口尺寸
screen = pygame.display.set_mode((800, 600))
# 设置当前窗口的标题
pygame.display.set_caption('大球吃小球')
### 新增部分START
# 设置窗口的背景色(颜色是由红绿蓝三原色构成的元组)
screen.fill((242, 242, 242))
# 绘制一个圆(参数分别是: 屏幕, 颜色, 圆心位置, 半径, 0表示填充圆)
pygame.draw.circle(screen, (255, 0, 0), (100, 100), 30, 0)
# 刷新当前窗口(渲染窗口将绘制的图像呈现出来)
pygame.display.flip()
### 新增部分END
running = True
# 开启一个事件循环处理发生的事件
while running:
# 从消息队列中获取事件并对事件进行处理
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if __name__ == '__main__':
main()
加载图像
如果需要直接加载图像到窗口上,可以使用pygame中image模块的函数来加载图像,再通过之前获得的窗口对象的blit方法渲染图像,代码如下所示。
import pygame
def main():
# 初始化导入的pygame中的模块
pygame.init()
# 初始化用于显示的窗口并设置窗口尺寸
screen = pygame.display.set_mode((800, 600))
# 设置当前窗口的标题
pygame.display.set_caption('大球吃小球')
### 新增部分START
# 设置窗口的背景色(颜色是由红绿蓝三原色构成的元组)
screen.fill((255, 255, 255))
# 通过指定的文件名加载图像
ball_image = pygame.image.load('E:/fdj.png')
# 在窗口上渲染图像
screen.blit(ball_image, (50, 50))
### 新增部分END
# - 绘制一个圆(参数分别是: 屏幕, 颜色, 圆心位置, 半径, 0表示填充圆)
# - pygame.draw.circle(screen, (255, 0, 0), (100, 100), 30, 0)
# 刷新当前窗口(渲染窗口将绘制的图像呈现出来)
pygame.display.flip()
running = True
# 开启一个事件循环处理发生的事件
while running:
# 从消息队列中获取事件并对事件进行处理
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if __name__ == '__main__':
main()
实现动画效果
说到动画这个词大家都不会陌生,事实上要实现动画效果,本身的原理也非常简单,就是 将不连续的图片连续的播放 ,只要 每秒钟达到了一定的帧数 ,那么就可以做出比较流畅的动画效果。 如果要让上面代码中的小球动起来,可以将小球的位置用变量来表示,并在循环中修改小球的位置再刷新整个窗口即可 。
import pygame
def main():
# 初始化导入的pygame中的模块
pygame.init()
# 初始化用于显示的窗口并设置窗口尺寸
screen = pygame.display.set_mode((800, 600))
# 设置当前窗口的标题
pygame.display.set_caption('大球吃小球')
### 增加/修改部分START
# 定义变量来表示小球在屏幕上的位置
x, y = 50, 50
running = True
# 开启一个事件循环处理发生的事件
while running:
# 从消息队列中获取事件并对事件进行处理
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# 设置窗口的背景色(颜色是由红绿蓝三原色构成的元组)
screen.fill((255, 255, 255))
# 绘制一个圆(参数分别是: 屏幕, 颜色, 圆心位置, 半径, 0表示填充圆)
pygame.draw.circle(screen, (255, 0, 0,), (x, y), 30, 0)
# 刷新当前窗口(渲染窗口将绘制的图像呈现出来)
pygame.display.flip()
# 每隔50毫秒就改变小球的位置再刷新窗口
pygame.time.delay(50)
x, y = x + 5, y + 5
### 增加/修改部分END
if __name__ == '__main__':
main()
碰撞检测
通常一个游戏中会有很多对象出现,而这些对象之间的“碰撞”在所难免,比如炮弹击中了飞机、箱子撞到了地面等。 碰撞检测在绝大多数的游戏中都是一个必须得处理的至关重要的问题 , pygame的sprite(动画精灵)模块就提供了对碰撞检测的支持 ,这里我们暂时不介绍sprite模块提供的功能,因为 要检测两个小球有没有碰撞其实非常简单,只需要检查球心的距离有没有小于两个球的半径之和 。 为了制造出更多的小球,我们可以通过对鼠标事件的处理,在点击鼠标的位置创建颜色、大小和移动速度都随机的小球,当然要做到这一点,我们可以把之前学习到的面向对象的知识应用起来 。
from enum import Enum, unique
from math import sqrt
from random import randint
import pygame
@unique
class Color(Enum):
"""颜色"""
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GRAY = (242, 242, 242)
@staticmethod
def random_color():
"""获得随机颜色"""
r = randint(0, 255)
g = randint(0, 255)
b = randint(0, 255)
return (r, g, b)
class Ball(object):
"""球"""
def __init__(self, x, y, radius, sx, sy, color=Color.RED):
"""初始化方法"""
self.x = x
self.y = y
self.radius = radius
self.sx = sx
self.sy = sy
self.color = color
self.alive = True
def move(self, screen):
"""移动"""
self.x += self.sx
self.y += self.sy
if self.x - self.radius <= 0 or self.x + self.radius >= screen.get_width():
# 超过边界了,所以去“对过”
self.sx = -self.sx
if self.y - self.radius <= 0 or self.y + self.radius >= screen.get_height():
self.sy = -self.sy
def eat(self, other):
"""吃其他球"""
if self.alive and other.alive and self != other:
dx, dy = self.x - other.x, self.y - other.y
distance = sqrt(dx ** 2 + dy ** 2)
if distance < self.radius + other.radius and self.radius > other.radius:
# 检查球心的距离有没有小于两个球的半径之和,如果小于,则没有碰撞;
# 检查本球的半径是否大于其他球的半径,大于的话,才能吃其他球。
other.alive = False
self.radius = self.radius + int(other.radius * 0.146)
def draw(self, screen):
"""在窗口上绘制球"""
pygame.draw.cicle(screen, self.color, (self.x, self.y), self.radius, 0)
事件处理
可以在事件循环中对 鼠标事件 进行处理, 通过事件对象的type属性可以判定事件类型,再通过pos属性就可以获得鼠标点击的位置 。如果要处理 键盘事件 也是在这个地方,做法与处理鼠标事件类似。
from enum import Enum, unique
from math import sqrt
from random import randint
import pygame
@unique
class Color(Enum):
"""颜色"""
# 设置下面这些有何用?
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GRAY = (242, 242, 242)
@staticmethod
def random_color():
"""获得随机颜色"""
r = randint(0, 255)
g = randint(0, 255)
b = randint(0, 255)
return (r, g, b)
class Ball(object):
"""球"""
def __init__(self, x, y, radius, sx, sy, color=Color.RED):
"""
初始化方法
:x/y: 球的圆心所在的位置的坐标
:sx/sy: 将要在x/y轴上移动的距离
:radius: 圆的半径
:color: 圆的填充色
"""
self.x = x
self.y = y
self.radius = radius
self.sx = sx
self.sy = sy
self.color = color
self.alive = True
def move(self, screen):
"""移动"""
self.x += self.sx
self.y += self.sy
if self.x - self.radius <= 0 or self.x + self.radius >= screen.get_width():
# 超过边界了,所以去“对过”
self.sx = -self.sx
if self.y - self.radius <= 0 or self.y + self.radius >= screen.get_height():
self.sy = -self.sy
def eat(self, other):
"""吃其他球"""
if self.alive and other.alive and self != other:
dx, dy = self.x - other.x, self.y - other.y
distance = sqrt(dx ** 2 + dy ** 2)
if distance < self.radius + other.radius and self.radius > other.radius:
# 检查球心的距离有没有小于两个球的半径之和,如果小于,则没有碰撞;
# 检查本球的半径是否大于其他球的半径,大于的话,才能吃其他球。
other.alive = False
self.radius = self.radius + int(other.radius * 0.146)
def draw(self, screen):
"""在窗口上绘制球"""
pygame.draw.circle(screen, self.color, (self.x, self.y), self.radius, 0)
def main():
# 1、定义用来装有所有球的容器
balls = []
# 2、初始化导入的pygame中的模块
pygame.init()
# 3.1、初始化 用于显示的窗口 并 设置窗口尺寸
screen = pygame.display.set_mode((800, 600))
# 3.2、设置当前窗口的标题
pygame.display.set_caption('大球吃小球')
running = True
# 4、开启一个事件循环处理发生的事件
while running:
# 4.1、从消息队列中获取事件并对事件进行处理——鼠标点击到的位置将创建一个球
for event in pygame.event.get():# ?????如何结束这个for循环呢,这个不断创建小球的for循环??
# 4.1.0、判断用户是否点击了关闭按钮
if event.type == pygame.QUIT:
running = False
# 4.1.1、通过事件对象的type属性可以判定事件类型,下面是处理鼠标事件的代码
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
# 4.1.1.1、通过pos属性获得点击鼠标的位置
x, y = event.pos
# 4.1.1.2、在点击鼠标的位置创建一个球(大小、速度和颜色随机)
radius = randint(10, 100) # 半径
sx, sy = randint(-10, 10), randint(-10, 10)
color = Color.random_color()
ball = Ball(x, y, radius, sx, sy, color)
# 4.1.1.3、将球添加到列表容器中
balls.append(ball)
# 4.2、设置窗口的背景色(颜色是由红绿蓝三原色构成的元组)
screen.fill((255, 255, 255))
# 4.3、取出容器中的球,如果没被吃掉就绘制,被吃掉了就移除
for ball in balls:
if ball.alive:
ball.draw(screen)
else:
balls.remove(ball) # ???????????//没有此方法啊
# 4.4、刷新当前窗口(渲染窗口将绘制的图像呈现出来)
pygame.display.flip()
# 4.5、每隔50毫秒就改变球的位置再刷新窗口
pygame.time.delay(50)
for ball in balls:
ball.move(screen)
# 检查球有没有吃到其他的球
for other in balls:
ball.eat(other)
if __name__ == '__main__':
main()
(PS. 运行过程并不能深入理解,因为还没有学习event事件等相关内容,此处暂时不处理,先继续向下学习。否则自己去找资源学习的话,不一定能学到准确的点上,且目前也不是针对那些的学习。)
运行效果如下:用户可以一直点击屏幕,每点击一个地方就出现一个圆,这些圆不停地移动,当发生碰撞时,大球将消灭掉小球,同时大球的半径会增加。
准确的说它算不上一个游戏,但是做一个小游戏的基本知识我们已经通过这个例子告诉大家了,有了这些知识已经可以开始你的小游戏开发之旅了。其实上面的代码中还有很多值得改进的地方,比如刷新窗口以及让球移动起来的代码并不应该放在事件循环中,等学习了多线程的知识后,用一个后台线程来处理这些事可能是更好的选择。如果希望获得更好的用户体验,我们还可以在游戏中加入背景音乐以及在球与球发生碰撞时播放音效,利用pygame的mixer和music模块,我们可以很容易的做到这一点,大家可以自行了解这方面的知识。事实上,想了解更多的关于pygame的知识,最好的教程是pygame的官方网站,如果英语没毛病就可以赶紧去看看啦。 如果想开发3D游戏,pygame就显得力不从心了,对3D游戏开发如果有兴趣的读者不妨看看Panda3D。