2023-03-28 | Python学习日记 (2)

均出自:https://github.com/jackfrued/Python-100-Days

python解释器

python是解释性语言。python解释器同一时间只能运行一个程序的一条语句。

服务器中可以通过输入python来进入python,输入quit()来退出

01. python的语言元素

变量和类型

整型:Python中可以处理任意大小的整数(Python 2.x中有int和long两种类型的整数,但这种区分对Python来说意义不大,因此在Python 3.x中整数只有int这一种了),而且支持二进制(如0b100,换算成十进制是4)、八进制(如0o100,换算成十进制是64)、十进制(100)和十六进制(0x100,换算成十进制是256)的表示法。
浮点型:浮点数也就是小数,之所以称为浮点数,是因为按照科学记数法表示时,一个浮点数的小数点位置是可变的,浮点数除了数学写法(如123.456)之外还支持科学计数法(如1.23456e2)。
字符串型:字符串是以单引号或双引号括起来的任意文本,比如'hello'和"hello",字符串还有原始字符串表示法、字节字符串表示法、Unicode字符串表示法,而且可以书写成多行的形式(用三个单引号或三个双引号开头,三个单引号或三个双引号结尾)。
布尔型:布尔值只有True、False两种值,要么是True,要么是False,在Python中,可以直接用True、False表示布尔值(请注意大小写),也可以通过布尔运算计算出来(例如3 < 5会产生布尔值True,而2 == 1会产生布尔值False)。
复数型:形如3+5j,跟数学上的复数表示一样,唯一不同的是虚部的i换成了j。实际上,这个类型并不常用,大家了解一下就可以了。

变量命名

硬性规则:

1、变量名由字母、数字和下划线构成,数字不能开头。
2、大小写敏感(大写和小写时两个不同的变量)
3、不要跟关键字(有特殊含义的单词)和系统保留字(如函数、模块等的名字)冲突

PEP 8要求:

1、用小写字母拼写,多个单词用下划线连接
2、受保护的实例属性用单个下划线开头
3、私有的实例属性用两个下划线开头

python中可以使用type函数对变量的类型进行检查。程序设计中的函数的概念跟数学上函数的概念是一致的。

python中内置的函数可对变量类型进行转换

**int()** : 将一个数值或字符串转换成整数,可以指定进制。

**float()** : 将一个字符串转换成浮点数

**str()**:将指定的对象转换成字符串形式,可以指定编码。

**chr()**:将整数转换成该编码对应的字符串(一个字符)。

**ord()**:将字符串(一个字符)转换成对应的编码(整数)。
image.png

上面的print函数中输出的字符串使用了占位符语法

**%d** 是整数的占位符,

**%f** 是小数的占位符,

**%%** 表示百分号(因为百分号代表了占位符,所以带占位符的字符串中要表示百分号必须写成%%),

字符串之后的%后面跟的变量值会替换掉占位符然后输出到终端中。

运算符:

image.png

python支持多种运算符,上图是运算符按照优先级从高到低的顺序列出了所有运算符。

!!!但是可以使用括号来确保运算的执行顺序。

赋值运算符:

它的作用是将右边的值赋给左边的变量。

比较运算符和逻辑运算符:

比较运算符也叫关系运算符,包括==****、!=****、<****、>****、<=****、>=
比较运算符会产生布尔值,要么是True要么是False

逻辑运算符有三个,分别是andornot
and字面意思是“而且”,所以and运算符会连接两个布尔值,如果两个布尔值都是True,那么运算的结果就是True;左右两边的布尔值有一个是False,最终的运算结果就是False
not运算符的后面会跟上一个布尔值,它的作用是得到与该布尔值相反的值,也就是说,后面的布尔值如果是True运算结果就是False,而后面的布尔值如果是False则运算结果就是True。

比较运算符的优先级高于赋值运算符

02.分支结构

If语句的使用

python中,构造分支结构可以使用ifelifelse关键字。

所谓关键字就是有特殊含义的单词,像ifelse就是专门用于构造分支结构的关键字,很显然你不能够使用它作为变量名(事实上,用作其他的标识符也是不可以)。

C/C++Java等语言不同,Python中没有用花括号来构造代码块而是使用了缩进的方式来表示代码的层次结构,如果if条件成立的情况下需要执行多条语句,只要保持多条语句具有相同的缩进就可以了。

换句话说连续的代码如果又保持了相同的缩进那么它们属于同一个代码块,相当于是一个执行的整体。缩进可以使用任意数量的空格,但通常使用4个空格,建议不要使用制表键或者设置你的代码编辑工具自动将制表键变成4个空格。

当然如果要构造出更多的分支,可以使用if...elif...else...结构或者嵌套的if...else...结构

根据实际开发的需要,分支结构是可以嵌套的。

03.循环结构

for-in循环

如果明确的知道循环执行的次数或者要对一个容器进行迭代,那么推荐使用for-in循环

for循环求0-100之间的和

sum = 0

for x in range(101):

 sum += x

print(sum)

range的用法非常灵活,下面给出了一个例子:

range(101):可以用来产生0到100范围的整数,需要注意的是取不到101。

range(1, 101):可以用来产生1到100范围的整数,相当于前面是开区间后面是闭区间。

range(1, 101, 2):可以用来产生1到100的奇数,其中2是步长,即每次数值递增的值。

range(100, 0, -2):可以用来产生100到1的偶数,其中-2是步长,即每次数字递减的值。

for循环求0-100之间偶数的和

sum = 0

for x in range(100,0,-2):

 sum += x

print(sum)

或

sum = 0

for x in range(2,101,2):

 sum += x

print(sum)

while 循环

如果要构造不知道具体循环次数的循环结构,推荐使用while循环。while循环通过一个能够产生或转换出bool值的表达式来控制循环,表达式的值为True则继续循环;表达式的值为False则结束循环

04、函数和模块的使用

定义函数:

python中可以使用def关键字来定义函数,和变量一样,每个函数也有一个响亮的名字,而且命名规则跟变量的命名规则是一致的。

在函数名后面的圆括号中可以放置传递给函数的参数,这一点和数学上的函数非常相似,程序中函数的参数就相当于是数学上说的函数的自变量,而函数执行完成后我们可以通过return关键字来返回一个值,这相当于数学上说的函数的因变量。

def fac(num):
   result = 1

   for n in range(1,num+1):
   result *= n
   return result

m = int(input('m = '))
n = int(input('n = '))
print(fac(m) // fac(n) // fac(m - n))

函数的参数:

在Python中,函数的参数可以有默认值,也支持使用可变参数

在传递参数时,可不按照设定的顺序进行传递

# 在参数名前面的*表示args是一个可变参数

def add(*args):
 total = 0

 for val in args:
 total += val
 return total

# 在调用add函数时可以传入0个或多个参数

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:

from module1 import foo
# 输出hello, world!
foo()

from module2 import foo
# 输出goodbye, world!
foo()

方式2:

import module1 as md1

import module2 as md2

md1.foo()

md2.foo()

需要说明的是,如果我们导入的模块除了定义函数之外还有可执行代码,那么Python解释器在导入这个模块时就会执行这些代码,事实上我们可能并不希望如此.

因此如果我们在模块中编写了执行代码,最好是将这些执行代码放入如下所示的条件中.
if __name__ == '__main__'

这样的话除非直接运行该模块,if条件下的这些代码是不会执行的
因为只有直接执行的模块的名字才是"__main__"。

__name__ 是Python中一个隐含的变量它代表了模块的名字

只有被Python解释器直接执行的模块的名字才是 __main__

变量的作用域:

python中可以在函数内部再定义函数

可以使用global关键字来指示foo函数中的变量a来自于全局作用域,如果全局作用域中没有a,那么下面一行的代码就会定义变量a并将其置于全局作用域。

def foo():
   global a
   a = 200
   print(a) # 200

if __name__ == '__main__':
   a = 100
   foo()
   print(a) # 200

05、字符串和常用数据结构

使用字符串

在Python程序中,如果我们把单个或多个字符用单引号或者双引号包围起来,就可以表示一个字符串。

s1 = 'hello, world!'
s2 = "hello, world!"

# 以三个双引号或单引号开头的字符串可以折行

s3 = """
hello,
world!
"""
print(s1, s2, s3, end='')

可以在字符串中使用 \ 表示转义,\ 后面的字符不再是它原来的意义。想表示 \ 要写成 \
如果不希望字符串中的 \ 表示转义,我们可以通过在字符串的最前面加上字母r来加以说明

Python为字符串类型提供了非常丰富的运算符
我们可以使用+运算符来实现字符串的拼接
可以使用*运算符来重复一个字符串的内容
可以使用in和not in来判断一个字符串是否包含另外一个字符串(成员运算)
我们也可以用[]和[:]运算符从字符串取出某个字符或某些字符(切片运算)

s1 = 'hello ' * 3
print(s1) # hello hello hello

s2 = 'world'
s1 += s2
print(s1) # hello hello hello world
print('ll' in s1) # True
print('good' in s1) # False

str2 = 'abc123456'
# 从字符串中取出指定位置的字符(下标运算)
print(str2[2]) # c

# 字符串切片(从指定的开始索引到指定的结束索引)
print(str2[2:5]) # c12
print(str2[2:]) # c123456
print(str2[2::2]) # c246
print(str2[::2]) # ac246
print(str2[::-1]) # 654321cba
print(str2[-3:-1]) # 45

在Python中,我们还可以通过一系列的方法来完成对字符串的处理

str1 = 'hello, world!'
# 通过内置函数len计算字符串的长度
print(len(str1)) # 13

# 获得字符串首字母大写的拷贝
print(str1.capitalize()) # Hello, world!

# 获得字符串每个单词首字母大写的拷贝
print(str1.title()) # Hello, World!

# 获得字符串变大写后的拷贝
print(str1.upper()) # HELLO, WORLD!

# 从字符串中查找子串所在位置
print(str1.find('or')) # 8
print(str1.find('shit')) # -1

# 与find类似但找不到子串时会引发异常
# print(str1.index('or'))
# print(str1.index('shit'))
# 检查字符串是否以指定的字符串开头
print(str1.startswith('He')) # False
print(str1.startswith('hel')) # True

# 检查字符串是否以指定的字符串结尾
print(str1.endswith('!')) # True

# 将字符串以指定的宽度居中并在两侧填充指定的字符
print(str1.center(50, '*'))

# 将字符串以指定的宽度靠右放置左侧填充指定的字符
print(str1.rjust(50, ' '))
str2 = 'abc123456'

# 检查字符串是否由数字构成
print(str2.isdigit()) # False

# 检查字符串是否以字母构成
print(str2.isalpha()) # False

# 检查字符串是否以数字和字母构成
print(str2.isalnum()) # True
str3 = ' [email protected] '
print(str3)

# 获得字符串修剪左右两侧空格之后的拷贝
print(str3.strip())

使用列表

列表(list),也是一种结构化的、非标量类型,它是值的有序序列,每个值都可以通过索引进行标识,定义列表可以将列表的元素放在[]中,多个元素用,进行分隔,可以使用for循环对列表元素进行遍历,也可以使用 [ ][:] 运算符取出列表中的一个或多个元素。

# 通过循环用下标遍历列表元素
for index in range(len(list1)):
   print(list1[index])

# 通过for循环遍历列表元素
for elem in list1:
   print(elem)

# 通过enumerate函数处理列表之后再遍历可以同时获得元素索引和值
for index, elem in enumerate(list1):
   print(index, elem)

向列表中添加元素以及如何从列表中移除元素

list1 = [1, 3, 5, 7, 100]
# 添加元素
list1.append(200)
list1.insert(1, 400)

# 合并两个列表
# list1.extend([1000, 2000])
list1 += [1000, 2000]
print(list1) # [1, 400, 3, 5, 7, 100, 200, 1000, 2000]
print(len(list1)) # 9

# 先通过成员运算判断元素是否在列表中,如果存在就删除该元素
if 3 in list1:
   list1.remove(3)

if 1234 in list1:
   list1.remove(1234)

print(list1) # [1, 400, 5, 7, 100, 200, 1000, 2000]

# 从指定的位置删除元素
list1.pop(0)
list1.pop(len(list1) - 1)
print(list1) # [400, 5, 7, 100, 200, 1000]

# 清空列表元素
list1.clear()
print(list1) # []

和字符串一样,列表也可以做切片操作,通过切片操作我们可以实现对列表的复制或者将列表中的一部分取出来创建出新的列表

fruits = ['grape', 'apple', 'strawberry', 'waxberry']
fruits += ['pitaya', 'pear', 'mango']
# 列表切片
fruits2 = fruits[1:4]
print(fruits2) # apple strawberry waxberry

# 可以通过完整切片操作来复制列表
fruits3 = fruits[:]
print(fruits3) # ['grape', 'apple', 'strawberry', 'waxberry', 'pitaya', 'pear', 'mango']

fruits4 = fruits[-3:-1]
print(fruits4) # ['pitaya', 'pear']

# 可以通过反向切片操作来获得倒转后的列表的拷贝
fruits5 = fruits[::-1]
print(fruits5) # ['mango', 'pear', 'pitaya', 'waxberry', 'strawberry', 'apple', 'grape']
list1 = ['orange', 'apple', 'zoo', 'internationalization', 'blueberry']
list2 = sorted(list1)

# sorted函数返回列表排序后的拷贝不会修改传入的列表
# 函数的设计就应该像sorted函数一样尽可能不产生副作用
list3 = sorted(list1, reverse=True)

# 通过key关键字参数指定根据字符串长度进行排序而不是默认的字母表顺序
list4 = sorted(list1, key=len)
print(list1)
print(list2)
print(list3)
print(list4)

# 给列表对象发出排序消息直接在列表对象上进行排序
list1.sort(reverse=True)
print(list1)

生成式和生成器

可以使用列表的生成式语法来创建列表

f = [x for x in range(1, 10)]
print(f)
f = [x + y for x in 'ABCDE' for y in '1234567']
print(f)

# 用列表的生成表达式语法创建列表容器
# 用这种语法创建列表之后元素已经准备就绪所以需要耗费较多的内存空间
f = [x ** 2 for x in range(1, 1000)]
print(sys.getsizeof(f)) # 查看对象占用内存的字节数
print(f)

# 请注意下面的代码创建的不是一个列表而是一个生成器对象
# 通过生成器可以获取到数据但**它不占用额外的空间存储数据**
# 每次需要数据的时候就通过内部的运算得到数据(**需要花费额外的时间**)
f = (x ** 2 for x in range(1, 1000))
print(sys.getsizeof(f)) # 相比生成式生成器不占用存储数据的空间
print(f)

for val in f:
   print(val)

Python中还有另外一种定义生成器的方式,就是通过yield关键字将一个普通函数改造成生成器函数

def fib(n):
   a, b = 0, 1
   for _ in range(n):
   a, b = b, a + b
   yield a

def main():
   for val in fib(20):
   print(val)

if __name__ == '__main__':
   main()

使用元组

Python中的元组与列表类似也是一种容器数据类型,可以用一个变量(对象)来存储多个数据,不同之处在于元组的元素不能修改,在前面的代码中我们已经不止一次使用过元组了。顾名思义,我们把多个元素组合到一起就形成了一个元组,所以它和列表一样可以保存多条数据。

# 定义元组
t = ('随风', 23, True, '中国')
print(t)

# 获取元组中的元素
print(t[0])
print(t[3])

# 遍历元组中的值
for member in t:
   print(member)

# 重新给元组赋值
# t[0] = '王大锤' # TypeError
# 变量t重新引用了新的元组原来的元组将被垃圾回收
t = ('王大锤', 20, True, '云南昆明')
print(t)

# 将元组转换成列表
person = list(t)
print(person)

# 列表是可以修改它的元素的
person[0] = '李小龙'
person[1] = 25
print(person)

# 将列表转换成元组
fruits_list = ['apple', 'banana', 'orange']
fruits_tuple = tuple(fruits_list)
print(fruits_tuple)

元组中的元素是无法修改的,事实上我们在项目中尤其是多线程环境中可能更喜欢使用的是那些不变对象(一方面因为对象状态不能修改,所以可以避免由此引起的不必要的程序错误,简单的说就是一个不变的对象要比可变的对象更加容易维护;另一方面因为没有任何一个线程能够修改不变对象的内部状态,一个不变对象自动就是线程安全的,这样就可以省掉处理同步化的开销。一个不变对象可以方便的被共享访问)。
所以结论就是:如果不需要对元素进行添加、删除、修改的时候,可以考虑使用元组,当然如果一个方法要返回多个值,使用元组也是不错的选择。

元组在创建时间和占用的空间上面都优于列表。我们可以使用sys模块的getsizeof函数来检查存储同样的元素的元组和列表各自占用了多少内存空间,这个很容易做到。

使用集合

Python中的集合跟数学上的集合是一致的,不允许有重复元素,而且可以进行交集、并集、差集等运算

# 创建集合的字面量语法
set1 = {1, 2, 3, 3, 3, 2}
print(set1)
print('Length =', len(set1))

# 创建集合的构造器语法(面向对象部分会进行详细讲解)
set2 = set(range(1, 10))
set3 = set((1, 2, 3, 3, 2, 1))
print(set2, set3)

# 创建集合的推导式语法(推导式也可以用于推导集合)
set4 = {num for num in range(1, 100) if num % 3 == 0 or num % 5 == 0}
print(set4)

Python中允许通过一些特殊的方法来为某种类型或数据结构自定义运算符

使用字典

字典是另一种可变容器模型,Python中的字典跟我们生活中使用的字典是一样一样的,它可以存储任意类型对象,与列表、集合不同的是,字典的每个元素都是由一个键和一个值组成的“键值对”,键和值通过冒号分开。下面的代码演示了如何定义和使用字典。

# 创建字典的字面量语法
scores = {'归墟': 95, '白元芳': 78, '狄仁杰': 82}
print(scores)

# 创建字典的构造器语法
items1 = dict(one=1, two=2, three=3, four=4)

# 通过zip函数将两个序列压成字典
items2 = dict(zip(['a', 'b', 'c'], '123'))

# 创建字典的推导式语法
items3 = {num: num ** 2 for num in range(1, 10)}
print(items1, items2, items3)

# 通过键可以获取字典中对应的值
print(scores['归墟'])
print(scores['狄仁杰'])

# 对字典中所有键值对进行遍历
for key in scores:
   print(f'{key}: {scores[key]}')

# 更新字典中的元素
scores['白元芳'] = 65
scores['诸葛王朗'] = 71
scores.update(冷面=67, 方启鹤=85)
print(scores)

if '武则天' in scores:
   print(scores['武则天'])

print(scores.get('武则天'))

# get方法也是通过键获取对应的值但是可以设置默认值
print(scores.get('武则天', 60))

# 删除字典中的元素
print(scores.popitem())
print(scores.popitem())
print(scores.pop('归墟', 100))

# 清空字典
scores.clear()
print(scores)

在屏幕显示跑马灯文字

import os
import time

def main():
   content = '北京欢迎你为你开天辟地…………'
   while True:
 # 清理屏幕上的输出
   os.system('cls') # os.system('clear')
   print(content)

 # 休眠200毫秒
   time.sleep(0.2)
   content = content[1:] + content[0]

if __name__ == '__main__':
   main()

设计函数产生指定长度的验证码,验证码由大小写字母和数字构成

import random

def generate_code(code_len=4):
    """
    生成指定长度的验证码
    :param code_len: 验证码的长度(默认4个字符)
    :return: 由大小写英文字母和数字构成的随机验证码
    """
    all_chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
    last_pos = len(all_chars) - 1
    code = ''
    for _ in range(code_len):
        index = random.randint(0, last_pos)
        code += all_chars[index]
    return code

设计一个函数返回给定文件名的后缀名

def get_suffix(filename, has_dot=False):

   """
   获取文件名的后缀名
   :param filename: 文件名
   :param has_dot: 返回的后缀名是否需要带点
   :return: 文件的后缀名
   """
   pos = filename.rfind('.')

   if 0 < pos < len(filename) - 1:
       index = pos if has_dot else pos + 1
       return filename[index:]
   else:
       return ''

06、面向对象编程基础

定义类:

在Python中可以使用class关键字定义类,然后在类中通过之前学习过的函数来定义方法,这样就可以将对象的动态特征描述出来

class Student(object):

   # __init__是一个特殊方法用于在创建对象时进行初始化操作
   # 通过这个方法我们可以为学生对象绑定name和age两个属性
   def __init__(self, name, age):
       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('骆昊', 38)
   # 给对象发study消息
   stu1.study('Python程序设计')
   # 给对象发watch_av消息
   stu1.watch_movie()
   stu2 = Student('王大锤', 15)
   stu2.study('思想品德')
   stu2.watch_movie()

if __name__ == '__main__':
   main()

访问可见性问题

对于上面的代码,有C++、Java、C#等编程经验的程序员可能会问,我们给Student对象绑定的name和age属性到底具有怎样的访问权限(也称为可见性)。因为在很多面向对象编程语言中,我们通常会将对象的属性设置为私有的(private)或受保护的(protected),简单的说就是不允许外界访问,而对象的方法通常都是公开的(public),因为公开的方法就是对象能够接受的消息。

在Python中,属性和方法的访问权限只有两种,也就是公开的和私有的,如果希望属性是私有的,在给属性命名时可以用两个下划线作为开头,下面的代码可以验证这一点。

class Test:
   def __init__(self, foo):
       self.__foo = foo

   def __bar(self):
       print(self.__foo)
       print('__bar')

def main():
       test = Test('hello')# AttributeError: 'Test' object has no attribute '__bar'
       test.__bar() # AttributeError: 'Test' object has no attribute '__foo'
       print(test.__foo)

if __name__ == "__main__":
   main()

定义一个类描述数字时钟

from time import sleep

class Clock(object):
    """数字时钟"""

    def __init__(self, hour=0, minute=0, second=0):
        """初始化方法
        :param hour: 时
        :param minute: 分
        :param 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()

if __name__ == '__main__':
    main()

定义一个类描述平面上的点并提供移动点和计算到另一个点距离的方法

from math import sqrt

class Point(objiect):

    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

    def move_to(self, x, y):
        """移动到指定位置"""
        self.x = x
        self.y = y

    def move_by(self, dx ,dy):
        """相对原位置增加多少"""
        self.x += dx
        self.y += dy

    def distance_to(self, another):
        """移动的距离"""
        dx = self.x - another.x
        dy = self.y - another.y
        return sqrt(dx ** 2 + dy ** 2)

    def __str__(self):
        return '(%s, %s)' % (str(self.x), str(self.y))**

def main():
    p1 = Point(3, 5)
    p2 = Point()
    print(p1)
    print(p2)
    p2.move_by(-1, 2)
    print(p2)
    print(p1.distance_to(p2))

if __name__ == '__main__':
    main()

07、面向对象进阶

@property装饰器

之前我们讨论过Python中属性和方法访问权限的问题,虽然我们不建议将属性设置为私有的,但是如果直接将属性暴露给外界也是有问题的,比如我们没有办法检查赋给属性的值是否有效。

我们之前的建议是将属性命名以单下划线开头,通过这种方式来暗示属性是受保护的,不建议外界直接访问,那么如果想访问属性可以通过属性的getter(访问器)和setter(修改器)方法进行对应的操作。如果要做到这点,就可以考虑使用@property包装器来包装gettersetter方法,使得对属性的访问既安全又方便,代码如下所示。

class Person(object):
    
    def __init__(self, name, age):
        self._name = name
        self._age = age

    # 访问器 - getter方法
    @property
    def name(self):
        return self._name

    @property
    def age(self):
        return self._age

    # 修改器 - setter方法
    @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('王大锤', 12)
    person.play()
    person.age = 22
    person.play()
    # person.name = '白元芳' # AttributeError: can't set attribute

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('王大锤', 22)
    person.play()
    person._gender = '男'
    # AttributeError: 'Person' object has no attribute '_is_gay'
    # person._is_gay = True

静态方法和类方法

之前,我们在类中定义的方法都是对象方法,也就是说这些方法都是发送给对象的消息。实际上,我们写在类中的方法并不需要都是对象方法,例如我们定义一个“三角形”类,通过传入三条边长来构造三角形,并提供计算周长和面积的方法,但是传入的三条边长未必能构造出三角形对象,因此我们可以先写一个方法来验证三条边长是否可以构成三角形,这个方法很显然就不是对象方法,因为在调用这个方法时三角形对象尚未创建出来(因为都不知道三条边能不能构成三角形),所以这个方法是属于三角形类而并不属于三角形对象的。我们可以使用静态方法来解决这类问题,代码如下所示。

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()

和静态方法比较类似,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())
        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关系通常称之为依赖,比如司机有一个驾驶的行为(方法),其中(的参数)使用到了汽车,那么司机和汽车的关系就是依赖关系。
继承和多态

刚才我们提到了,可以在已有类的基础上创建新类,这其中的一种做法就是让一个类从另一个类那里将属性和方法直接继承下来,从而减少重复代码的编写。
提供继承信息的我们称之为父类,也叫超类或基类;得到继承信息的我们称之为子类,也叫派生类或衍生类。
子类除了继承父类提供的属性和方法,还可以定义自己特有的属性和方法,所以子类比父类拥有的更多的能力,在实际开发中,我们经常会用子类对象去替换掉一个父类对象,这是面向对象编程中一个常见的行为,对应的原则称之为里氏替换原则。下面我们先看一个继承的例子。

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_av(self):
        if self._age >= 18:
            print('%s正在观看爱情动作片.' % self._name)
        else:
            print('%s只能观看《熊出没》.' % self._name)


class Student(Person):
    """学生"""

    def __init__(self, name, age, grade):
        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):
        super().__init__(name, age)
        self._title = title

    @property
    def title(self):
        return self._title

    @title.setter
    def title(self, title):
        self._title = title

    def teach(self, course):
        print('%s%s正在讲%s.' % (self._name, self._title, course))


def main():
    stu = Student('王大锤', 15, '初三')
    stu.study('数学')
    stu.watch_av()
    t = Teacher('骆昊', 38, '砖家')
    t.teach('Python程序设计')
    t.watch_av()

if __name__ == '__main__':
    main()

子类在继承了父类的方法后,可以对父类已有的方法给出新的实现版本,这个动作称之为方法重写(override)。通过方法重写我们可以让父类的同一个行为在子类中拥有不同的实现版本,当我们调用这个经过子类重写的方法时,不同的子类对象会表现出不同的行为,这个就是多态(poly-morphism)。

from abc import ABCMeta, abstractmethod

class Pet(object, metaclass=ABCMeta):
    """宠物"""
    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函数中调用该方法时,这个方法就表现出了多态行为(同样的方法做了不同的事情)。

案例:工资结算系统。
"""
某公司有三种类型的员工 分别是部门经理、程序员和销售员
需要设计一个工资结算系统 根据提供的员工信息来计算月薪
部门经理的月薪是每月固定15000元
程序员的月薪按本月工作时间计算 每小时150元
销售员的月薪是1200元的底薪加上销售额5%的提成
"""
from abc import ABCMeta, abstractmethod

class Employee(object, metaclass=ABCMeta):
    """员工"""
    def __init__(self, name):
        """
        初始化方法
        :param 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, sales=0):
        super().__init__(name)
        self._sales = sales

    @property
    def sales(self):
        return self._sales

    @sales.setter
    def sales(self, sales):
        self._sales = sales if sales > 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()

你可能感兴趣的:(2023-03-28 | Python学习日记 (2))