Python学习笔记

Python学习笔记,第1张

目录

一.基础语法

1. 标识符

2. 关键字

3. 注释

4. 行与缩进

5. 多行语句

6. input 等待用户输入与print 输出

7. import 与 from...import

二.数据存储

1 变量

2 数据类型

三.数据处理

1. 算术运算

2. 赋值运算

3. 关系运算

4. 成员运算

5. 逻辑运算

四.流程控制语句

1、作用

2、分类

3、分支结构/条件语句

4、循环语句

五.异常处理

1 异常

2 异常处理

六.函数

1. 涵义

2. 分类

3. 基本使用

4. 参数

5. 匿名函数

七.装饰器

1. 涵义

2. 高阶函数

3. 函数作用域

4. 闭包函数

5. 函数装饰器

6. 类装饰器

7. 装饰器链

八.面向对象编程

1. 涵义

2. 基本概念

3. 类和实例

4. 三大特性

5. 类的特殊属性(魔术方法)

九.模块与包

1. 涵义

2. 导入

十.正则表达式

十一.常用模块与 *** 作

1. time

2. random

3. 读写 txt 文件

4. os

5.数据库 *** 作

6. 读写 excel 文件


Python是一种解释型、面向对象、动态数据类型的高级程序设计语言

一.基础语法 1. 标识符
  • 第一个字符必须是字母表中字母或下划线 _ 。


  • 标识符的其他的部分由字母、数字和下划线组成。


  • 标识符对大小写敏感。


2. 关键字

不能把关键字用作任何标识符名称。


Python 的标准库提供了一个 keyword 模块,可以输出当前版本的所有关键字:

>>> import keyword
>>> keyword.kwlist
['False', 'None', 'True', 'and', 'as', 'assert', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield']
3. 注释

单行注释以 # 开头,Pycharm中单行注释与取消单行注释的快捷键为:ctrl+/

多行注释可以用多个 # 号,或者 ''' 和 """

4. 行与缩进

python最具特色的就是使用缩进来表示代码块,不需要使用大括号 {} 。


缩进的空格数是可变的,但是同一个代码块的语句必须包含相同的缩进空格数。


5. 多行语句

Python 通常是一行写完一条语句,但如果语句很长,我们可以使用反斜杠 \ 来实现多行语句,例如:

total = item_one + \
        item_two + \
        item_three

在 [], {}, 或 () 中的多行语句,不需要使用反斜杠 \ ,例如:

total = ['item_one', 'item_two', 'item_three',
        'item_four', 'item_five']
6. input 等待用户输入与print 输出
age = input('请输入年龄:')
print age

执行上面的代码,程序就会等待用户输入,无论从键盘输入何种数据,input一律存储为字符串。


字符串不能进行数据运算

用户完成输入后按下 enter 键时,程序将向下执行

print 默认输出是换行的,如果要实现不换行需要在变量末尾加上 end=" ":

x="a"
y="b"
# 换行输出
print( x )
print( y )
print('---------')
# 不换行输出
print( x, end=" " )
print( y, end=" " )

以上实例执行结果为:

a
b
---------
a b
7. import 与 from...import

python 用 import 或者 from...import 来导入相应的模块。


将整个模块(somemodule)导入,格式为: import somemodule

从某个模块中导入某个函数,格式为: from somemodule import somefunction

从某个模块中导入多个函数,格式为: from somemodule import firstfunc, secondfunc, thirdfunc

将某个模块中的全部函数导入,格式为: from somemodule import *

二.数据存储 1 变量

在python中,变量和数据是分开存储的,数据保存在内存中的一个位置,变量中保存的是数据在内存中的地址。


变量中记录数据的地址,就叫做引用。


使用id()函数可以查看变量中保存数据所在内存的地址。


变量是没有类型的,仅仅是一个对象的引用(一个指针),可以是指向 List 类型对象,也可以是指向 String 类型对象。


引用:变量必须先定义才能引用

删除:可以使用del语句删除一些对象引用。


del var_a, var_b

2 数据类型

内置的 type() 函数可以用来查询变量所指的对象类型。


2.1 基本数据类型

涵义:只能存储一个数据,且数据只能整体使用,不能只使用其中一部分,比如a=123,,123是一个整体,不能拆出1,2,3来使用

包括:整数(int)、浮点型(float)、布尔型(bool)

整数:在Python 3里,只有一种整数类型 int,表示为长整型

浮点数:也称小数或实数,关键字为float

布尔型:表示真假或对错的类型,主要包括True、False,关键字为bool

2.2 序列数据类型

涵义:可以存储多个元素/数据,可以整体使用,也可以使用其中一部分,比如a=‘欧阳奋强’,可以整体使用a表示一个人的名字,也可以使用一部分比如欧阳,表示姓氏

包括:字符串(str)、列表(list)、元组(tuple)、字典(dic)、集合(set)

2.2.1 字符串

用于存储文字/文本(一般情况下不能进行数学运算)的类型,关键字为str

特点

  • 字符串是不可变的,不能修改某个字符的值,只能被重新赋值

    str = 'Hello'
    # 不允许修改某个字符的值,比如 str[0] = 'h'
    # 允许 str = 'hello'
  • 字符串中每个字符都有唯一编号(下标/序号),Python会给字符串中字符自动编号

  • 字符串中的每个字符成为元素或成员

表示方法

  • 单引号

  • 双引号

  • 三引号:允许一个字符串跨多行用于存储多行字符串,可以有回车换行,同时可以使用换行符\n、制表符\t以及其他特殊字符

  • 用input输入:a=input('请输入姓名')

  • 用str转型:a=str(123)

访问字符串中的值

访问子字符串,可以使用方括号 [ ] 通过索引获取字符串中字符,

字符串切片

字符串的截取的语法格式:变量[头下标:尾下标:步长],可截取字符串中的一部分,遵循左闭右开原则,表示截取从某头下标开始到某尾下标以前的字符,下标可以为空表示取到头或尾。


步长不指定时为1,当指定步长为正时,表示正序(从左往右截取),当指定步长为负时,表示逆序(从右往左截取)

例如:

str = abcdef
# str[:3] = abc   从头截取到索引为3以前的字符(截取前3位)
# str[3:] = def   从索引为3的字符截取到最后
# str[2:5] = cde
# str[1:5:2] = bd   以步长2(即间隔1个字符的距离)从索引为1的字符截取到索引为5以前的字符
# str[5:1:-2] = fd   以步长2(即间隔1个字符的距离)从索引为5的字符从右往左截取到索引为1以前的字符
# str[::-1] = fedcba

转义字符

需要在字符串中使用特殊字符时,python 用反斜杠 \ 来转义字符

  • 续行符:\(在行尾时)

    print("line1 \
    ... line2 \
    ... line3")

    输出结果为:

    line1 ... line2 ... line3
  • 反斜杠符号:\

    print("\")
    # 输出结果为
    \
  • 单引号:\'

  • 双引号:\"

  • 换行:\n

  • 横向制表符:\t

  • 纵向制表符:\v

  • 回车:\r,将 \r 后面的内容移到字符串开头,并逐一替换开头部分的字符,直至将 \r 后面的内容完全替换完成

    print('google runoob taobao\r123456')
    # 输出结果为
    123456 runoob taobao

字符串运算符

下表实例变量 a 值为字符串 "Hello",b 变量值为 "Python":

*** 作符描述实例
+字符串拼接a + b 输出结果: HelloPython
*重复输出字符串a*2 输出结果:HelloHello
[]通过索引获取字符串中字符a[1] 输出结果 e
[ : ]截取字符串中的一部分,遵循左闭右开原则,str[0:2] 是不包含第 3 个字符的。


a[1:4] 输出结果 ell
in成员运算符 - 如果字符串中包含给定的字符返回 True'H' in a 输出结果 True
not in成员运算符 - 如果字符串中不包含给定的字符返回 True'M' not in a 输出结果 True
r/R原始字符串 - 原始字符串:所有的字符串都是直接按照字面的意思来使用,没有转义特殊或不能打印的字符。


原始字符串除在字符串的第一个引号前加上字母 r(可以大小写)以外,与普通字符串有着几乎完全相同的语法。


print( r'\n' ) print( R'\n' ) 输出结果为\n
%格式化字符串请看下一节内容。


字符串格式化

  • 使用格式化 *** 作符 %

    基本的用法是通过 %将一个值插入到一个有字符串格式符的字符串中。


    缺点:当字符串更长,待插入变量更多,则使用%来格式化字符串的可读性将急剧下降且容易出错,该方法将会逐渐被淘汰。


    字符串格式符:

    符 号描述
    %s格式化字符串
    %d格式化整数
    %f格式化浮点数字,可指定小数点后的精度

    格式符辅助指令:

    符号功能
    *定义宽度或者小数点精度
    m.nm 是显示的最小总宽度,n 是小数点后的位数(如果可用的话)
    name = 'james'
    age = 28
    stu_info = '我叫 %s , 今年 %d 岁' % (name , age)
    print(stu_info)

  • 使用字符串内建函数 format()

    字符串中待替换的域使用{ }表示

    位置设定:

    # 不设置位置参数,按默认顺序
    name = 'james'
    age = 18
    str = 'this is {}, his age is {}'.format(name, age)
    print(str)
    
    # 设置位置参数指定格式化位置
    name = 'james'
    age = 18
    str = 'this is {1}, his age is {0}'.format(age, name)
    print(str)
    
    # 设置关键字指定格式化位置
    name = 'james'
    age = 18
    str = 'this is {n}, his age is {a}'.format(n=name, a=age)
    print(str)
    

    参数传递:

    # 元组传参
    stu1 = ('james', 18)
    stu2 = ('jack', 20)
    str1 = 'this is {}, his age is {}'.format(*stu1) # 不指定位置
    str2 = 'this is {0}, his age is {1}'.format(*stu2) # 指定位置
    print(str)
    
    # 字典传参
    dict = {'name':'james', 'age':18}
    str1 = 'this is {name}, his age is {age}'.format(**dict)
    str2 = 'this is {stu[name]}, his age is {stu[age]}'.format(stu=dict)
    print(str1)
    print(str2)
    
    # 列表传参
    list = ['james', 18]
    str1 = 'this is {0[0]}, his age is {0[1]}'.format(list)
    str2 = 'this is {stu[0]}, his age is {stu[1]}'.format(stu=list)
    print(str1)
    print(str2)
    
    # 对象传参
    class AssignValue(object):
        def __init__(self, value):
            self.value = value
    my_value = AssignValue(6)
    print('value 为: {0.value}'.format(my_value)) # {}中的0.代表引用第1个对象

    格式限定符:

    format通过丰富的的“格式限定符”(语法是 {}中带:号)对需要格式的内容完成更加详细的制定。


    # 填充与对齐  :号后面带填充的字符,只能是一个字符,不指定的话默认是用空格填充,且填充常跟对齐一起使用,^、<、>分别是居中、左对齐、右对齐,后面带宽度。


    N = 99 print('{:>8}'.format(N)) print('{:->8}'.format(N)) print('{:-<8}'.format(N)) print('{:-^8}'.format(N)) # 输出结果为 99 ------99 99------ ---99--- # 精度 :号后面可设置精度(以.开始加上精度),然后用f结束,若不设置,默认为精度为6,自动四舍五入,可带符号+显示数字正负标志。


    N = 99.1234567 NN = -99.1234567 print('{:f}'.format(N)) print('{:.2f}'.format(N)) print('{:+.2f}'.format(N)) print('{:+.2f}'.format(NN)) # 输出结果为 99.123457 99.12 +99.12 -99.12 # 进制 # b、d、o、x分别是二进制、十进制、八进制、十六进制 print '{:b}'.format(11) print '{:d}'.format(11) print '{:o}'.format(11) print '{:x}'.format(11) print '{:#x}'.format(11) print '{:#X}'.format(11)

    数字格式输出描述
    3.1415926{:.2f}3.14保留小数点后两位
    3.1415926{:+.2f}+3.14带符号保留小数点后两位
    -1{:+.2f}-1.00带符号保留小数点后两位
    2.71828{:.0f}3不带小数
    5{:0>2d}05数字补零 (填充左边, 宽度为2)
    5{:x<4d}5xxx数字补x (填充右边, 宽度为4)
    10{:x<4d}10xx数字补x (填充右边, 宽度为4)
    1000000{:,}1,000,000以逗号分隔的数字格式
    0.25{:.2%}25.00%百分比格式
    1000000000{:.2e}1.00e+09指数记法
    13{:>10d}13右对齐 (默认, 宽度为10)
    13{:<10d}13左对齐 (宽度为10)
    13{:^10d}13中间对齐 (宽度为10)

    其它用法:

    # 可以用{}对{}进行转义
    print("{{hello}} {{{0}}}".format("world"))
    # 输出结果为
    {hello} {world}
    
    # 格式化 datetime
    from datetime import datetime
    now = datetime.now()
    print("{:%Y-%m-%d %X}".format(now))

  • 使用字符串字面量f-string

    f-string 格式化字符串以 f 开头,后面跟着字符串,字符串中的变量或表达式用大括号 { } 包起来,它会将变量或表达式计算后的值替换进去。


    { }中还可以调用函数。


    # 替换变量
    name = 'james'
    age = 18
    print(f'this is {name}, his age is {age}')
    
    # 使用表达式
    x = 1
    print(f'{x+1}') # 输出结果为 2
    print(f'{x+1=}') # 使用 = 拼接表达式与结果,输出结果为 x+1=2
    
    # { }中调用函数
    def to_lowercase(input):
        return input.lower()
    
    name = "JAMES"
    
    print(f'{to_lowercase(name)} is funny' ) # 输出结果为 james is funny

  • 使用Template类格式化字符串(模板字符串)

    Template是string模块中的一个类,它可以将字符串的格式以模板的形式固定下来,重复利用。


    调用Template的substitute或者safe_substitute方法,通过关键字或者字典向模板对应位置传递参数,并且可通过继承string.Template,覆盖变量delimiter(变量标识符,默认使用 $)和idpattern(替换规则),自定义模板类来定制不同形式的模板。


    # 从string模块引用Template类
    from string import Template 
    # 用字典存放学生信息,字典中的key值必须与模板中的变量名保持一致
    stuInfo1 = {'ID': '000001', 'name': 'Lucy', 'age': 18, 'home_addr': 'BeiJing', 'final_score': 98}
    stuInfo2 = {'ID': '000002', 'name': 'Jack', 'age': 20,  'final_score': 94}
    # 创建字符串模板
    template_str = '【学号】: $ID\n【姓名】: $name\n【年龄】:$age\n【地址】:$home_addr\n【成绩】:$final_score'
    t = Template(template_str)
    # 调用substitute方法,替换字符串模板中对应位置的值
    r1 = t.substitute(stuInfo1)
    r2 = t.substitute(stuInfo2) # stuInfo2 字典信息中缺少模板中需要的 home_addr,因此会报错 KeyError
    # 调用safe_substitute方法,安全替换则不会报错,而是会保留模板中未被成功替换的原值
    r3 = t.safe_substitute(stuInfo2)
    print(r1)
    print(r2)
    print(r3)
    # 可通过继承string.Template,自定义模板类
    import string
    
    class MyTemplate(string.Template):  # 继承模板类
        delimiter = '%'  # 自定义模板中的变量标识符为 %
        idpattern = '[a-z]+_[a-z]+'  # 正则表达式,意味着只能匹配到模板中带有下划线的 home_addr 与 final_score,+ 号代表前面的字符必须至少出现一次
    
    # 用字典存放学生信息,字典中的key值必须与模板中的变量名保持一致
    stuInfo = {'ID': '000001', 'name': 'Lucy', 'age': 18, 'home_addr': 'BeiJing', 'final_score': 98}
    # 创建字符串模板
    t = MyTemplate('【学号】: %ID\n【姓名】: %name\n【年龄】:%age\n【地址】:%home_addr\n【成绩】:%final_score')
    # 调用safe_substitute方法,替换字符串模板中对应位置的值
    r = t.safe_substitute(stuInfo)
    print(r)
    # 输出结果为
    【学号】: %ID
    【姓名】: %name
    【年龄】:%age
    【地址】:BeiJing
    【成绩】:98

字符串常用内建函数

a、字符串长度

  • len(string) 计算字符串长度

s = 'hello'
n = len(s) # n存的值为5
print(n)

b、查找字符

  • count(sub, start= 0,end=len(string)) 统计字符串里某个字符出现的次数。


    可选参数为在字符串搜索的开始与结束位置。


    sub -- 搜索的子字符串 start -- 字符串开始搜索的位置。


    可省略,默认为第一个字符 end -- 字符串中结束搜索的位置。


    可省略,默认为字符串的最后一个位置。


    s = 'hello'
    n = s.count('l',2,4) # 统计在hello从索引2到索引4的范围中l出现的次数
    print(n) # 输出结果为 2
  • find(sub, start= 0,end=len(string)) 检测字符串里某个字符是否在某个位置范围出现,如果出现则返回首次出现时的索引号,否则返回-1,start与end可省略

    s = 'hello'
    n = s.find('l') 
    print(n)
    # 关联
    # 从字符串中取出变动位置的字符
    str = '亲爱的铂金会员张三,欢迎使用我们的特权服务' # 登录信息中会员名字会变化,张

    三、李四...,其余字符不变 left = str.find('会员') + len('会员') # 确定左边界索引 right = str.find(',欢迎使用') # 确定右边界索引 name = str[left:right] # 使用字符串切片,将登录信息中的会员名字取出来 print(name)

  • index(sub, start= 0,end=len(string)) 检测字符串里某个字符是否在某个位置范围出现,如果出现则返回首次出现时的索引号,否则报错 substring not found

    s = 'hello'
    n = s.index('p')
    print(n)

c、转换大小写

  • lower() 转换字符串中所有大写字符为小写.

  • upper() 转换字符串中所有小写字符为大写.

    str = 'Hello,Python'
    str1 = str.lower()
    str2 = str.upper()
    print(str1)
    print(str2)

d、去掉首尾空白符

  • lstrip() 截掉字符串左边的空白符(不指定的话则默认是空白符)或指定字符序列,空白符:空格、Tab制表符、换行

  • rstrip() 截掉字符串右边的空白符或指定字符序列

  • strip() 截掉字符串左右两边的空白符或指定字符序列,不能删除中间部分的字符。


    str1 = '123abcrunoob321'
    str2 = ' hello\tpython   '
    print (str1.strip( '12' ))  # 字符串首尾要移除字符序列 12
    print (str2.strip())
    # 输出结果为
    3abcrunoob3
    hello	python

f、字符串开头和结尾

  • startswith(sub, beg=0,end=len(string)) 检查字符串是否是以指定子字符串开头,如果是则返回 True,否则返回 False。


    如果参数 beg 和 end 指定值,则在指定范围内检查。


  • endswith(sub, beg=0,end=len(string)) 检查字符串是否是以指定子字符串结尾,如果是则返回 True,否则返回 False。


    如果参数 beg 和 end 指定值,则在指定范围内检查。


    str1 = '''
    -- 删除数据
    delete from user where username = 'zhanshan'
    '''
    str1 = str1.strip()
    str2 = 'd:\a\b\c.txt' # 包含路径的文件名 
    print(str1.startswith('--')) # True
    print(str2.endswith('.txt')) # True

g、字符串替换

  • replace(old, new, max) 把字符串中的 old(旧字符串) 替换成 new(新字符串),如果指定第三个参数max,则替换不超过 max 次,不指定则不限次数。


    相当于把原字符串复制一份新的,在新字符串上替换,而不是修改原字符串

    str = 'hello,python'
    new1 = str.replace('h','H')
    new2 = str.replace('h','') # 把h换成空字符,相当于删除h
    print(new1) # Hello,pytHon
    print(new2) # ello,pyton

h、分割字符串

  • split(str, num) 通过指定分隔符对字符串进行切片,如果第二个参数 num 有指定值(默认为-1即分隔所有),则分割为 num+1 个子字符串,返回分割后的字符串列表。


    str -- 分隔符,默认为所有的空白符,包括空格、换行(\n)、制表符(\t)等。


    num -- 分割次数。


    默认为 -1, 即分隔所有。


    url = "http://www.baidu.com/python/image/123456.jpg"
    # 以 “/” 对url进行分隔
    s1 = url.split('/')  # 输出结果为 ['http:', '', 'www.baidu.com', 'python', 'image', '123456.jpg']
    s2 = url.split('/',2)  # 输出结果为 ['http:', '', 'www.baidu.com/python/image/123456.jpg']
    s3 = url.split('/')[-1]  # 输出结果为 123456.jpg
    print(s1)
    print(s2)
    print(s3)
    
    s = '''
    insert into user values...
    delete from user where...
    update user set username where...
    '''
    # 去掉首尾空白符后,以回车\n对s进行分割
    sqls1 = s.strip().split('\n')  # 输出结果为 ['insert into user values...', 'delete from user where...', 'update user set username where...']
    sqls2 = s.strip().split('\n')[0]  # 输出结果为 insert into user values...

2.2.2 列表

特点

可用于存储多个数据,每个元素可以是同类型的数据,也可以是不同类型。


列表中存储的多个数据之间一般都有关联,可以是同类事物,也可以是同类事物的不同属性。


可以整体使用,也可以单独使用其中某一个或多个数据(常用)。


表示方法

以[ ]定界,多个数据/元素/成员之间用英文逗号间隔

s = 'hello1234' #1、2、3、4,形式上像数,但在字符串中是字符
s = [23, 24, 25, 21, 22, 23] #元素同类型,[]和逗号不存
s = ['张三', '男', 23, 1.78, '北京朝阳'] #元素不同类型
s = [name, gender, age] #name、gender、age必须是已定义的变量(变量内部已存入数据),意思是把变量中的数据存入列表中
names=s.split(',') #以逗号为分割点,将字符串s拆成多个数据,存入列表names中
names=s.split('\n') #以回车为分割点,将字符串s拆成多个数据,存入列表names中
names=s.split() #以空白符(空格、Tab、回车)为分割点,将字符串s拆成多个数据,存入列表names中
s=list('hello') #将字符串打散,每个字符依次存入列表s中,结果为 ['h','e','l','l','o'],不如split用的多

引用

整体引用:

s=[2,3,4,5,6]
print(s)

切片:与字符串切片类似

s=[22, 23, 21, 22, 23]
#  0   1   2   3   4
s[0]
s[-1]
s[2:4]

列表运算

[1,2,3]+[4,5] #合并/连接列表,[1,2,3,4,5]

[2,3]*3 #复制列表, [2,3, 2,3, 2,3]

*[2,3,4] #拆解列表,比复制使用更多, 2 3 4或2,3,4,常用于函数参数

a,b,c=[2,3,4] #拆解列表,2存入a,3存入b,4存入c
#存储3条测试用例,用例列:账号、密码、预期
cases=[
    ['admin', '123456', '登录成功'],
    ['', '123456', '用户名不能为空'],
    ['admin', '', '密码不能为空']
]
print(*cases) # 输出结果为 ['admin', '123456', '登录成功'] ['', '123456', '用户名不能为空'] ['admin', '', '密码不能为空']
case = ['登录_01','测试登录成功','admin','123456','登录成功']
case_id, case_name, uname, upass, expect = case
#二维列表
s=[
    [1,2,3],
    [4,5,6],
    [7,8,9]
]
print(s) # [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
print(s[1]) # [4,5,6]
print(s[2][1]) # 8

列表数据处理

a、增

对比字符串:字符串只能查

s.append(数据、列表或字典) #将数据、列表或字典追加到列表s末尾,无返回值,只能加一个成员。


最常用 s.insert(下标, 数据或列表) #将数据或列表插入到列表s的指定位置处 s.extend(列表) #将列表打散,追加到列表s的末尾

s1 = [1,2,3]
s2 = 4
s3 = [4,5,6]

s1 = s1.append(s2) # 结果为 [1,2,3,4]
s1 = s1.append(s3) # 结果为 [1,2,3,[4,5,6]]
s1 = s1.append(4,5,6) # 错,只能加一个数据
s1 = s1.insert(1,s2) # 结果为 [1,4,2,3],影响性能
s1 = s1.insert(1,s3) # 结果为 [1,[4,5,6],2,3,4]
s1 = s1.extend(s3) # 结果为 [1,2,3,4,5,6]
#从键盘输入3人的信息,存入列表
s=[] #空列表
name=input('姓名:')
gender=input('性别:')
age=int(input('年龄:'))
s.append([name, gender, age])
#---------------------------
name=input('姓名:')
gender=input('性别:')
age=int(input('年龄:'))
s.append([name, gender, age])
#----------------------------
name=input('姓名:')
gender=input('性别:')
age=int(input('年龄:'))
s.append([name, gender, age])
print(s)

b、删

e=s.pop() # 删除最后一个元素/成员/数据,有返回值,删除的数据存入e中
e=s.pop(i) # 删除第i号元素,有返回值,删除的数据存入e中
del s[i] # 删除索引为i号的元素,无返回值
s.remove(元素) # 删除指定的元素/数据, 如果数据不存在,会抛异常
s.clear() # 清除列表,删除列表中的所有数据,但申请的内存s还在
del s # 删除数据以及整个列表,内存收回

c、改

s[i]=新数据

d、查

n=len(s) # 求列表s长度(即列表中数据的个数),字符串也有len
n=s.count(数据) # 统计列表s中指定数据的个数,字符串也有count
n=s.index(数据) # 查询列表s中指定数据首次出现的位置/编号/下标,如果数据不存在,则抛异常,字符串也有index
m=max(s) # 求列表s中的最大值
m=min(s) # 求列表s中的最小值
m=sum(s) # 求列表s中所有数的和(元素必须都是数)
# 'hello'是字符串,是数据
# 35是数,也是数据

2.2.3 元组

特点

元组是不可变的,不能进行增删改 *** 作(对比:列表是可变的,字符串是不可变的)

元组可以理解为常量列表(不可增删改的一种特殊的列表)

表示方法

s = (1, 2, 3, 4) # 同类型的元素
s = ('张三', '男', 23, 1.78) # 不同类型的元素
s = tuple(字符串/列表) # 把字符串或列表转型为元组
s = 1, 2, 3, 4 # 省略定界符
s = '张三', '男', 23, 1.78

引用

print(s) #整体引用
s[i] #切片
s[i:j] #切片,取元组中第i号到第j-1号之间的所有元素
s[-2] #负下标

元组运算

(1,2,3)+(4,5) #合并/连接元组
(1,2,3)*3 #复制元组
*(1,2,3) #拆解元组,相当于*和()抵消

元组数据处理

元组只支持查询,不支持增、删、改、清空元组

len()、count()、index()、max()、min()、sum()

2.2.4 字典

特点

变量:只能存储少量数据(比如10个人的年龄,不适合变量存储)

列表:能存大量数据,缺点是数据含义不清

a=['张三', '男', 23, 178, 168] # 增加编程人员的负担和交流成本

字典:能存大量数据,优点是数据含义清晰,大量用于接口中(接口中叫 json 字符串)

字典是无序的(字符串/列表/元组:都是有序的,有序:有序号/编号/下标,从0开始的编号)

表示方法

a = {'name':'张三', 'gender':'男', 'age':23, 'height':178, 'weight':168}
a = dict(name='张三', gender='男', age=23, height=178, weight=168) # 不常用
a = dict.fromkeys(字符串/列表/元组, 默认值) # 类型转换,fromkeys中默认值可以省略,省略时,python会用None(空值)代替默认值

'name':'张三' 叫做键值对,称为字典的元素/成员

冒号左边的叫键,是对数据的解释,是数据的含义,键key一般使用字符串(可以用整数、浮点数,但极少使用),冒号右边的叫值,是具体的数据,值的类型任意 。


字典中的键是不能重复的,否则python只保留最后一个

引用

print(a) # 整体使用
a[key] # 获得某个键的值

字典运算

不支持+连接/合并、*复制

*dict:拆解出字典中的键

**dict:将字典转为 “键=值” 的形式,常用于函数形参中

字典数据处理

字典是可变的,支持增删改(对比:字符串、元组不允许,列表允许)。


只能增加键值对,只能删除键值对,只能修改键的值

a[不存在的键]=值 # 增加元素
a[已存在的键]=值 # 修改元素
a.update(字典b) # 将字典b中的键值对增加到字典a中
e=a.pop(键) # 删除元素,元素存在时,删除的key的值存入e中;元素不存在时,抛异常
e=a.pop(键, 键不存在时的返回值) # 删除元素,元素存在时,删除的key的值存入e中;元素不存在时,返回值存入e中
del a[键] # 删除元素
a.clear() # 清空字典,申请的内存a还在
del a # 删除字典,收回内存(所有类型都支持)
a.items() # 获得所有的键值对,返回可遍历的(键, 值) 元组数组。


把字典中每对 key 和 value 组成一个元组,并把这些元组放在列表中返回。


a.keys() # 获得所有的键,返回可遍历的键元组数组。


a.values() # 获得所有的值,返回可遍历的值元组数组。


len(a) # 获得字典中键值对的个数

dict = {'Google': 'www.google.com', 'taobao': 'www.taobao.com'}
print(dict.items())
# 遍历字典列表
for key,values in dict.items():
    print(key,values)
from selenium import webdriver
browser=webdriver.Firefox() #打开火狐浏览器
browser.get('file://C:/Users/tedu/Desktop/才高/Day05/index.html') #打开网页
elem={'uname':'zhsan', 'upass':'123456'}
for k,v in elem.items():
    browser.find_element_by_name(k).send_keys(v)

2.2.5 集合

特点

集合也用于存储大量数据,特点是数据含义不清,要求数据不能重复 (自动去重)

集合是无序的

集合支持增、删、查元素,不支持修改元素

表示方法

a = {23, 34, 24, 25, 46}
a = set(字符串/列表/元组/字典) # 数据转型

引用

无法单独引用某个元素,只能循环遍历每一个元素

集合运算

不支持+合并、*复制

合并:|

交集:&

差集:-

集合数据处理

集合处理函数(增、删、查):

s.add(e) # 向集合s中增加元素e(只能添加1个元素),若元素e存在,则自动忽略
s.update(s1) # 将集合s1中所有元素增加到集合s中,自动去重
s.remove(e) # 删除元素e,如果元素不存在,抛异常
s.discard(e) # 删除元素e,如果元素不存在,忽略
s.clear() # 清空集合
len(s) # 查询集合中的元素个数
# 不支持index、find、count...

2.3 数据类型转换

三.数据处理 1. 算术运算

算术运算符只能用于整数或浮点数。


以下假设变量 a=10,变量 b=21:

运算符描述实例
+加 - 两个对象相加a + b 输出结果 31
-减 - 得到负数或是一个数减去另一个数a - b 输出结果 -11
*乘 - 两个数相乘或是返回一个被重复若干次的字符串a * b 输出结果 210
/除 - x 除以 yb / a 输出结果 2.1
%取模 - 返回除法的余数b % a 输出结果 1
**幂 - 返回x的y次幂a**b 为10的21次方
//取整除 - 向下取接近商的整数,丢弃小数部分9//2=4;-9//2=-5
2. 赋值运算

复合赋值运算,以下假设变量a为10,变量b为20:

运算符描述实例
=简单的赋值运算符c = a + b 将 a + b 的运算结果赋值为 c
+=加法赋值运算符c += a 等效于 c = c + a
-=减法赋值运算符c -= a 等效于 c = c - a
*=乘法赋值运算符c *= a 等效于 c = c * a
/=除法赋值运算符c /= a 等效于 c = c / a
%=取模赋值运算符c %= a 等效于 c = c % a
**=幂赋值运算符c **= a 等效于 c = c ** a
//=取整除赋值运算符c //= a 等效于 c = c // a

链式赋值:a = b =c = 10 从右往左,相当于 c=10,b=c,a=b

同时赋值:a, b, c = 2, 3, 4 不是按从右到左的顺序,而是同时

3. 关系运算

以下假设变量a为10,变量b为20:

运算符描述实例
==等于 - 比较对象是否相等(a == b) 返回 False。


!=不等于 - 比较两个对象是否不相等(a != b) 返回 True。


>大于 - 返回x是否大于y(a > b) 返回 False。


<小于 - 返回x是否小于y。


所有比较运算符返回1表示真,返回0表示假。


这分别与特殊的变量True和False等价。


注意,这些变量名的大写。


(a < b) 返回 True。


>=大于等于 - 返回x是否大于等于y。


(a >= b) 返回 False。


<=小于等于 - 返回x是否小于等于y。


(a <= b) 返回 True。


支持特殊用法:0 4. 成员运算

运算符描述实例
in如果在指定的序列中找到值返回 True,否则返回 False。


x 在 y 序列中 , 如果 x 在 y 序列中返回 True。


not in如果在指定的序列中没有找到值返回 True,否则返回 False。


x 不在 y 序列中 , 如果 x 不在 y 序列中返回 True。


a = 10
b = 2
list = [1, 2, 3, 4, 5 ]
 
if ( a in list ):
   print ("变量 a 在给定的列表中 list 中")
else:
   print ("变量 a 不在给定的列表中 list 中")
 
if ( b not in list ):
   print ("变量 b 不在给定的列表中 list 中")
else:
   print ("变量 b 在给定的列表中 list 中")
5. 逻辑运算

可用于连接一个或者两个关系运算,运算结果为True或者False。


运算符逻辑表达式描述实例
andx and y布尔"与" - 如果 x 为 False,x and y 返回 x 的值,否则返回 y 的计算值。


orx or y布尔"或" - 如果 x 是 True,它返回 x 的值,否则它返回 y 的计算值。


notnot x布尔"非" - 如果 x 为 True,返回 False 。


如果 x 为 False,它返回 True。


四.流程控制语句 1、作用

流程控制语句处理多行程序代码。


流程控制语句可以解决:

  • 实现不同业务

  • 过滤错误数据

  • 减少重复代码

2、分类
  • 顺序结构:按照顺序一行一行的写代码,程序执行时按照书写代码的顺序执行

  • 分支/选择结构:编程者会写几份不同的程序,每次执行程序时,只执行其中的一份程序

  • 循环结构:编程者写一份程序,会反复执行多次

3、分支结构/条件语句

Python条件语句是通过一条或多条语句的执行结果(True或者False)来决定执行的代码块。


Python程序语言指定任何非0和非空(null)值为true,0 或者 null为false。


3.1 单分支结构

基本形式

if 判断条件:
    执行语句……
else:
    执行语句……

# if 语句的判断条件可以用>(大于)、<(小于)、==(等于)、>=(大于等于)、<=(小于等于)来表示其关系。


# 当if有多个条件时可使用括号来区分判断的先后顺序,括号中的判断优先执行,此外 and 和 or 的优先级低于>(大于)、<(小于)等判断符号 # "判断条件"成立时(非零),则执行后面的语句,而执行内容可以多行,以缩进来区分表示同一范围。


# else 为可选语句,当需要在条件不成立时执行内容则可以执行相关语句。


# 有if,可以没有else;有else时,必须有if。


#从键盘输入性别,当输入正确时,输出“你的性别是...”,输入错误时,什么也不做
'''
思路/步骤:
1、申请内存,提示输入性别,从键盘输入数据,存入变量中
2、如果对了,输出你的性别是...
'''
gender=input('性别:')
if gender=='男' or gender=='女':
    print('你的性别是'+gender)
'''
白盒测试方法:
  语句覆盖:要求程序中的所有代码行必须至少执行一次,用例:男
  条件覆盖:要求每个小条件至少要真一次,假一次,用例:男、女
  逻辑覆盖:大条件/条件整体至少真一次,假一次,用例:男、人
  多条件覆盖/条件组合覆盖:对对、对错、错对、错错,用例:男、女、人
'''
#单分支:当两个整数都是正数时,计算和,否则什么也不做
'''
步骤:
1、定义两个变量,存两个数
2、如果两个数都是正的,则求和
'''
a, b=int(input('数1:')), int(input('数2:'))
if a>0 or b>0:
    print(a+b)
'''
单元测试(白盒测试方法):
  语句覆盖:3、4
  条件覆盖:a>0对错各一次,b>0对错各一次,用例:(3、4),(-3、-4)
  逻辑覆盖:整体对错各一次,用例:(3、4)、(-3、-4)
  条件组合覆盖:对对、对错、错对、错错,用例:(3、4)、(3、-4)、(-3、4)、(-3、-4)
'''
#使用单分支,求一个数的绝对值
'''
思路/步骤:
1、定义变量,存一个数
2、定义变量,存结果,一开始存这个数
3、如果这个数小于0,则修改结果为这个数的相反数
'''
x=float(input('数:'))
result=x
if x<0:
    result=-x
print(result)
'''
条件覆盖:x>0对错各一次,用例:3、-3,边界值0,数据类型
'''
#双分支:键盘输入一个数,计算绝对值
'''
需求:
    正数、0:绝对值是自己
    负数:绝对值是它的相反数
思路/步骤:
    1、定义变量
    2、如果变量中存储的数据是大于等于0的,则结果就是这个变量
       否则,结果是这个变量的相反数
    3、输出结果
'''
x=float(input('输入一个数:'))
#双分支
if x>=0:
    y=x
else:
    y=-x
print(y)
'''
测试:3、0、-3
'''
#单语句,双分支的简单写法,结果变量=结果表达式1 if 条件 else 结果表达式2
y=x if x>=0 else -x
print(y)

3.2 多分支结构

基本形式

if 判断条件1:
    执行语句1……
elif 判断条件2:
    执行语句2……
elif 判断条件3:
    执行语句3……
else:
    执行语句4……
    
# 每个条件必须都是互斥的(不能有交集)
#多分支:从键盘输入分数,判断分数所属的等级(优秀、良好、中等、及格、不及格)
'''
思路/步骤:
    1、定义变量(申请内存,存入数据)
    2、分析:结果有6种不同情况(互斥),考虑使用if elif else
      如果分数在90-100,结果是优秀
      否则如果分数在80-89,结果是良好
      否则如果分数在70-79,结果是中等
      ...60-69,及格
      ...0-59,不及格
      其他情况(else否则),一律报错
    3、输出结果
'''
score=int(input('分数:'))
if score>=90 and score<=100:
    grade='优秀'
elif 80<=score<90:
    grade='良好'
elif 70<=score<80:
    grade='中等'
elif 60<=score<70:
    grade='及格'
elif 0<=score<60:
    grade='不及格'
else:
    grade='输入错误,分数只能在0-100之间'
print(grade)

3.3 嵌套分支语句

if 条件1:
	if 条件11:
		语句段1
	else:
		语句段2
else:
	if 条件2:
		语句段3
	else:
		语句段4
# 分支嵌套:从键盘输入3个数,作为三角形的三条边长,判断三个数能否构成三角形,以及能够构成何种类型的三角形(等边、等腰、普通三角形、输入错误、不能构成三角形)
'''
思路/步骤(情景法)
    1、定义变量,存储数据
    2、需求:任意两边和大于第三边
      如果三个数都是正的
        如果任意两个数之和大于第三个
          如果三个都相等,则结果是等边三角形
          否则如果两个相等,则结果是等腰三角形
          否则(三个都不等):则结果是普通三角形(3、4、5)
        否则:提示不能构成三角形
      否则:报错
    3、输出结果
'''
a,b,c=int(input('第一边:')),int(input('第二边:')),int(input('第三边:'))
if a>0 and b>0 and c>0:
    if  a+b>c and a+c>b and b+c>a:
        if a==b==c:
            result='等边三角形'
        elif a==b or a==c or b==c:
            result='等腰三角形'
        else:
            result='普通三角形'
    else:
        result='不能构成三角形'
else:
    result='边长必须是正数'
print(result)

4、循环语句

循环语句允许我们执行一个语句或语句组多次。


当结果是有规律的重复时,考虑使用循环,流程图如下:

4.1 while循环

基本形式

while 判断条件(condition):
    执行语句(statements)……
    
# 执行语句可以是单个语句或语句块。


判断条件可以是任何表达式,任何非零、或非空(null)的值均为true。


# 当判断条件为 false 时,循环结束。


# 如果条件判断语句永远为 true,循环将会无限的执行下去出现死循环问题,可以使用 CTRL+C 来中断循环。


# while … else 在循环条件为 false 时执行 else 语句块: count = 0 while count < 5: print count, " is less than 5" count = count + 1 else: print count, " is not less than 5" # else 中的语句会在循环正常执行完(即 while 不是通过 break 跳出而中断的)的情况下执行 # 类似 if 语句的语法,如果 while 循环体中只有一条语句,你可以将该语句与while写在同一行中, 如下所示:待补充

# while循环:求1-9中输数的平方
i=1 # 一开始
while i<=9:
    print(i*i, end=' ') # end=' '数据之间用空格间隔,节约行空间(少占行)
    i+=1
# 输出列表中的元素
a=[23, 24, 21, 22, 23, 25, 22]
#  0   1   2   3   4   5   6

# i=0 # 从0号开始看数据
# while i<=6: # 6写死了,容易导致缺陷
#     print(a[i],end=' ')
#     i+=1 # 下一个数据

# 好的写法
i=0 # 从0号开始看数据
while i 
# 输出200以内的费波那西数列
# 0 1 1 2 3 5 8 ...
'''
举例子、找规律
a   b   a+b
0   1   1
1   1   2
1   2   3
2   3   5
3   5   8
'''
a,b = 0,1
while a <= 200:
    print(a, end=' ')
    a,b = b,a+b #先算b和a+b,结果同时存入a和b

4.2 for循环

for循环可以用来遍历任何序列的项目,如一个列表或者一个字符串。


流程图如下:

基本形式

for  in :
    
else:
     # else 中的语句会在循环正常执行完(即 for 不是通过 break 跳出而中断的)的情况下执行
for letter in 'Python':     # 第一个实例,字符串遍历
   print("当前字母: %s" % letter)
 
fruits = ['banana', 'apple',  'mango']
for fruit in fruits:        # 第二个实例,遍历列表元素
   print ('当前水果: %s'% fruit)

# 另外一种遍历列表元素的方式是通过索引
fruits = ['banana', 'apple',  'mango']
for index in range(len(fruits)):  # 使用了内置函数 len() 和 range(),函数 len() 返回列表的长度,即元素的个数,range返回一个序列的数。


print ('当前水果 : %s' % fruits[index]) # 枚举法遍历

for i in range(m,n,s): # 表示i从m开始,每次加s(步长),一直变化到n-1(左闭右开原则)
	循环体
# 说明:m可以省略,省略时,从0开始;s可以省略,省略时,每次加1

for k,v in 字典.items(): # k对应键,v对应值
	循环体
# 输出1-10中的奇数
for i in range(1,11,2): #i从1到10,每次加2
	print(i,end=' ')

# 计算10以内的数的平方
for i in range(11):
    print(i*i,end=' ')

# 对比while循环
i=0
while i<11:
    print(i*i,end=' ')
    i=i+1

# 遍历列表
a=[23, 34, 24, 22, 23, 35, 25, 24]
#  0   1   2   3   4   5   6   7
# for循环
for i in a: #i不表示第几号,而是表示某个数据
    print(i,end=' ')
# 对比while循环
# i=0
# while i 
# 输出水仙花数
'''
水仙花数:3位正整数
153= 1*1*1 + 5*5*5 + 3*3*3
     1       125     27
思路:举例子、找规律
    100:问100==1^3+0^3+0^3?,不等,不出来
    101:问101==1^3+0^3+1^3?,不等,不出来
    ...
    153:问153==1^3+5^3+3^3?,相等,出来
    ...
    999:问...
规律:
    左边数:100~999,反复做
        拆数为三个数(百位、十位、个位)
        如果数等于拆出的3个数的立方和,则print左边数
        否则,什么也不做
'''
# while循环
# left=100
# while left<1000: #left<=999
#     bai=left//100
#     shi=left//10%10
#     ge=left%10
#     if left==bai**3+shi**3+ge**3:
#         print(left,end=' ')
#     left+=1

# for循环
for left in range(100,1000):
    bai=left//100
    shi=left//10%10
    ge=left%10
    if left==bai**3+shi**3+ge**3:
        print(left,end=' ')
for num in range(10,20):  # 迭代 10 到 20 之间的数字
   for i in range(2,num): # 根据因子迭代
      if num%i == 0:      # 确定第一个因子
         j=num/i          # 计算第二个因子
         print ('%d 等于 %d * %d' % (num,i,j))
         break            # 跳出当前循环
   else:                  # 循环的 else 部分
      print ('%d 是一个质数' % num)
# 测试用例
cases=[
    ['login_01','测试登录成功','admin','123456','登录成功'],
    ['login_02','测试用户名错误','adm','123456','用户名错误'],
    ['login_03','测试密码错误','admin','123','密码错误']
]
# print(cases)
# print(*cases)
# for case in cases:
#     print(*case)
# for case in *cases: #错
for case in cases:
    case_id,case_name,uname,upass,expect=case
    print(case_id,case_name,uname,upass,expect)

4.3 循环控制

4.3.1 break

彻底退出循环,一般结合if使用

# 编写一个捐款程序,假设要捐款10000元
'''
情景:
    来一个人,请捐款
    将捐款存起来,计算总额
    问够了吗
    如果够了,则退出
    否则,接着捐
'''
all = 0
while True: # 人为设计的死循环
    juan=float(input('请捐款:'))
    all=all+juan
    if all>=10000:
        break
    # else:
        # continue
print(all)

4.3.2 continue

提前退出本轮循环,继续下一轮循环,一般结合if使用。


continue绝对多数情况下,都可以省略不写

# 查找列表中某个数据的下标
a=[2, 3, 4, 5, 3, 4, 6, 2, 3, 2, 7, 8]
#  0  1  2  3  4  5  6  7  8  9  0  1
for i in range(len(a)):
    zhao=3
    if a[i]==zhao:
        print(i,end=' ')
        continue # 找所有的,此处continue可省略
        # break # 只找第一个
        
# 输入一个数据作为值,输出字典中对应的值和键
a={'name':'张三','gender':'男','age':23, 'height':168, 'weight':168}
for k,v in a.items():
    if v==168:
        print(k,v)
        continue       

4.4 嵌套循环

for iterating_var in sequence:
	for iterating_var in sequence:
		statements(s)
	statements(s)
		
while expression:
	while expression:
    	statement(s)
	statement(s)
		
while expression:
    for iterating_var in sequence:
        statements(s)
    statements(s)

while expression:
	for iterating_var in sequence:
    	statement(s)
	statement(s)    
# 输出九九乘法表
'''
举例子
    1*1=1
    2*1=2 2*2=4
    3*1=3 3*2=6 3*3=9
    ...
    9*1=9 9*2=18 ... 9*9=81
找规律:
    *左边:1~9,变得慢(先写,外循环,外循环慢)
        *右边:1~左边(后写,内循环,内循环快)
            反复做:左*右=乘积 空格
        回车
'''
for left in range(1,10):
    for right in range(1,left+1):
        print('%s*%s=%s'%(left,right,left*right),end=' ')
    print()
# 输出100以内的素数
'''
素数/质数:一个大于1的正整数,如果只能被1和自己整除
8/2=4 整除(2能被8整除)
9/2=4.5 不能整除(2不能被9整除)
举例子:
   2:别的都除不开,出来
   3:3除以2,除不开,出来
   4:4除以2,除开,不出来,下一个数
   5:5除以2,除不开,5除以3,除不开,5除以4,除不开,出来
   7:7除以2,除不开,7除以3,除不开,7除以4,除不开,7除以5,除不开,7除以6,除不开,出来
   ...
   9:9除以2,除不开,9除以3,除开,不出来,下一个数
   ...
   100:100除以2,除开,下一个数
找规律:
    完全或部分重复或者有规律的变化
    有重复:用循环
    除开:余数等于0,除开,不等于0,除不开
    除以左边:有规律变化,3~100
    除以右边:有规律变化,2~左边-1
    反复做:
        判断除开除不开
        如果除开,下一个数(左边+1)
        如果除不开
           如果右边==左边-1
               出来
算法:
外循环:左边:3-100
    内循环:右边:2~左边-1
        如果左边%右边==0
            break
        否则
            如果右边==左边-1
                出来
'''
print(2,end=' ')
for left in range(3,101):
    for right in range(2,left-1+1):
        if left%right==0:
            break
        else:
            if right==left-1:
                print(left,end=' ')
                
# 或者:
print(2,end=' ')
for a in range(3, 101):
    for b in range(2, a):
        if a % b != 0:
            if b == a-1:
                print(a,end=' ')
        else:
            break

五.异常处理 1 异常

异常即是一个事件,该事件会在程序执行过程中发生,影响了程序的正常执行。


一 般使用 if 难以处理这种错误。


异常是Python对象,表示一个错误。


当Python脚本发生异常时我们需要捕获处理它,否则程序会终止执行。


python标准异常:

异常名称描述
BaseException所有异常的基类
SystemExit解释器请求退出
KeyboardInterrupt用户中断执行(通常是输入^C)
Exception常规错误的基类
StopIteration迭代器没有更多的值
GeneratorExit生成器(generator)发生异常来通知退出
StandardError所有的内建标准异常的基类
ArithmeticError所有数值计算错误的基类
FloatingPointError浮点计算错误
OverflowError数值运算超出最大限制
ZeroDivisionError除(或取模)零 (所有数据类型)
AssertionError断言语句失败
AttributeError对象没有这个属性
EOFError没有内建输入,到达EOF 标记
EnvironmentError *** 作系统错误的基类
IOError输入/输出 *** 作失败
OSError *** 作系统错误
WindowsError系统调用失败
ImportError导入模块/对象失败
LookupError无效数据查询的基类
IndexError序列中没有此索引(index)
KeyError映射中没有这个键
MemoryError内存溢出错误(对于Python 解释器不是致命的)
NameError未声明/初始化对象 (没有属性)
UnboundLocalError访问未初始化的本地变量
ReferenceError弱引用(Weak reference)试图访问已经垃圾回收了的对象
RuntimeError一般的运行时错误
NotImplementedError尚未实现的方法
SyntaxErrorPython 语法错误
IndentationError缩进错误
TabErrorTab 和空格混用
SystemError一般的解释器系统错误
TypeError对类型无效的 *** 作
ValueError传入无效的参数
UnicodeErrorUnicode 相关的错误
UnicodeDecodeErrorUnicode 解码时的错误
UnicodeEncodeErrorUnicode 编码时错误
UnicodeTranslateErrorUnicode 转换时错误
Warning警告的基类
DeprecationWarning关于被弃用的特征的警告
FutureWarning关于构造将来语义会有改变的警告
OverflowWarning旧的关于自动提升为长整型(long)的警告
PendingDeprecationWarning关于特性将会被废弃的警告
RuntimeWarning可疑的运行时行为(runtime behavior)的警告
SyntaxWarning可疑的语法的警告
UserWarning用户代码生成的警告
2 异常处理
# try....except...else...finally
try:
	可能出错的代码
except 异常类名1 as 别名: # 如果try子句没出错,则except不执行;一旦出错,那么try子句余下的部分将被忽略,如果异常的类型和except之后的异常名称相符,那么对应的 except 子句将被执行。


最多只有一个except分支会被执行。


如果一个异常没有与任何的 except 匹配,那么这个异常将会传递给上层的 try 中。


异常处理并不仅仅处理那些直接发生在 try 子句中的异常,而且还能处理子句中调用的函数(甚至间接调用的函数)里抛出的异常。


except可以不带任何异常类型,也可以带多个异常类型 异常处理代码段1 except 异常类名2: 异常处理代码段2 except 异常类名3: 异常处理代码段3 ... except BaseException: # BaseException是所有异常的统称 异常处理代码3 else: 如果没有异常发生则会执行的代码 finally: 无论是否发生异常都将执行最后的代码 # except可以不带任何异常类型,不能通过该程序识别出具体的异常信息。


因为它捕获所有的异常。


try: 可能出错的代码 except: 发生异常,执行这块代码 else: 如果没有异常执行这块代码 # except可以带多个异常类型。


try: 可能出错的代码 except(Exception1, Exception2, ..., ExceptionN): 发生以上多个异常中的一个,执行这块代码 else: 如果没有异常执行这块代码

try:
    a={'name':'张三','gender':'男','age':23}
    print(a['name'])
    print(a['height'])
except ValueError:
    print('数据类型错误')
except IndexError:
    print('下标越界')
except KeyError:
    print('字典键不存在')
except BaseException as e:
    print('其他错误'+str(e))

六.函数 1. 涵义

函数是组织好的,可重复使用的,用来实现单一,或相关联功能的代码段。


函数是对代码的封装。


函数能提高应用的模块性代码的重复利用率,实现模块化程序设计,减少程序的复杂度,使代码更容易修改和维护。


2. 分类
  • 内置函数:print、input

  • 标准库函数:先import,然后使用函数

  • 第三方函数库:先下载安装,然后import,然后使用函数

  • 自定义函数

3. 基本使用

3.1 定义函数

3.1.1 规则

  • 函数代码块以 def 关键词开头,后接函数名称和圆括号()。


    圆括号()不能省略。


    函数名是必须符合Python名称规范要求的标识符,需要避免使用Python的关键字。


    函数名也是申请的内存,不过存储的是代码,这段代码不会自动执行。


  • 任何传入参数和自变量必须放在圆括号中间,称为形参,是用来接收参数用的,在函数内部作为变量使用,函数的参数可以没有,也可以是多个甚至可变个数。


    调用函数时传递的真实数据称为实参,是用来把数据传递到函数内部用的。


  • 函数内容以冒号起始,并且缩进。


  • 函数的第一行语句可以选择性地使用文档字符串—用于存放函数说明,提高代码的可读性。


  • 如果需要返回值,可使用return [返回值] 结束函数,选择性地返回一个值给调用方。


    该返回值可以是任意类型。


    需要注意的是,return 语句在同一函数中可以出现多次,但只要有一个得到执行,就会直接结束函数的执行。


    如果函数中没有return语句或return语句不带任何返回值,那么该函数的返回值为 None。


    生成器函数使用yield返回值。


3.1.2 语法

# 基本形式
def 函数名([参数]):
	'函数文档字符串' 
	函数体

# 不带 return 语句
def 函数名(): # 无参函数
	函数体

def 函数名(形参1, 形参2, ...): # 有参函数
	函数体

# 带 return 语句
def 函数名(): # 无参函数
	函数体
	return 返回值

def 函数名(形参1, 形参2, ...): # 有参函数
	函数体
	return 返回值

3.1.3 实例

# 无参函数,不带 return 语句,函数返回值为 None
def add(): 
    a=float(input('第一个数:'))
    b=float(input('第二个数:'))
    c=a+b
    print(c)

# 有参函数,不带 return 语句,函数返回值为 None
def add(a, b):
    c = a + b
d = add(2,3)
print(d) # 输出结果为 None

# 有参函数,带 return 语句,但是return语句不带任何返回值,此时函数返回值还是为 None
def add(a, b):
    c = a + b
    return
d = add(2,3)
print(d) # 输出结果为 None

# 有参函数,带 return 语句,且return语句带返回值
def add(a, b):
    c = a + b
    return c
d = add(2,3) # 用变量保存函数的返回值
print(d) # 输出结果为 5
print(add(3,4)) # 将函数返回值作为某个函数的实际参数,输出结果为 7

3.2 调用函数

函数定义完成后,可以通过另一个函数调用执行,也可以直接从 Python 命令提示符执行。


函数名() # 括号不能省略
函数名(实参1, 实参2, ...) # 实参表
存返回值的变量 = 函数名() # 用变量保存函数返回值
存返回值的变量 = 函数名(实参1, 实参2, ...)
# 无参函数,无返回值    
def add(): 
    a=float(input('第一个数:'))
    b=float(input('第二个数:'))
    c=a+b
    print(c)
    
# add() # 调用函数
# add() # 第二次调用
for i in range(3): # 循环调用3次
    add()

    
# 有参函数,无返回值    
def add(a, b): # a、b是形参(形式参数),表示将来a和b中各存一个数
    c=a+b 
    # 形参以及函数内部定义的变量,都是局部变量,只在函数调用以及函数代码执行期间,才申请内存,函数执行完成后,内存会被收回,局部变量不复存在
    # 局部变量在函数内部使用,临时保存函数内部需要使用的数据
    # 不同的函数,可以定义相同的名字的局部变量,但是各用各的不会产生影响
    print(c)

# x=float(input('第一个数:'))
# y=float(input('第二个数:'))
# add(x, y) #调用函数add,x和y是实参(实际参数):具体数据或者有具体数据的变量/列表

# 允许全局变量与局部变量同名
a=float(input('第一个数:')) # 这里a,b是全局变量,全局变量是在函数外部定义的变量,(没有定义在某一个函数内),所有函数内部都可以使用这个变量
b=float(input('第二个数:'))
add(a, b) # 执行到此行时,程序会跳转执行函数add的函数体,a和b按照顺序传递给add的形参(a和b叫做位置参数)
print('over')


# 有参函数,有返回值
def add(a, b): # 这里的a、b、c是局部变量
    c=a+b
    return c

print(add(3, 4)) 
a=float(input('第一个数:')) # 下面的a、b、c是全局变量
b=float(input('第二个数:'))
c=add(a,b)
print(c)

4. 参数

4.1 必须参数

必须参数要求调用函数时实参必须以正确的顺序传入函数。


调用函数时传入的参数(实参)数量必须和声明函数时的参数(形参)数量一样,形参的顺序与实参的顺序逐一对应,这种参数的使用模式称为位置参数。


位置参数是最常用的一种参数使用形式。


def add(a, b): # 形参a,b是必须参数
    sum = a + b
    print(sum)
add(2, 3) # 实参2,3以位置参数的方式传入函数内部,实参与形参一一对应,2传给a,3传给b

4.2 默认值参数

定义函数时,可以设置参数的默认值。


调用函数时,默认值参数可以传值也可以不传值,如果没有传入默认值参数,则该参数被认为使用默认值。


默认值参数要放在必须参数后面。


def add(a, b, c=4): # 形参c是默认值参数,默认值为4
    sum = a+b+c
    print(sum)
add(2, 3) # 调用函数时未传入默认值参数,此时默认c传入的是默认值4
add(2, 3, 1) # 调用函数时传入默认值参数,此时c传入的是实参1
# 列表做参数默认值
def add(a=[]): # 形参是列表,默认值是空列表
    s = 0 # 存储原始和
    for i in a: # 求a中的每个数据的和
        s = s+i # s+=i
    print(s)
# add() # 无参,对
# add(1) # 错,类型错误,实参不是列表
# add([1]) # 对,[]不能省略
add([1,2,3]) # 对


# 元组做默认值
def add(a=()): # a是元组,默认空元组
    s = 0
    for i in a:
        s+=i
    print(s)
# add() # 对
# add(1) # 错
# add(2,3) # 错
add((2,3)) # 对

4.3 关键字参数

关键字参数与函数调用有关,在函数调用时,指定形参等于指定实参的参数使用模式称为关键字参数,调用函数时使用关键字参数来确定传入的参数值。


使用关键字参数允许函数调用时参数的顺序与声明时不一致,因为 Python 解释器能够用参数名匹配参数值。


位置参数与关键字参数混用时,位置参数在前,关键字参数在后。


def add(a, b, c=2): 
    sum = a + b + c
    print(sum)
add(a=10, b=20, c=30) # 对,形参名=值的写法,就是关键字参数
add(b=10, a=30, c=20) # 对,形参顺序可变
add(20, c=10, b=30) # 对,20是位置参数,给a
add(20, 10, c=30) # 对
add(10, c=20, a=30) # 错,python猜不出10给b,实际上10一定给a
add(b=10, 20, c=30) # 错,位置参数与关键字参数混用时,位置参数在前,关键字参数在后
# 字典做实参
def out(a, b):
    print(a, b)
# d={'x':'zhsan','y':'男'}
# out(d) #错,d给a,b缺失
# out(*d) #*d可以拆出字典的键
# out(**d) #**d的结果是x='zhsan',y='男'(关键字形式)
# out(x='zhsan',y='男') #错,关键字参数必须是a、b
d={'a':'zhsan','b':'男'}
out(**d) # **d表示a='zhsan',b='男'

4.4 变参

也称不定长参数,定义函数时,加了星号*的变量名会以元组的形式存放所有未命名的变量参数

def out(a, *b): 
    print(a)
    print(b)
out(2) # b = () b是空元组
# 输出结果为 
2
()

def out(a, *b): 
    print(a)
    print(b)
out(2, 3, 4) # b = (3, 4)
# 输出结果为 
2
(3, 4)

def out(a, *b):
    print(a)
    print(b)
out(2, [3, 4]) # b = ([3, 4],)
# 输出结果为 
2
([3, 4],)

def out(a, *b):
    print(a)
    print(b)
out(2, [3, 4], [5, 6]) # b = ([3, 4], [5, 6])
# 输出结果为 
2
([3, 4], [5, 6])
# 求任意多个数之和
def add(*a): # a是变参,是元组
    s=0
    for i in a:
        s=s+i
    print(s)
add() # 对
add(3) # 对,a=(3)
add(3,4) # 对,a=(3,4)
add(3,4,5,6,7,8,9) # 对,a=(3,4,5,6,7,8,9)
# 变参:也可以用**,此时变量名会以字典的形式存放所有未命名的变量参数
def out(**a): # a是变参,是字典
    for k,v in a.items():
        print(k,v)
# out() # 对
# out(2) # 错
# out(3,4) # 错
out(x=3, b=4) # 必须用关键字参数写法
5. 匿名函数

使用 lambda 来创建匿名函数。


所谓匿名,意即不再使用 def 语句这样标准的形式定义一个函数。


表达式只能有一个,而且不能加return。


适合编写很简单的函数,比较复杂的函数仍然建议使用def定义。


mul=lambda x,y,z: x*y*z #不能写return
print(mul(2,3,4))

七.装饰器 1. 涵义

装饰器本质上就是一个函数,它可以让原函数在不需要做任何代码变动以及不改变原函数调用方式的前提下增加额外的功能,装饰器的返回值也是一个函数对象(函数的指针)。


经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。


装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。


概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。


理解装饰器首先需要理解Python高阶函数(将函数名作为实参、将函数名作为返回值)以及闭包函数

2. 高阶函数

实参为函数的函数,或者返回值为函数的函数(若返回值为该函数本身,则为递归),满足其一则为高阶函数。


2.1 参数为函数

python 中一切皆对象,函数也不例外。


函数名其实就是指向函数的变量,比如求绝对值函数 abs,我们可以用一个变量 f 指向 abs 函数,那么当调用 f() 的时候可以得到和 abs() 一样的效果。


既然变量可以指向函数,而函数的参数可以接收变量。


也就是说一个函数可以接收另一个函数作为参数。


# 定义函数相当于申请内存,内存中存放函数体中的代码块,函数名实际上是函数的内存地址,函数名()才是调用函数执行对应内存地址中存储的代码
def test():
    print('函数名test作为实参传入')
def deco(func):
    print('执行函数deco')
    func()
deco(test) # 调用函数deco,函数名test作为实参传入deco函数

2.2 返回值为函数

def test():
    print('执行函数test')
def deco():
    print('执行函数deco,返回函数名')
    return test # 返回函数名test即test指向的内存地址
t=deco() # 调用函数deco,返回函数名test即函数test的内存地址,存入t,t=test
t() # test(),调用test函数
def test():
    print('执行函数test')
def deco(func):
    print('执行函数deco,返回函数名')
    return func # 返回变量func即func指向的内存地址
t=deco(test) # 调用函数deco,函数名test作为实参传入deco函数,返回函数名test即函数test的内存地址,存入t,t=test
t() # test(),调用test函数
3. 函数作用域

Python中的作用域可分为四类:

  • L:local,局部作用域,即函数中定义的变量;

  • E:enclosing,嵌套的父级函数的局部作用域,即包含此函数的上级函数的局部作用域,但不是全局的(闭包常见);

  • G:globa,全局变量,就是模块级别定义的变量;

  • B:built-in,系统固定模块里面的变量,比如int, bytearray等。


搜索变量的优先级顺序依次是:局部作用域>外层作用域>当前模块中的全局变量>python内置作用域,也就是LEGB

只有模块、类、及函数才能引入新的作用域,其它的代码块(如if、try、for等)是不会引入新的作用域的

if 2>1:
    x = 1 # if并没有引入一个新的作用域,x仍处在当前作用域中,后面代码可以使用
print(x)  # 1
def test():
    x = 2 # def、class、lambda是可以引入新作用域的
print(x) # NameError: name 'x2' is not defined

内部作用域要修改全局变量的值时,要使用global关键字,修改外部上级作用域变量要使用nonlocal关键字

4. 闭包函数

在函数内部定义函数,就是嵌套函数。


如果内部函数引用了外部函数的变量(甚至是外部函数之外,但不是全局变量),并且外部函数的返回值是内部函数,则该内部函数称为闭包函数,被内部函数引用的外部变量称为自由变量。


自由变量和这个非全局的内部函数一起构成了闭包,闭包中被内部函数引用的变量,不会因为外部函数调用结束而被释放掉,而是一直存在内存中,直到内部函数被调用结束。


由于作用域的原因,函数外部无法使用函数中定义的变量和内部函数,要想在函数外部调用内部函数,函数需要将内部函数名作为返回值返回。


# 闭包函数的基本形式:
def 外部函数名():
	内部函数需要的变量
     def 内部函数名():
         引用外部变量
     return 内部函数名
# 实例:为test函数添加计时功能
import time

def deco(func):
    def wrapper(): # wrapper函数为闭包函数
        print('{} is running'.format(func.__name__)) # func.__name__ 表示获取对象名
        starttime = time.time()
        func()
        endtime = time.time()
        print('程序执行时间为:%s' % (endtime - starttime))
    return wrapper

def test():
    print('我是test函数')
    time.sleep(1.5)

test = deco(test) # test作为实参传递给deco函数,调用deco函数,返回包含了test的wrapper,再存入test,此时test相当于是wrapper
test() # wrapper()

5. 函数装饰器

上述实例实际上是函数装饰器的雏形,装饰器函数本质上就是闭包函数的运用。


Python提供语法糖@来简化上述代码,将语法糖@符号与装饰器函数的名称一起使用,并将其放置在要装饰的函数的定义上方,这就意味着将被装饰函数作为参数传递给装饰器函数,并且将装饰后的函数返回

# 实例:使用装饰器为test函数添加计时功能
import time

def deco(func):
    def wrapper(): # wrapper函数为闭包函数
        print('{} is running'.format(func.__name__)) # func.__name__ 表示获取对象名
        starttime = time.time()
        func()
        endtime = time.time()
        print('程序执行时间为:%s' % (endtime - starttime))
    return wrapper

@deco # 等价于 test = deco(test)
def test():
    print('我是test函数')
    time.sleep(1.5)
test() # wrapper()
# 被装饰函数带有参数和返回值
import time

def deco(func):
    def wrapper(a, b): # wrapper函数为闭包函数
        print('{} is running'.format(func.__name__))
        starttime = time.time()
        res = func(a, b)
        endtime = time.time()
        print('程序执行时间为:%s' % (endtime - starttime))
        return res
    return wrapper

@deco # 等价于 test = deco(test)
def test(a, b):
    print('求两个数的和')
    c = a + b
    time.sleep(1.5)
    return c
test(1, 5) # wrapper(1, 5)

print(test.__name__) # wrapper

如果有多个被装饰函数,且它们的参数个数各不相同,这时候可以用 args 和 **kwargs 作为装饰器内部嵌套函数的参数,args 和 **kwargs 表示接受任意数量和类型的参数,由此可总结出通用装饰器函数的形式

def deco(func):
	def wrapper(*args, **kwargs):
    	"""被装饰函数执行之前的 *** 作"""
    	res = func(*args, **kwargs)
        """被装饰函数执行之后的 *** 作"""
        return res
    return wrapper  	

上述实例中被装饰函数test在运行后,其__name__属性发生变化,实际上test相当于是wrapper。


为了解决这个问题,保证被装饰函数__name__属性不变,我们可以使用functools模块里的wraps方法

# 不改变被装饰函数的__name__属性
import time
from functools import wraps

def deco(func):
    @wraps(func)
    def wrapper(*args, **kwargs): # wrapper函数为闭包函数
        print('{} is running'.format(func.__name__))
        starttime = time.time()
        res = func(*args, **kwargs)
        endtime = time.time()
        print('程序执行时间为:%s' % (endtime - starttime))
        return res
    return wrapper

@deco 
def test(a, b):
    print('求两个数的和')
    c = a + b
    time.sleep(1.5)
    return c
test(1, 5) 

print(test.__name__) # test

还可以借助Python第三方库的decorator模块(需事先安装)来避免使用闭包函数,进一步简化装饰器编写和使用,这种方法同样不会改变被装饰函数__name__属性

import time
from decorator import decorator

@decorator
def deco(func, *args, **kwargs):
    print('{} is running'.format(func.__name__))
    starttime = time.time()
    res = func(*args, **kwargs)
    endtime = time.time()
    print('程序执行时间为:%s' % (endtime - starttime))
    return res

@deco 
def test(a, b):
    print('求两个数的和')
    c = a + b
    time.sleep(1.5)
    return c
test(1, 5)

print(test.__name__) # test

因此可进一步总结出被装饰函数带参数的通用装饰器函数的形式

from functools import wraps

def deco(func):
	@wraps(func)
	def wrapper(*args, **kwargs):
    	"""被装饰函数执行之前的 *** 作"""
    	res = func(*args, **kwargs)
        """被装饰函数执行之后的 *** 作"""
        return res
    return wrapper  	
from decorator import decorator

@decorator
def deco(func, *args, **kwargs):
    """被装饰函数执行之前的 *** 作"""
    res = func(*args, **kwargs)
    """被装饰函数执行之后的 *** 作"""
    return res

定义装饰器的时候传入装饰器的第一个参数是被装饰的函数,有时装饰器函数还带有额外的参数,可以通过传递不同的参数使得装饰器更加灵活,此时装饰器需要三层嵌套结构,最外面的一层用于传递装饰器的额外参数

# 当装饰器函数的flag参数值为'true'时,延迟计时1秒
import time
from functools import wraps

def deco(flag): # 第一层,用于传递装饰器的额外参数flag
    def outer_wrapper(func): # 第二层
        @wraps(func)
        def wrapper(*args, **kwargs): # 第三层 
            print('{} is running'.format(func.__name__))
            if flag == 'true': 
                starttime = time.time()
                time.sleep(1)
                print('延迟计时1秒')
                res = func(*args, **kwargs)
                endtime = time.time()
                print('程序执行时间为:%s' % (endtime - starttime))
                return res
            else:
                starttime = time.time()
                res = func(*args, **kwargs)
                endtime = time.time()
                print('程序执行时间为:%s' % (endtime - starttime))
                return res
        return wrapper
    return outer_wrapper

@deco(flag = 'true') 
# 运行deco(flag = 'true')后返回outer_wrapper,此时@deco(flag = 'true')相当于是@outer_wrapper,然后将test作为实参传递给outer_wrapper运行
def test(a, b):
    print('求两个数的和')
    c = a + b
    time.sleep(1.5)
    return c
test(1, 5)

上述代码也可以借助decorator模块简化为两层嵌套结构

import time
from decorator import decorator

def deco(flag):
    @decorator
    def wrapper(func, *args, **kwargs):
            print('{} is running'.format(func.__name__))
            if flag == 'true':
                starttime = time.time()
                time.sleep(1)
                print('延迟计时1秒')
                res = func(*args, **kwargs)
                endtime = time.time()
                print('程序执行时间为:%s' % (endtime - starttime))
                return res
            else:
                starttime = time.time()
                res = func(*args, **kwargs)
                endtime = time.time()
                print('程序执行时间为:%s' % (endtime - starttime))
                return res
    return wrapper

@deco(flag = 'true')
def test(a, b):
    print('求两个数的和')
    c = a + b
    time.sleep(1.5)
    return c
test(1, 5)

因此可总结出装饰器函数带参数的通用装饰器函数

from functools import wraps

def deco(parameter):
    def outer_wrapper(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            """被装饰函数执行之前的 *** 作"""
            res = func(*args, **kwargs)
            """被装饰函数执行之后的 *** 作"""
            return res
        return wrapper
    return outer_wrapper	
from decorator import decorator

def deco(parameter):
    @decorator
    def wrapper(func, *args, **kwargs):
        """被装饰函数执行之前的 *** 作"""
        res = func(*args, **kwargs)
        """被装饰函数执行之后的 *** 作"""
        return res
    return wrapper

实例练习

# 编写一个装饰器,实现字符串首字母大写,其他小写
def cap(func):
    def inner(s):
        s=s[0].upper()+s[1:].lower()
        func(s)
    return inner

@cap
def test(a):
    print(a)
test('heLLo')
# 编写一个装饰器,拆解二维列表
def pre(a=[]):
    def inner(func):
        for i in a:
            func(*i)
    return inner

cases=[[1,2,3,4],[5,6,7,8],[9,10,11,12]]

@pre(cases) # @pre只执行pre函数,@pre()还会执行内部函数
def test(a,b,c,d):
    print('数据:%d %d %d %d'%(a,b,c,d))
# 拆解多个一维列表的装饰器
def pre(*a):
    def inner(func):
        for i in a:
            func(*i)
    return inner

@pre([1,2,3],[4,5,6]) # @pre只执行pre函数,@pre()还会执行内部函数
def test(a,b,c):
    print('数据:%d %d %d'%(a,b,c))

6. 类装饰器

7. 装饰器链

八.面向对象编程 1. 涵义

面向过程编程,把函数作为程序的基本单元。


程序设计时,根据需求将某些功能独立的代码封装成一个又一个函数,程序运行就是按照顺序去执行各个函数。


  • 优点:极大的降低了写程序的复杂度,只需要顺着要执行的步骤,堆叠代码即可。


  • 缺点:一套流水线或者流程就是用来解决一个问题,代码牵一发而动全身。


  • 应用场景:一旦完成基本很少改变的场景

面向对象编程(OPP),object oriented programming,把对象作为程序的基本单元,程序设计时,首先确定职责,根据职责设计不同的对象,在一个对象中封装多个方法,每个对象都可以接收其他对象发过来的消息,并处理这些消息,程序的执行就是一系列消息在各个对象之间传递,各个对象调用相关的方法。


  • 优点:提高程序的扩展性,对某一个对象单独修改,会立刻反映到整个体系中,更加适合应对复杂的需求变化

  • 缺点:可控性差,无法向面向过程的程序设计流水线式的可以很精准的预测问题的处理流程与结果,面向对象的程序一旦开始就由对象之间的交互解决问题

  • 应用场景:需求经常变化的场景

2. 基本概念
  • 类(Class): 是对一群具有 相同 特征(属性) 或者 行为(方法) 的事物的一个统称,是抽象的,不能直接使用。


    因此类就是用来描述具有相同属性和方法的对象的集合。


    它定义了该集合中每个对象所共有的属性和方法。


    对象被称作类的实例。


    类可以理解为是可用来创建实例对象的模板

  • 实例(对象):由类创建出来的一个个具体的“对象”,可以直接使用

3. 类和实例

3.1 创建类

类也是对象,同时,与字符串、列表等等类型一样,类也是一种对象类型(__class__属性为type,也就是说类其实是Python中type这个类的实例对象)

class ClassName: # 类名使用大驼峰命名规范
    类体 # 含有各种属性(变量)和方法(函数)

3.2 构造方法

__init__函数,也称构造函数(初始化方法),用于在创建类的时候,对这种对象类型进行初始化,也就是用于说明这种类型的基本结构。


  • 在创建对象(类的实例化)时构造函数会被自动调用

  • 创建类时可以不写构造函数,实例化对象时系统会自动隐式调用默认的无参构造函数

  • 构造函数可以有多个参数,但最少也要有一个 self 参数且必须作为第一个参数,如果有多个参数,创建对象时必须传入参数(self不需要手动传参),这些参数会自动传入构造函数中

  • 构造函数没有返回值

class Calculator:
    def __init__(self, num1, num2):
        self.num1 = num1
        self.num2 = num2
    def plus(self):
        sum = self.num1 + self.num2
        print(sum)
    def multiply(self):
        result = self.num1 * self.num2
        print(result)

calc = Calculator(2, 3)
calc.plus()

3.3 self参数

同一个类可以通过实例化产生多个对象,当某个对象调用类方法时,该对象会把自身的引用作为第一个参数自动传给该方法,换句话说,Python 会自动绑定类方法的第一个参数指向调用该方法的对象。


如此,Python解释器就能知道到底要 *** 作哪个对象的方法了

  • self 只是约定俗成的参数名,可以更改但不建议,相当于Java中的this

  • 类中的方法(无论是构造方法还是实例方法,最少要包含一个参数),第一个参数必须写self,不能省略

  • self 指向类的实例对象本身,而不是指向类本身,

  • self 不需要手动传参,在调用类时会自动将当前的实例对象传递给self

class Test:
    def prt(self):
        print(self)
        print(self.__class__)

t = Test()
t.prt() # 相当于是Test.prt(t),将test类的实例对象t传给self
Test.prt(t)

3.4 类的实例化

创建一个类的实例即类的具体对象的过程,又称为类的实例化,基本语法格式如下:

# 定义类
class ClassName: 
    类体 # 含有各种属性(变量)和方法(函数)
# 实例化    
instance = ClassName() # instance就是ClassName类实例化的对象

定义的类只有进行实例化,也就是使用该类创建对象之后,才能得到利用

instance.变量名
instance.方法名(参数)
class Student:
    # 下面定义了2个类变量
    school = '长江中学'
    grade = '高二(3)班'
    def __init__(self, name, gender):
        # 下面定义2个实例变量
        self.name = name
        self.gender = gender
    # 下面定义了1个实例方法
    def score(self, math, physics, chemistry):
        total = math + physics + chemistry
        if 80 <= total <= 100:
            print('姓名:{}\n总分数:{}\n综合成绩:A'.format(self.name, total))
        if 60 <= total < 80:
            print('姓名:{}\n总分数:{}\n综合成绩:B'.format(self.name, total))
        if 0 <= total < 60:
            print('姓名:{}\n总分数:{}\n综合成绩:C'.format(self.name, total))

student = Student('jack', 'male') # 实例化
student.score(25, 20, 30)

3.5 类变量与实例变量

类变量:是在类体中,但在各个类方法外定义的变量

  • 类变量在所有实例化的对象中是公用的

  • 既可以使用类名直接调用,也可以使用类的实例化对象调用

  • 通过类名不仅可以调用类变量,也可以修改它的值,动态地为类和对象添加类变量

  • 通过类的实例化对象是无法修改类变量的。


    通过实例化对象对类变量赋值,其本质将不再是修改类变量的值,而是在给该对象定义新的实例变量,因此,任何时候都建议用类名的方式访问类变量

实例变量:是在类体中,所有类方法内部以self.变量名的方式定义的变量

  • 实例变量只能通过对象名访问,无法通过类名访问

  • 实例变量只作用于当前调用方法的实例

  • 由于在实例化时类的构造方法会被自动调用,因此一个类的所有实例对象都包含构造方法里的实例变量

  • 实例变量和类变量可以同名,但这种情况下使用类对象将无法调用类变量,它会首选实例变量,这也是不推荐“类变量使用对象名调用”的原因

  • 修改某个对象的实例变量的值,不会影响类的其它实例化对象,更不会影响同名的类变量

class A(object):
    var1 = 1 # 类变量

    def func1(self):
        A.var1 = 2 # 通过类名访问并修改类变量的值
        self.var2 = 3 # 实例变量

    def func2(self):
        A.var1 = 4 # 通过类名访问并修改类变量的值
        self.var2 = 5 # 实例变量

# 实例化
a = A()
b = A()
c = A()

# 调用类的方法
b.func1() # func1中的实例变量作用于对象b
c.func2() # func2中的实例变量作用于对象c

# 通过类名访问类变量
print(A.var1) # 4
# 通过对象名访问类变量
print(a.var1) # 4
print(b.var1) # 4
print(c.var1) # 4
# 通过对象名访问实例变量
print(b.var2) # 3
print(c.var2) # 5
# 通过对象名修改类变量的值
b.var1 = 6 # 通过实例化对象对类变量赋值,其本质将不再是修改类变量的值,而是在给该对象定义新的实例变量
c.var1 = 7
print(A.var1) # 4
print(b.var1) # 6 
print(c.var1) # 7

3.6 实例方法、类方法与静态方法

类方法:在类体中,采用 @classmethod 装饰器修饰的方法为类方法

  • 类方法和实例方法相似,它最少也要包含一个参数,只不过类方法中通常将其命名为 cls

  • Python 会自动将类本身绑定给 cls 参数(注意,绑定的不是类对象)。


    也就是说,我们在调用类方法时,无需显式为 cls 参数传参

  • 类方法的调用,既可以使用类名,也可以使用类对象

静态方法:在类体中,采用 @staticmethod 装饰器修饰的方法为类方法

  • 静态方法没有类似 self、cls 这样的特殊参数,因此 Python 解释器不会对它包含的参数做任何类或对象的绑定,也正因为如此,类的静态方法中无法调用任何类属性和类方法。


  • 静态方法的调用,既可以使用类名,也可以使用类对象

实例方法:在类体中,不用任何修改的方法为实例方法,通常情况下,在类中定义的方法默认都是实例方法

  • 构造方法理论上也属于实例方法

  • 最少要包含一个 self 参数,用于绑定调用此方法的实例对象

  • 实例方法通常用类对象直接调用

  • Python 也支持使用类名调用实例方法,但此方式需要手动给 self 参数传值

class A(object):
    def __init__(self):
        print('这是构造方法')

    def func1(self):
        print('这是实例方法')

    @classmethod
    def func2(cls):
        print('这是类方法')

    @staticmethod
    def func3():
        print('这是静态方法')


a = A()

a.func1()
A.func1(a)
a.func2()
A.func2()
a.func3()
A.func3()

3.7 扩展:新式类与经典类

在Python 2及以前的版本中,由任意内置类型派生出的类(只要一个内置类型位于类树的某个位置),都属于“新式类”,都会获得所有“新式类”的特性;反之,即不由任意内置类型派生出的类,则称之为“经典类”。


“新式类”和“经典类”的区分在Python 3之后就已经不存在,在Python 3.x之后的版本,因为所有的类都派生自内置类型object(不必显式的继承object),即所有的类都是“新式类”。


在多重继承关系下,新式类的MRO(Method Resolution Order, 方法解析顺序),采用的是从左到右,广度优先的方式进行查找

# Python 3.x中默认都是新式类,不必显式的继承object,object可以不写
class A(object): 
    def name(self):
        return 'A'
 
class B(A):
    pass
 
class C(A):
    def name(self):
        return 'C'
 
class D(B, C):
    pass
 
if __name__ == '__main__':
    print(D().name()) # 查找顺序为D->B->C->A,因此print(D().name())调用的是类C的name方法,将输出字符C

在多重继承关系下,经典类的MRO(Method Resolution Order, 方法解析顺序),方法和属性的查找链是按照从左到右,深度优先的方式进行查找

# 所有经典类的代码必须在Python2下运行
class A():
    def name(self):
        return 'A'
 
class B(A):
    pass
 
class C(A):
    def name(self):
        return 'C'
 
class D(B, C):
    pass
 
if __name__ == '__main__':
    print(D().name()) # 查找顺序为D->B->A->C,因此print(D().name())调用的是类A的name方法,将输出字符A

新式类相同父类只执行一次构造函数,经典类重复执行多次

4. 三大特性

4.1 封装

涵义:隐藏对象的属性和实现细节,只对外提供必要的公共访问方法。


作用:隔离复杂度,便于使用。


提高数据安全性和复用性

4.1.1 私有属性和私有方法

Python对于类的成员没有严格的访问控制限制,这与其他面相对对象语言有区别。


关于私有属性和私有方法,有如下要点:

  • 通常我们约定,两个下划线开头的属性(方法)是私有的(private),其他为公共的(public)

  • 类内部可以访问私有属性(方法)

  • 在类外部无法直接访问双下滑线开头的属性,但可以通过 _类名__私有属性(方法)名 的方式访问,因此,这种 *** 作并没有严格意义上地限制外部访问,仅仅只是一种语法意义上的名称变形,这种变形只针对外部

class Demo:
    __price = 25.8 # 私有类属性

    def __init__(self, u_name, u_age):
        self.uname = u_name # 公有属性
        self.__uage = u_age # 私有属性

    # 私有方法
    def __age(self):
        print("这是私有方法")
        print("调用共有属性:", self.uname)
        print("调用私有属性:", self.__uage) # 类内部调用私有属性
        print("调用私有类属性:", self.__price) # 类内部调用私有类属性

    # 公有方法    
    def name(self):
        print("这是公有方法")
        print("调用共有属性:", self.uname)
        print("调用私有属性:", self.__uage)  # 类内部调用私有属性
        print("调用私有类属性:", self.__price) # 类内部调用私有类属性

        
d = Demo('Tom', 18)
print(dir(d)) # 使用 dir()函数可以查看对象内的所有的属性和方法

# 调用共有方法
d.name

# 调用私有方法(正确示例)
d._Demo__age()
d._Demo__

# 调用私有方法(错误示范)
d.__age()

# 调用私有属性
print(Demo._Demo__price) # 25.8
print(d._Demo__uage) # 18
  • 变形的过程只在类的定义时发生一次,之后定义的__开头的属性都不会变形

class Foo:
    __x = 1  # 变形为 _Foo__x = 1

    def __f1(self):  # 变形为 _Foo__f1
        print('from test')

    def f2(self):
        print(self.__x) # print(self._Foo__x)
        print(self.__f1) # print(self._Foo__f1)

Foo.__y=3 # 不会发生变形
print(Foo.__dict__) # 查看类的__dict__属性
print(Foo.__y)
  • 在子类定义的__x不会覆盖在父类定义的__x,因为子类中变形成了:_子类名__x,而父类中变形成了:_父类名__x,即双下滑线开头的属性在继承给子类时,子类是无法覆盖的

# 正常情况
class A:
    def fun(self):
        print('from A')
    def test(self):
        self.fun()

class B(A):
    def fun(self):
        print('from B')

b = B()
b.test() # from B
# 把fun定义成私有的,即__fun
# 正常情况
class A:
    def __fun(self): # 在定义时变形为_A__fun
        print('from A')
    def test(self):
        self.__fun() # self._A__fun()

class B(A):
    def fun(self):
        print('from B')

b = B()
b.test() # b._A__fun()
  • 除此之外,还可以定义以单下划线_开头的类属性或者类方法(例如 _name、_display(self)),以单下划线开头的表示的是protected类型的变量,即只能允许其本身与子类进行访问;同时表示弱内部变量标示。


    虽然它们也能通过类对象正常访问,但这是一种约定俗成的用法。


    通常通过单下划线_来实现模块级别的私有化,一般约定以单下划线_开头的变量、函数为模块私有的,也就是说 from moduleName import 将不会引入以单下划线_开头的变量、函数。


实例

class Test:
    # 核心私有方法,用于发短信
    def __send_msg(self):
        print('...正在发送短信...')
        
    # 公共方法,满足一定条件后调用核心私有方法
    def send_msg(self, new_money):
        if new_money > 100:
            self.__send_msg()
        else:
            print('余额不足,请先充值')
            
t = Test()
t.send_msg(80)
t.send_msg(120)

4.1.2 property() 与 @property

通过定义私有属性(方法)可以将数据隐藏起来,从而限制了类外部对数据的直接 *** 作,同时类的内部应该提供相应的接口来允许类外部间接地访问和 *** 作数据,并且接口之上还可以附加额外的逻辑来对数据的 *** 作进行严格地控制,规避脏数据。


封装的意义之一便是对数据加以访问保护机制。


class Student():

    def __init__(self,name,age):
        self.__name = name
        self.__age = age

    def get_name(self):
        return self.__name

    def set_name(self,name):
        if not isinstance(name, str):
            raise TypeError("Expected a string")
        elif len(name) <= 1 :
            print("name的长度必须要大于1")
        else:
            self.__name = name

    def get_age(self):
        return self.__age

    def set_age(self, age):
        if age > 0 and age < 100:
            self.__age = age
        else:
            print("输入的年龄必须要大于0,小于100岁")

stu = Student('jack',18)

stu.set_name('louis')       # 通过接口设置数据,可以有效规避脏数据
print(stu.get_name())       # 通过接口获取数据

stu.set_age(-9)         # 通过接口设置数据,可以有效规避脏数据
print(stu.get_age())    # 通过接口获取数据

通过使用property() 属性函数可简化内部访问器接口方法的调用,即简化为 类名.属性名 的方式调用属性的方法

property() 函数的作用是在新式类中返回属性值

基本格式:属性名 = property(fget=None, fset=None, fdel=None, doc=None)

  • property()函数具有四个可选参数

    fget:用于获取属性值的方法,读 fset:用于设置属性值的方法,写 fdel:用于删除属性值的方法,删 doc:属性的说明文档字符串

  • 类名.属性名 将触发 getter

    类名.属性名 = 值 将触发 setter

    del 类名.属性名 将触发 deleter

class Student():

    def __init__(self):
        self.__name = None
        self.__age = None

    def getname(self):
        print('获取姓名属性时执行的代码')
        return self.__name

    def setname(self,name):
        print('设置姓名属性时执行的代码')
        if not isinstance(name, str):
            raise TypeError("Expected a string")
        elif len(name) <= 1 :
            print("name的长度必须要大于1")
        else:
            self.__name = name

    def delname(self):
        print('删除姓名属性时执行的代码')
        del self.__name

    stuname = property(getname, setname, delname, '学生姓名')

    def getage(self):
        print('获取年龄属性时执行的代码')
        return self.__age

    def setage(self, age):
        print('设置年龄属性时执行的代码')
        if age > 0 and age < 100:
            self.__age = age
        else:
            print("输入的年龄必须要大于0,小于100岁")

    def delage(self):
        print('删除年龄属性时执行的代码')
        del self.__age

    stuage = property(getage, setage, delage, '学生年龄')


stu = Student()

print('查看姓名属性的文档字符串:' + Student.stuname.__doc__)
stu.stuname = '张三' # 设置属性
print(stu.stuname) # 获取属性
del stu.stuname # 删除属性

print('查看年龄属性的文档字符串:' + Student.stuage.__doc__)
stu.stu_age = 170 # 设置属性
print(stu.stuage) # 获取属性
del stu.stuage # 删除属性

@property 语法糖提供了比 property() 函数更简洁直观的写法。


property的使用将函数调用转变为属性调用,遵循了统一访问的原则。


使用@property可以非常容易的实现属性的读写控制(类型检查、限定取值以及添加其他功能等)

  • 被 @property 装饰的方法是获取属性值的方法(代表只读),被装饰方法的名字会被用做 属性名(不能与类里的其他属性名相同)。


  • 被 @属性名.setter 装饰的方法是设置属性值的方法(代表可写)

  • 被 @属性名.deleter 装饰的方法是删除属性值的方法(代表可删)

class Student():

    def __init__(self):
        self.__name = None
        self.__age = None

    @property
    def stuname(self):
        print('获取姓名属性时执行的代码')
        return self.__name

    @stuname.setter
    def stuname(self,name):
        print('设置姓名属性时执行的代码')
        if not isinstance(name, str):
            raise TypeError("Expected a string")
        elif len(name) <= 1 :
            print("name的长度必须要大于1")
        else:
            self.__name = name

    @stuname.deleter
    def stuname(self):
        print('删除姓名属性时执行的代码')
        del self.__name


    @property
    def stuage(self):
        print('获取年龄属性时执行的代码')
        return self.__age

    @stuage.setter
    def stuage(self, age):
        print('设置年龄属性时执行的代码')
        if age > 0 and age < 100:
            self.__age = age
        else:
            print("输入的年龄必须要大于0,小于100岁")

    @stuage.deleter
    def stuage(self):
        print('删除年龄属性时执行的代码')
        del self.__age


stu = Student()

stu.stuname = '张三' # 设置属性
print(stu.stuname) # 获取属性
del stu.stuname # 删除属性

stu.stuage = 170 # 设置属性
print(stu.stuage) # 获取属性
del stu.stuage # 删除属性
  • 可以省略设置属性值的方法,此时该属性变成只读属性。


    如果此时仍然设置属性,会抛出异常 AttributeError: can't set attribute


  • 如果报错 RecursionError: maximum recursion depth exceeded while calling a Python object,很可能是对象属性名和 @property 装饰的方法名重名了,一般会在对象属性名前加一个下划线 _ 避免重名,并且表明这是一个受保护的属性

4.1.3 描述符

4.1.3.1 涵义

Property()函数其实是(数据)描述符协议函数,描述符协议是一组成员函数定义,包括:

函数作用返回值是否必须
__get__(self, obj, type)获取属性值属性的值
__set__(self, obj, value)设置属性值None
__delete__(self, obj)删除属性None

如果一个类实现了以上任意成员函数之一,则它便是一个描述符类,其实例对象便是一个描述符,描述符不能定义在被使用类的构造函数中,只能定义为被使用类的属性,它只属于类,不属于实例。


也就是说,一个描述符是一个有“绑定行为”的对象属性(object attribute),其属性访问机制被描述符协议__get____set____delete__方法重写。


上述函数中,self是描述符类的实例对象,即下例中的Foo.attrobj指使用描述符的实例对象,即下例中的footypeobj的类型,即下例中的Foovalue指属性的值,即下例中的new value

# MyDescriptor是描述符类
class MyDescriptor:
    def __init__(self):
        self.data = None
    def __get__(self, obj, type):
        print('get called')
        return self.data
    def __set__(self, obj, value):
        print('set called')
        self.data = value
    def __delete__(self, obj):
        print('delete called')
        del self.data

class Foo:
    attr = MyDescriptor() # Foo 类的 attr 属性为 MyDescriptor 类的实例对象,因此 attr 是一个描述符(类属性)

foo = Foo()

print(foo.attr) # 当访问 foo 的 attr 属性时, MyDescriptor 的 __get__ 函数被调用,输出结果为 get called None
foo.attr = 'new value'  # 当为 attr 设置一个新值时, MyDescriptor 的 __set__ 函数被调用
print(foo.attr) # 输出结果为 get called new value
del foo.attr # 删除属性 attr 时, MyDescriptor 的 __delete__ 函数被调用,输出结果为 delete called

4.1.3.2 作用

描述符(Descriptor)是Python语言独有的特性,是一种创建托管属性的方法(一个类可以将属性控制全权委托给描述符类)。


描述符具有诸多优点,诸如:保护属性不受修改、属性类型检查和自动更新某个依赖属性的值等。


描述符是实现大部分Python类特性中最底层的数据结构的实现手段,我们常使用的@classmethod、@staticmethd、@property、super甚至是__slots__等属性都是通过描述符来实现的。


它是很多高级库和框架的重要工具之一,是使用到装饰器或者元类的大型框架中的一个非常重要组件。


4.1.3.3 类型

数据描述符(data descriptor):实现了__get____set__ 两种方法的描述符

非数据描述符(non-data descriptor):只实现了__get__ 一种方法的描述符

4.1.3.4 属性访问机制

当对一个实例属性进行访问时,Python 会按 obj.__dict__type(obj).__dict__type(obj)的父类.__dict__ 顺序进行查找,如果查找到目标属性并发现是一个描述符,Python 会调用描述符协议来改变默认的属性访问机制。


(1)__dict__属性

在 Python 类的内部,无论是类属性还是实例属性,都是以字典的形式进行存储的,其中属性名作为键,而属性值作为该键对应的值。


为了方便用户查看类中包含哪些属性,Python 类提供了 __dict__ 属性。


需要注意的一点是,该属性可以用类名或者类的实例对象来调用,用类名直接调用 __dict__,会输出由该类中所有类属性组成的字典;而使用类的实例对象调用 __dict__,会输出由类中所有实例属性组成的字典

  • 类的__dict__属性,返回类内部所有类属性、内置属性和⽅法组成的字典。


  • 类的实例对象的__dict__属性,返回实例属性和值组成的字典

  • 当有继承关系时,父类的__dict__ 并不会影响子类的__dict__,子类自己的 __dict__不会包含父类的__dict__

  • 内置的数据类型(int、list等)没有__dict__属性

  • 可以直接通过__dict__访问、设置、修改、删除属性,例如 obj.__dict__['x'] = 3

class Parent():
    a = 0
    b = 1

    def __init__(self):
        self.a = 2
        self.b = 3

    def p_test(self):
        print('a normal func.')

    @staticmethod
    def static_test(self):
        print( 'a static func.')

    @classmethod
    def class_test(self):
        print ('a calss func.')

class Child(Parent):
    c = 2
    d = 3

    def __init__(self):
        self.c = 4
        self.d = 5

    def c_test(self):
        print('a normal func.')


# 创建对象
p = Parent()
c = Child()
# 类.__dict__
print(Parent.__dict__)
print(Child.__dict__)
# 类对象.__dict__
print(p.__dict__)
print(c.__dict__)

(2)属性访问的默认机制

实例属性字典 → 类属性字典 → 父类属性字典 →...→ object 属性字典,即 obj.__dict__['x'] -> class.__dict__['x'] -> super().__dict__['x'] ->...-> object.__dict__['x']

属性查找链与方法解析顺序(MRO)基本一致,Python的每一个有父类的类都有一个与方法解析顺序相关的特殊属性:__mro__, 它以元组形式存储方法解析时的对象查找顺序,从左往右越靠前的优先级越高

(3)属性访问相关的魔术方法

__getattribute__是实例对象查找属性或方法的入口,实例对象访问属性或方法时都需要调用到__getattribute__,之后才会根据默认机制在各个__dict__中查找相应的属性值或方法对象,若没有找到则最后会调用__getattr__

__getattribute__(self, name)

通过实例对象访问属性时(比如 obj.attr),无论属性是否存在,该方法都会被无条件调用,其中传入的name参数指的是所访问的属性名(字符串),该方法应当返回一个值或者抛出一个AttributeError异常(当属性不存在)

该方法可以拦截对对象属性的所有访问企图,当对象属性被访问时,自动调用该方法(只适用于新式类),可以通过重写该方法,将传入的属性处理后返回给调用者,实现属性访问控制等特定功能

使用类名调用类属性时,不会调用__getattribute__方法,该内建方法只针对实例对象对属性的调用,包括对类属性的调用

需要注意的是,正式由于它拦截对所有属性的访问(包括对__dict__的访问),在使用中要避免发生无线递归调用的问题。


__getattribute__方法中访问当前实例的属性时,唯一安全的方式是使用基类(超类) 的__getattribute__()方法,比如 object.__getattribute__(self, name)或者 super().__getattribute__(attr)

class Student():
    country = "china"  # 类属性不会存到实例对象s1的__dict__中

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __getattribute__(self, attr):  # 注意:attr传入的是属性名,不是属性值
        print("开始属性校验拦截功能")
        print(attr) # 打印访问的属性名
        return object.__getattribute__(self, attr)  # 返回调用attr属性的结果
        # return super().__getattribute__(attr)


s1 = Student("tom", 19)
print(Student.country)# 通过类名调用属性,不会调用__getattribute__方法
print(s1.country) # 通过实例对象调用类属性,会自动调用__getattribute__方法
print(s1.name) # 通过实例对象调用实例属性,也会自动调用__getattribute__方法
print(s1.age)
# 重写__getattribute__方法,实现特定功能
class Student():
    country = "china"  # 类属性不会存到实例对象s1的__dict__中

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __getattribute__(self, attr):  # 注意:attr传入的是属性名,不是属性值
        print("开始属性校验拦截功能")
        result = object.__getattribute__(self, attr)
        if attr == 'name':
            print('现在调用的是name属性')
            if len(result) <= 1:
                print('姓名长度不能小于2')
            else:
                private_name = result[0]+'*'+result[-1]
                return private_name
        if attr == 'age':
            print('现在调用的是age属性')
            if result <= 0 or result >= 100:
                print('年龄只能在1到99岁之间')
            else:
                private_age = '年龄'+str(result)+'岁'
                return private_age


s1 = Student('jack', 18)
print(s1.name)
print(s1.age)

__getattr__(self, name)

当默认属性访问抛出 AttributeError 异常(可能是 __getattribute__ 无法找到对应实例的属性而抛出 AttributeError 异常,或者实例本身无法在继承树上找到对应的实例,再或者是描述符对象的 __get__ 对该属性抛出 AttributeError异常导致)时,该方法被调用。


该方法应当返回一个值或者抛出一个 AttributeError 异常。


如果按正常的属性搜索机制找到了属性,__getattr__不会被调用

如果类同时存在__getattr____getattribute__,并且如果__getattribute__抛出了AttributeError异常,那么该异常将会被忽略,__getattr__才会被调用

class Student():
    country = "china"  # 类属性不会存到实例对象s1的__dict__中

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __getattribute__(self, attr):  # 注意:attr传入的是属性名,不是属性值
        print("开始属性校验拦截功能")
        print(attr) # 打印访问的属性名
        #if attr not in object.__getattribute__(self, '__dict__'):
        	#raise AttributeError
        return object.__getattribute__(self, attr) # 返回调用attr属性的结果,如果attr不存在抛,会出AttributeError错误

    def __getattr__(self, attr):
        print('开始执行__getattr__')
        return attr

s1 = Student("tom", 19)
print(s1.sex)

(4)描述符对属性访问机制的影响

当描述符与实例属性同名时,数据描述符会覆盖实例对象的__dict__属性字典中的对应属性,非数据描述符属性会被实例对象的__dict__属性字典中的对应属性覆盖

# 数据描述符
class DataDesc:
    def __init__(self, default=None):
        self._name = default

    def __set__(self, obj, value):
        print("访问数据描述符里的 __set__")
        self._name = value

    def __get__(self, obj, type):
        print("访问数据描述符里的 __get__")
        return self._name

# 非数据描述符
class NonDataDesc:
    def __init__(self, default=None):
        self._age = default

    def __get__(self, obj, type):
        print("访问非数据描述符里的 __get__")
        return self._age


class Student:
    country = 'china'
    name = DataDesc(0)
    age = NonDataDesc(0)

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __getattribute__(self, item):
        print("调用 __getattribute__")
        return super(Student, self).__getattribute__(item)

    def __repr__(self):
        return "".format(
                self.name, self.country, self.age)

print(Student.__mro__) # 查看方法解析顺序
std = Student('jack', 18) # 设置实例属性age = 18,并且调用数据描述符的__set__方法,设置std._name = 'jack'
print(Student.__dict__) # 查看Student类的属性字典
print(std.__dict__) # 查看实例对象std的属性字典(查找实例对象的__dict__属性也会调用__getattribute__方法),由于name属性被数据描述符覆盖,因此其中不包含name属性
print(std.name) # 先调用__getattribute__方法,然后查找是否存在数据描述符,存在则调用数据描述符的__get__方法,获取std._name
print(std.age) # 调用__getattribute__方法,然后查找是否存在数据描述符,不存在则查找实例属性字典,找到实例属性age = 18

(5)总结

实例绑定的属性访问 obj.attr

【获取属性】:obj.attr

  1. 查找类中是否重写了__getattribute__方法

    1. 如果有则直接调用重写的__getattribute__方法

    2. 如果没有则调用默认的__getattribute__方法,跳转到步骤2

  2. 依次查找Cls.__mro__的类字典__dict__中是否存在属性attr(数据描述符会覆盖同名的实例属性,所以首先确定是否有数据描述符)

    1. 如果存在,且是数据描述符,则直接调用该数据描述符的__get__方法

    2. 否则(存在但是非数据描述符或者是普通属性,或者不存在),则跳转到步骤3

  3. 查找实例属性字典__dict__中是否存在

    1. 如果实例属性字典__dict__中存在,则返回实例属性字典中的 attr

    2. 如果实例属性字典__dict__中不存在

      1. 如果是非数据描述符,则获取属性时调用描述符协议中的__get__方法

      2. 如果是普通属性,则直接获取该属性

      3. 否则未找到attr或者以上步骤抛出 AttributeError 异常,则查找 Cls 类中是否重写了__getattr__方法

        1. 如果有则直接调用重写的__getattribute__方法

        2. 否则不存在属性attr,抛出 AttributeError 异常

【设置属性】:obj.attr = value

  1. 查找类中是否重写了__setattr__方法

    1. 如果有则直接调用重写的__setattr__方法

    2. 如果没有则调用默认的__setattr__方法,跳转到步骤2

  2. 依次查找Cls.__mro__的类字典__dict__中是否存在属性attr

    1. 如果存在,且是数据描述符(准确的说只需要定义__set__即可),则直接调用该数据描述符的__set__方法

    2. 否则(存在但是非数据描述符或者是普通属性,或者不存在),则跳转到步骤3

  3. 查找实例属性字典__dict__中是否存在

    1. 如果实例属性字典__dict__中存在,则直接对attr赋值

    2. 如果实例属性字典__dict__中不存在,则在实例属性字典中添加新的属性attr

【删除属性】:del obj.attr

  1. 查找类中是否重写了__delattr__方法

    1. 如果有则直接调用重写的__delattr__方法

    2. 如果没有则调用默认的__delattr__方法,跳转到步骤2

  2. 依次查找Cls.__mro__的类字典__dict__中是否存在属性attr

    1. 如果存在,且是描述符(只需要定义__del__即可),则直接调用该数据描述符的__del__方法

    2. 否则(存在但是未定义__del__的描述符或者是普通属性,或者不存在),则跳转到步骤3

  3. 查找实例属性字典__dict__中是否存在

    1. 如果实例属性字典__dict__中存在,则删除该属性

    2. 否则,无法删除不存在的属性attr,抛出AttributeError异常

类绑定的属性访问 Cls.attr

【获取属性】:Cls.attr

  1. 查找 MetaCls 类中是否重写了__getattribute__方法

    1. 如果有则直接调用重写的__getattribute__方法

    2. 如果没有则调用默认的__getattribute__方法,跳转到步骤2

  2. 依次查找MetaCls.__mro__的类字典__dict__中是否存在属性attr

    1. 如果存在,且是数据描述符,则返回Descr.__get__(attr, Cls, MetaCls)

    2. 否则(存在但是非数据描述符或者是普通属性,或者不存在),则跳转到步骤3

  3. 依次查找Cls.__mro__的类字典__dict__中是否存在

    1. 如果存在,且是Cls中的描述符(定义__get__即可),则返回Descr.__get__(attr, None, Cls)

    2. 如果存在,且是Cls中的未定义__get__的描述符或普通属性,则直接返回attr

    3. 如果不存在

      1. 如果是 MetaCls 中的非数据描述符,则返回Descr.__get__(attr, Cls, MetaCls)

      2. 如果是 MetaCls 中的普通属性,则直接获取该属性

      3. 否则未找到attr或者以上步骤抛出 AttributeError 异常,则查找 MetaCls 类中是否重写了__getattr__方法

        1. 如果有则直接调用重写的__getattribute__方法

        2. 否则不存在属性attr,抛出 AttributeError 异常

【设置属性】:Cls.attr = value

  1. 查找 MetaCls 类中是否重写了__setattr__方法

    1. 如果有则直接调用重写的__setattr__方法

    2. 如果没有则调用默认的__setattr__方法,跳转到步骤2

  2. 依次查找MetaCls.__mro__的类字典__dict__中是否存在属性attr

    1. 如果存在,且是描述符(只需要定义__set__即可),则直接调用该数据描述符的__set__方法

    2. 否则(存在但是非数据描述符或者是普通属性,或者不存在),则跳转到步骤3

  3. 查找类属性字典__dict__中是否存在

    1. 如果类属性字典__dict__中存在,则直接对attr赋值

    2. 如果类属性字典__dict__中不存在,则在类属性字典中添加新的属性attr

【删除属性】:del Cls.attr

  1. 查找 MetaCls 类中是否重写了__delattr__方法

    1. 如果有则直接调用重写的__delattr__方法

    2. 如果没有则调用默认的__delattr__方法,跳转到步骤2

  2. 依次查找MetaCls.__mro__的类字典__dict__中是否存在属性attr

    1. 如果存在,且是描述符(只需要定义__del__即可),则直接调用该数据描述符的__del__方法

    2. 否则(存在但是未定义__del__的描述符或者是普通属性,或者不存在),则跳转到步骤3

  3. 查找类属性字典__dict__中是否存在

    1. 如果类属性字典__dict__中存在,则删除该属性

    2. 否则,无法删除不存在的属性attr,抛出AttributeError异常

4.1.3.5 描述符与property

property 的实现是 C 实现,但是也可以用纯Python 版本模拟实现 property,代码如下:(待研究)

class property:
    "Emulate PyProperty_Type() in Objects/descrobject.c"

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)

参考示例

property是python提供的一个轻量级的数据描述符类,便于对属性的访问加以控制

# @property装饰器,实现学生成绩管理(限定数学、语文、英语分数的取值范围0到100)
class Student:
    def __init__(self, name, math, chinese, english):
        print('运行__init__')
        self.name = name
        self.math = math
        self.chinese = chinese
        self.english = english

    @property
    def math(self):
        print('in math getter')
        return self._math

    @math.setter
    def math(self, value):
        print('in math setter')
        if 0 <= value <= 100:
            self._math = value
        else:
            raise ValueError("Valid value must be in [0, 100]")

    @property
    def chinese(self):
        print('in chinese getter')
        return self._chinese

    @chinese.setter
    def chinese(self, value):
        print('in chinese setter')
        if 0 <= value <= 100:
            self._chinese = value
        else:
            raise ValueError("Valid value must be in [0, 100]")

    @property
    def english(self):
        print('in english getter')
        return self._english

    @english.setter
    def english(self, value):
        print('in english setter')
        if 0 <= value <= 100:
            self._english = value
        else:
            raise ValueError("Valid value must be in [0, 100]")

    # 重写__repr__方法,自定义输出实例化对象时的信息
    def __repr__(self):
        return "".format(
                self.name, self.math, self.chinese, self.english
            )

stu = Student('小明', 76, 87, 68) # 实例化Student,运行构造函数,stu.math = 76,调用被@math.setter装饰的方法,stu._math = 76
print(stu) # 相当于执行 print(stu.__repr__()),在__repr__方法中返回stu.math,会调用被@property装饰的方法,返回stu._math即76
print(stu.__dict__)
# 使用描述符,实现学生成绩管理(限定数学、语文、英语分数的取值范围0到100)
class Score:
    def __init__(self, default=0):
        print('运行Score的__int__')
        self._score = default

    def __set__(self, instance, value):
        print('调用__set__方法')
        if not isinstance(value, int):
            raise TypeError('Score must be integer')
        if not 0 <= value <= 100:
            raise ValueError('Valid value must be in [0, 100]')

        self._score = value

    def __get__(self, instance, owner):
        print('调用__get__方法')
        return self._score

    def __delete__(self):
        print('调用__delete__方法')
        del self._score

class Student:
    # 创建三个描述符实例对象,执行Score类中的构造方法
    math = Score(0)
    chinese = Score(0)
    english = Score(0)

    def __init__(self, name, math, chinese, english):
        
        self.name = name
        self.math = math
        self.chinese = chinese
        self.english = english


    def __repr__(self):
        return "".format(
                self.name, self.math, self.chinese, self.english
            )

stu = Student('小明', 76, 87, 68)# 实例化Student,运行构造函数,由于查找到math为数据描述符,则直接调用描述符协议的__set__方法,math._score = 76
print(stu)
print(stu.__dict__) # 实例属性字典中只有name,其他三个math,chinese,english都是类属性(数据描述符)
# 优化
class Score:
    def __init__(self, subject):
        print('运行Score的__init__')
        self.name = subject

    def __get__(self, instance, owner):
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if not isinstance(value, int):
            raise TypeError('Score must be integer')
        if 0 <= value <= 100:
            instance.__dict__[self.name] = value # 由于数据描述符覆盖了实例属性,赋值意味着在实例属性字典中添加属性,math,chinese,english被加入
        else:
            raise ValueError('Valid value must be in [0, 100]')


class Student:
    # 创建三个描述符实例对象,执行Score类中的构造方法
    math = Score("math") # math.name = 'math'
    chinese = Score("chinese")
    english = Score("english")

    def __init__(self, name, math, chinese, english):
        print('运行Student的__int__')
        self.name = name
        self.math = math
        self.chinese = chinese
        self.english = english

    def __repr__(self):
        return "".format(
                self.name, self.math, self.chinese, self.english
            )

stu = Student('jack', 76, 87, 68) # 实例化Student,运行构造函数,由于查找到math为数据描述符,则直接调用描述符协议的__set__方法,stu.__dict__['math'] = 76,将math属性添加到实例属性字典
print(stu)
print(stu.__dict__) # {'name': 'jack', 'math': 76, 'chinese': 87, 'english': 68}

4.1.3.6 描述符与静态方法(statisticmethod)

4.1.3.7 描述符与类方法(classmethod)

4.2 继承

4.2.1 概要

涵义:

继承是一种创建新类的方式,即创建一个派生类(derived class)可以继承基类(base class)的属性和方法,通过继承创建的新类称为子类(或派生类),被继承的类称为父类(或基类、超类)

类与类之间的继承关系实际上指的是 什么“是”什么 的关系,即 “is”关系,要想找出子类与父类的继承关系必须先进行抽象,继承关系就是基于抽象的结果,是经过从特殊到一般,从具体到抽象的过程,然后通过编程语言来实现

可以通过 isinstance (变量,类型)函数来判断对象之间的关系

面向对象的开发范式大致为:划分对象→抽象类→将类组织成为层次化结构(继承和合成) →用类与实例进行设计和实现

基本形式:

class 父类1():
    # 父类1定义部分

class 父类2():
    # 父类2定义部分    

class 子类(父类1, 父类2, ...):
    # 子类定义部分

如果该类没有显式指定继承自哪个类,则默认继承 object 类(object 类是 Python 中所有类的父类),object类提供了一些常用内置方法的实现,如用来在打印对象时返回字符串的内置方法__str__

Python 的继承是多继承机制,即一个子类可以同时拥有多个直接父类

作用:

继承机制经常用于创建和现有类功能类似的新类,又或是新类只需要在现有类基础上添加一些属性和方法,但又不想直接将现有类代码复制给新类。


而通过使用继承这种机制,子类可以继承父类所有的属性和方法,因此继承可以减少代码冗余,解决类与类之间的代码复用问题

 

4.2.2 原理

4.2.2.1 菱形问题

大多数面向对象语言都不支持多继承,而在Python中,一个子类是可以同时继承多个父类的,这固然可以带来一个子类可以对多个不同父类加以重用的好处,但也有可能引发著名的 Diamond problem菱形问题(或称钻石问题,有时候也被称为“死亡钻石”),菱形其实就是对下面这种继承结构的形象比喻

这种继承结构下导致的问题称之为菱形问题:如果A中有一个方法,B和/或C都重写了该方法,而D没有重写它,那么D继承的是哪个版本的方法:B的还是C的?如下所示

class A(object):
    def test(self):
        print('from A')


class B(A):
    def test(self):
        print('from B')


class C(A):
    def test(self):
        print('from C')


class D(B,C):
    pass


obj = D()
obj.test() # 结果为:from B

要想搞明白obj.test()是如何找到方法test的,需要了解python的继承实现原理

4.2.2.2 继承原理

对于创建的每一个类,Python都会计算出一个MRO(Method Resolution Order 方法解析顺序)列表,即在调用方法时,会对当前类以及所有的基类进行搜索,以确定该方法之所在,而这个搜索的顺序就是MRO,该MRO列表就是一个简单的所有基类的线性顺序列表,如下

print(D.mro())# 新式类内置了mro方法可以查看线性列表的内容,经典类没有该内置该方法
[, , , , ]

在Python2.3以后MRO的实现是基于C3线性化算法(了解),C3算法利用了拓扑排序

拓扑排序的实现步骤:循环执行以下两步,直到不存在入度为0的顶点为止

  • 选择一个入度为0的顶点(不需要经过任何点,可直接到达)并输出之

  • 从网中删除此顶点及所有出边

class A:
    def go(self):
        pass

class B:
    def go(self):
        pass

class C(A):
    def go(self):
        pass

class D(B):
    def go(self):
        pass

class E(A):
    def go(self):
        pass

class F(C,E):
    def go(self):
        pass

class G(F,D):
    def go(self):
        pass


print(G.__mro__) # 继承顺序是G->F->C->E->A->D->B->Object
# (, , , , , , , )

画图分析:

首先找到入度为0的点,这里是G,删除G和它的所有边之后,入度为0的点为F和D,因为定义G时写的是G(F,D),F在D之前,所以先处理F,删除F和它的所有边,到这里继承顺序是GF,这时入度为0的点为C,E,D,先处理C,删除C和它的所有边,到这里继承顺序是GFC,这时入度为0的点为E,D,先处理E,删除E和它的所有边,到这里继承顺序是GFCE,这时入度为0的点是A和D,先处理A,到这里继承顺序是GFCEA,这时入度为0的点是D,删除D和它的所有边,到这里继承顺序是GFCEAD,现在入度为0的点是B,删除B和它的所有边,到这里继承顺序是GFCEADB,现在入度为0的点是Object,删除Object和它的所有边。


继承图上面再没有节点了,所以继承顺序的搜索完成。


最终的继承顺序是G->F->C->E->A->D->B->Object

4.2.3 super()

子类直接通过父类名调用父类的属性和方法,这种方式在单继承时是没问题的,但是如果是多继承,则可能出现重复调用等种种问题,super()函数能解决这种问题

super不是关键字,而是一个类(描述符类), 调用super()会创建一个super对象,这个super对象充当一个访问代理的角色,帮助子类的对象调用父类或者兄弟类的方法

其实super不是针对调用父类的属性和方法而设计的,它的本质是在一个由多个类组成的有序集合中(MRO)搜寻一个特定的类,并找到这个类中的特定函数,将一个实例绑定到这个函数上,生成一个绑定方法(bound method),并返回这个绑定方法(绑定给谁,谁来调用就自动将它本身当作第一个参数传入,未绑定则需要手动传递参数)

super支持四种调用方式:

  • super(type)

  • super(type, obj)

  • super(type, type1)

  • super()

其中super(type)创建一个未绑定的super对象(unbound),其余三种方式创建的是绑定的super对象(bound),super()是python3中支持的优化写法(这种方式只能用在类体内部),相当于super(type, obj)type传入调用super的当前类的类名,obj传入调用super的方法的第一个参数,objtype的实例化对象,或者也可以是type的子类的实例化对象

class A:
    def go(self):
        print('from A')

class B(A):
    def go(self):
        super().go() # 相当于 super(B, self).go(),搜索B在self(即实例d)所对应类的MRO列表中(D->B->C->A)的下一个类C中是否有目标方法go,在C中找到了方法go,然后绑定到实例d并运行C中的方法go
        print('from B')

class C(A):
    def go(self):
        print('from C')

class D(B, C):
    def go(self):
        super().go() # 相当于 super(D, self).go(),即搜索D在self所对应类的MRO列表中的下一个类中是否有目标方法go,self这里就是D的实例对象d,且逐级传递时self会保持不变,也就是说以d所对应类D的MRO列表(D->B->C->A)为搜索顺序保持不变,D的下一个类就是B,在B中找到了方法go,然后绑定到实例d并运行B中的方法go


d = D()
print(D.__mro__) # D->B->C->A
d.go()

第一个参数是当前调用super的类,这个参数就是为了在MRO中找到下一个类,然后从这个类开始搜寻函数。


第二个参数有两个作用,一是确定从哪个类获取MRO列表,二是作为实例,绑定到要调用的函数上

推荐使用super方法而不是用父类名来调用父类,且两种方式不要混用

另外需要注意,多继承使用super时的参数传递问题,一种解决方法是使用 *args 和 **kwargs 包装的参数和关键字参数,不过,这是一种很糟糕的解决方法,由于任何类型的参数都可以传入,这会导致代码变得脆弱

class Parent(object):
    def __init__(self, name, *args, **kwargs):  # 为避免多继承报错,使用不定长参数,接受参数
        print('parent的init开始被调用')
        self.name = name
        print('parent的init结束被调用')


class Son1(Parent):
    def __init__(self, name, age, *args, **kwargs):  # 为避免多继承报错,使用不定长参数,接受参数
        print('Son1的init开始被调用')
        self.age = age
        super().__init__(name, *args, **kwargs)  # 为避免多继承报错,使用不定长参数,接受参数
        print('Son1的init结束被调用')


class Son2(Parent):
    def __init__(self, name, gender, *args, **kwargs):  # 为避免多继承报错,使用不定长参数,接受参数
        print('Son2的init开始被调用')
        self.gender = gender
        super().__init__(name, *args, **kwargs)  # 为避免多继承报错,使用不定长参数,接受参数
        print('Son2的init结束被调用')


class Grandson(Son1, Son2):
    def __init__(self, name, age, gender):
        print('Grandson的init开始被调用')
        super().__init__(name, age, gender)


print(Grandson.__mro__)

gs = Grandson('grandson', 12, '男')
print('姓名:', gs.name)
print('年龄:', gs.age)
print('性别:', gs.gender)

4.2.4 继承与重构

子类继承了其父类的所有属性和方法,同时还可以对父类的属性和方法进行重构,定义自己的属性和方法

子类如果重新定义了父类的某一方法,那么该方法就会覆盖父类的同名方法,如果希望子类在保留父类方法的基础上进行扩展,而不是完全覆盖,就需要先调用父类的方法,然后再进行功能的扩展,这时就可以通过super来实现对父类方法的调用

关于构造函数的继承与重构

  • 如果子类没有定义构造函数,当子类实例化时,会自动调用父类的__init__()方法

  • 如果子类定义了构造函数即重构了__init__()方法,当子类实例化时,不会自动调用父类的__init__()方法;如果需要调用父类的__init__()方法,则可使用super()方法

class A:
    def __init__(self, x):
        self.x = x
class B(A):
    def __init__(self, x, y):
        super().__init__(x)
        self.y = y
b = B(1, 2)
print(b.x, b.y)

4.2.5 继承与组合

组合指的是,在一个类中以另外一个类的对象作为数据属性,称为类的组合。


组合与继承都是用来解决代码的重用性问题。


不同的是:继承是一种“是”的关系,比如老师是人、学生是人,当类之间有很多相同的之处,应该使用继承;而组合则是一种“有”的关系,比如老师有生日,老师有多门课程,当类之间有显著不同,并且较小的类是较大的类所需要的组件时,应该使用组合

class Teacher:
    def __init__(self, name, age, gender, salary, level):
        self.name = name
        self.age = age
        self.gender = gender
        self.salary = salary
        self.level = level
 
    def walk(self):
        print("%s is walking" % self.name)
 
    def teach(self):
        print("%s is teaching" % self.name)
 
class Date:
    def __init__(self, year, mon, day):
        self.year = year
        self.mon = mon
        self.day = day
 
    def tell_birth(self):
        print("%s %s %s" % self.year, self.mon, self.day)
 
Ali = Teacher('Ali', 84, 'female', 30000, -1)
Birth = Date(1900, 13, 43)
 
Ali.birth = Birth  # 组合, Ali 有生日, 组合的应用,这里Birth是Date类的对象,把它作为Ali这个实例的属性,将它们组合到一起,可以理解为将Teacher类与 Birth类组合到一起了
class Course:
    def __init__(self,name,period,price):
        self.name=name
        self.period=period
        self.price=price
    def tell_info(self):
        print('<%s %s %s>' %(self.name,self.period,self.price))

class Date:
    def __init__(self,year,mon,day):
        self.year=year
        self.mon=mon
        self.day=day
    def tell_birth(self):
       print('<%s-%s-%s>' %(self.year,self.mon,self.day))

class People:
    school='清华大学'
    def __init__(self,name,sex,age):
        self.name=name
        self.sex=sex
        self.age=age

#Teacher类基于继承来重用People的代码,基于组合来重用Date类和Course类的代码
class Teacher(People): #老师是人
    def __init__(self,name,sex,age,title,year,mon,day):
        super().__init__(name,age,sex)
        self.birth=Date(year,mon,day) #老师有生日
        self.courses=[] #老师有课程,可以在实例化后,往该列表中添加Course类的对象
    def teach(self):
        print('%s is teaching' %self.name)


python=Course('python','3mons',3000.0)
linux=Course('linux','5mons',5000.0)
teacher1=Teacher('lili','female',28,'博士生导师',1990,3,23)

# teacher1有两门课程
teacher1.courses.append(python)
teacher1.courses.append(linux)

# 重用Date类的功能
teacher1.birth.tell_birth()

# 重用Course类的功能
for obj in teacher1.courses: 
    obj.tell_info()

4.2.6 Mixin 混入类

一个子类可以同时继承多个父类,这样的设计常被人诟病,一来它有可能导致菱形问题,二来在人的世界观里继承应该是“is-a”关系。


比如轿车类之所以可以继承交通工具类,是因为基于人的世界观:轿车是一个(“is-a”)交通工具,而在人的世界观里,一个物品不可能是多种不同的东西,因此多重继承在人的世界观里是说不通的,它仅仅只是代码层面的逻辑。


不过在某些情况下,一个类的确是需要继承多个类的

例如:民航飞机、直升飞机、轿车都是一个(is-a)交通工具,前两者都有一个功能是飞行fly,但是轿车没有,所以在交通工具中定义飞行功能的方法是不合理的;如果民航飞机和直升机都各自写自己的飞行fly方法,又违背了代码尽可能重用的原则(如果以后飞行工具越来越多,那会重复代码将会越来越多);而如果采用多层继承的方法,将父类交通工具逐级细分,分为会飞的交通工具和不会飞的交通工具两大子类,会飞的交通工具又包含民航飞机、直升飞机两个子类,不会飞的交通工具包含轿车一个子类,这样会导致过于复杂的层级结构,所以也是不合理的

这时候,就可以采用多继承的方法,Python提供了Mixins机制,简单来说Mixins机制指的是子类混合(mixin)不同类的功能,而这些类采用统一的命名规范(例如Mixin后缀),以此标识这些类只是用来混合功能的,并不是用来标识子类的从属"is-a"关系的,所以Mixins机制本质仍是多继承,但同样遵守”is-a”关系。


Mixin的目的就是给一个类增加多个功能,这样,在设计类的时候,优先考虑通过多继承来组合多个Mixin的功能,而不是设计多层次的复杂的继承关系

Mixin 混入类是Python程序设计中的一种技术,从概念上讲,混入不定义新类型,只是打包方法,便于重用。


混入类应该提供某方面的特定行为,只实现少量关系非常紧密的方法并且混入类绝对不能实例化

class Vehicle:  # 交通工具
    pass


class FlyableMixin:
    def fly(self):
        '''
        飞行功能相应的代码        
        '''
        print("I am flying")


class CivilAircraft(FlyableMixin, Vehicle):  # 民航飞机
    pass


class Helicopter(FlyableMixin, Vehicle):  # 直升飞机
    pass


class Car(Vehicle):  # 汽车
    pass

# ps: 采用某种规范(如命名规范)来解决具体的问题是python惯用的套路

使用Mixin类实现多重继承要非常小心

  • 首先它必须表示某一种功能,而不是某个物品,python 对于mixin类的命名方式一般以 Mixin, able, ible 为后缀

  • 其次它必须责任单一,如果有多个功能,那就写多个Mixin类,一个类可以继承多个Mixin,为了保证遵循继承的“is-a”原则,只能继承一个标识其归属含义的父类

  • 然后,它不依赖于子类的实现

  • 最后,子类即便没有继承这个Mixin类,也照样可以工作,就是缺少了某个功能。


    (比如飞机照样可以载客,就是不能飞了)

4.2.7 接口与抽象类

参考示例

4.3 多态

5. 类的特殊属性(魔术方法)

九.模块与包 1. 涵义

1.1 模块

模块,英文为 Modules,模块就是 Python 程序。


换句话说,任何 Python 程序都可以作为模块,在 Python 中,一个模块就是一个扩展名为 .py 的源程序文件,模块中除了可以定义函数,类和变量,也能包含可执行的代码(这些代码只有在模块第一次被导入时才会被执行)

前面讲了封装,并且还介绍了很多具有封装特性的结构,比如说:

  • 诸多容器,例如列表、元组、字符串、字典等,它们都是对数据的封装;

  • 函数是对 Python 代码的封装;

  • 类是对方法和属性的封装,也可以说是对函数和数据的封装。


模块可以理解为是对代码更高级的封装,即把能够实现某一特定功能的代码编写在同一个 .py 文件中,并将其作为一个独立的模块,这样既可以方便其它程序或脚本导入并使用,同时还能有效避免函数名和变量名发生冲突

每个模块都有各自独立的符号表,在模块内部被所有的函数当作全局符号表来使用,也就是说,每个模块都有自己独立的命名空间。


命名空间可以理解为若干名称的集合,该空间中包含了所有名称与之所对应的对象映射,变量是映射到对象的名称(标识符),命名空间是变量名(键)及其对应对象(值)的字典,内置的 dir() 函数可查看模块内定义的所有名称(关于命名空间与作用域,待研究)

在Python中模块一般有如下3种:

  • Python内置标准库模块

  • 第三方模块

  • 自定义模块

1.2 包

包,英文为 Package,Python中的包是一个分层文件目录结构,用来对模块进行管理和分类,把一些功能相近的模块组织在一起,或是将一个较为复杂的模块拆分为多个组成部分,将这些 .py 源程序文件放在同一个文件夹下,按照 Python 的规则进行管理,这样的文件夹和其中的文件就称为包。


目录只有包含 __init__.py 文件时才会被认作是一个包,若这个文件不存在,那么这个目录只是一个目录而不是一个包(python 3.3以后,任何目录都是package(即使没有__init__.py)。


__init__.py 文件可以是空文件,也可以存放初始化代码,事实上 __init__.py还应定义 __all__用来支持模糊导入

库,则是功能相关联的包的集合,分为标准库和第三方库,Python 安装目录下的 Lib 文件夹内存放了内置的标准库,Lib/site-packages 目录下(有的 Linux 发行版是 lib/dist-packages)则存放了用户自行安装的第三方库(模块)

2. 导入

可以通过关键字 import 来导入模块和包

2.1 方法

# import ...
import module # 导入某个模块
import module1, module2, module3 # 导入多个模块
import module as alias # 导入某个模块并取别名
import module1 as alias1, module2 as alias2, module3 as alias3
import package # 导入包,仅会运行此包目录下的 __init__.py, 如果__init__.py 中没有明确的与模块导入相关的初始化 *** 作,此包下面的模块是不会自动导入的
import package.module

# from ... import ...
from module import name # 导入模块中的某个变量,函数或者类
from module import name1, name2, name3 # 导入模块中的多个变量,函数或者类
from module import name as alias # 导入模块中的某个变量,函数或者类并取别名
from module import name1 as alias1, name2 as alias2, name3 as alias3 
from package import module # 导入包中的某个模块
from package.subpkg import module # 
from package.module import name

'''
两种方式的区别
import ... 保存它原有的命名空间,创建一个到被导入模块命名空间的引用对象;只能用来导入模块,不能导入模块中的对象(变量、函数、类)
from ... import ... 不创建一个到被导入模块命名空间的引用对象,而是把被导入模块中的一个或多个对象直接放入当前命名空间,因此会覆盖当前命名空间同名的变量名
'''

1.为源代码文件中定义的对象创建一个名字空间,通过这个名字空间可以访问到模块中定义的函数及变量。


2.在新创建的名字空间里执行源代码文件. 3.创建一个名为源代码文件的对象,该对象引用模块的名字空间,这样就可以通过这个对象访问模块中的函数及变量

2.2 过程

  • 查找sys.modules,判断该模块是否已经被加载,如果已经加载,则只是将该模块的名称加入到运行模块的命名空间

    sys.modules是一个全局字典,字典中保存着所有被导入模块的模块名到模块对象的映射(包括python内置模块),该字典是python启动后就加载在内存中。


    每当程序员导入新的模块,sys.modules将自动记录该模块。


    可以在程序中多次导入同一个模块,但模块中的代码仅仅在该模块被首次导入时执行,模块中的方法和变量在首次导入时已经加载到内存中,之后的导入不会重复加载,当再次导入该模块时,只是简单的创建一个到该模块命名空间的引用而已,python会直接到sys.modules字典中查找,从而加快了程序运行的速度

  • 如果sys.modules中未发现该模块,则按照sys.path中存放的路径顺序继续查找

    sys.path在python脚本执行时动态生成,它返回的是一个列表,该列表包含了以下几部分(优先级从上往下):

    • 程序的根目录(即当前执行的程序文件所在目录,系统自动存入,直接执行该模块.py文件时存入的是绝对路径,带 -m 参数执行或者交互式环境执行时存入的都是相对路径)

    • PYTHONPATH环境变量设置的目录

    • 标准库的目录

    • 任何能够找到的.pth文件中列出的目录

    • 第三方扩展的site-package目录

  • 如果按照上述路径仍未查找到该模块,则抛出异常;如果按照上述路径查找到该模块,则继续以下步骤

    • 创建一个新的、空的module对象(它可能包含多个module)

    • sys.modules中记录该模块

    • 在内存中加载module的代码(如果需要,需先编译)

    • 执行module中的代码

2.3 注意

  • import ... 与 from ... import ... 区别

    import ...

    保存被导入模块原有的命名空间,创建一个到被导入模块命名空间的引用对象(名称为被导入模块的模块名,如果有 as 子句则使用其指定的别名);只能用来导入模块,不能导入模块中的对象(变量、函数、类);必须通过被导入模块的模块名即 module.name 的方式调用被导入模块中的对象(变量、函数、类)

    from ... import ...

    不创建一个到被导入模块命名空间的引用对象,而是把被导入模块中的一个或多个对象直接放入当前命名空间(名称为被导入模块中的对象名,如果有 as 子句则使用其指定的别名),因此会覆盖当前命名空间同名的变量名;调用时可省略被导入模块的模块名

    from ... import *(不推荐)

    把被导入模块中的全部公有名称直接放入当前命名空间,模块的公有名称由模块命名空间中的 __all__ 变量确定(包则由 __init__.py 文件的__all__ 的变量确定), __all__ 是一个字符串列表 。


    如果 __all__ 没有被定义,则公有名称是模块命名空间中所有不以 '_' 开头的名称

  • 绝对导入与相对导入

    绝对导入的格式为 import A.Bfrom A import B,相对导入格式为 from . import Bfrom ..A import B.代表当前模块,..代表上层模块,...代表上上层模块,依此类推

    存在相对导入语句的模块,不能直接运行,否则会有异常,因为在没有明确指明包结构的情况下,Python 是根据 __name__属性来决定一个模块在包中的结构的,如果一个模块被直接运行,则它自己为顶层模块(__name__的值是__main__),不存在层次结构,所以无法确定相对路径,因此一个Python应用程序的主模块(程序运行入口模块),应当总是使用绝对路径引用

  • sys.path

    动态修改

    sys.path.append('路径')
    sys.path.insert(0,'路径')

    # 把项目所在的地址添加到 sys.path 列表里面,便于快速导入模块
    import sys
    from os.path import abspath, dirname
    current_path = dirname(dirname(abspath(__file__)))
    sys.path.insert(0, current_path)

  • __init__.py

    __all__

  • if __name__=='__main__'

    模块也是对象,并且所有的模块都有一个内置属性__name__,直接运行该模块时,__name__的值是__main__,如果该模块是被导入的,__name__的值是模块名。


    若希望引入模块时其中的某些代码块不执行,可以在模块中添加判断语句 if __name__=='__main__',使这些代码块仅在该模块自身运行时执行,因此 if __name__=='__main__'常用于代码调试

if __name__=='__main__':
	pass

十.正则表达式

十一.常用模块与 *** 作 1. time
import time
time.time():输出自1970以来经历的秒数(浮点数)
time.strftime('%Y/%m/%d %H:%M:%S'):输出格式化以后的日期时间字符串
2. random
import random
random.random() #产生[0,1)之间的随机数
random.randint(0,9) #产生[0,9]之间的随机整数
random.choice(可迭代对象) #从可迭代对象中随机选择一个
# 输出100个随机手机号码
s=[]
while len(s)<100:
    i=random.randint(13800000100,13800000201)
    if i not in s:
      s.append(i)
print(s)
3. 读写 txt 文件

3.1 读

file=open(文件名, 'r', encoding='utf-8') # r 表示以字符形式读文件
for row in file: # row表示1行数据,实际上是以一个\n为单位进行循环遍历
	print(row, end='')

3.2 写

# 覆盖
file=open(文件名, 'w') # w 表示write
file.write(一个字符串) # 如果想回车,加\n
file.writelines([字符串1, 字符串2, ...]) # 写多个字符串

# 追加
file=open(文件名, 'a') # a表示append
file.write(一个字符串) # 如果想回车,加\n
file.writelines([字符串1, 字符串2, ...]) # 写多个字符串

3.3 关闭文件

file=open(文件名, 'r', encoding='utf-8')
file.close()

3.4 上下文管理协议

可以自动关闭文件

with open(文件名,'r/w/a') as file:
	读写文件的代码
'''
cases.txt 

测试登录成功的情况,admin,123456,登录成功
测试登录密码错误的情况,admin,123,密码错误
'''



with open('cases.txt', 'r', encoding='utf-8') as file:
    for row in file:
        # print(row,end='')
        row=row.strip() #strip去掉首尾空白符
        # 方法1
        # case=row.split(',')
        # print(*case) # 如果想把数据作为参数传给函数,这样用
        # 方法2
        case_name,uname,upass,expect=row.split(',')
        print(case_name)
        print(uname, upass)
4. os
import os
os.getcwd() # 获得当前所在目录
os.listdir(目录名) # 获得指定目录中所有的内容(目录和文件),不递归,目录名可省略
os.mkdir(目录名) # 创建目录,目录存在时报错
os.chdir(目录名) # 切换目录
os.rmdir(目录名) # 只能删除空目录
open(文件名, 'w') # 创建文件,文件如果已经存在,则打开,写文件
open(文件名, 'r') # 读
os.rename(目录或文件名, 新目录名或新文件名) # 重命名
os.remove(文件名) # 删除文件
import shutil
shutil.rmtree(目录名) #删除任意目录
5.数据库 *** 作

安装第三方模块 pymysql

import pymysql
# 1.连接数据库
conn = pymysql.connect(host=主机或地址, user=用户名, password=密码, db=数据库名)

# 2.创建游标
# 游标是用于一行一行处理数据的工具,可以理解为内存中的一张表,游标可以指向一行一行的数据
cursor = conn.cursor()

# 3.指定sql语句
sql="insert into users(uname,upass) values('admin','123')"
sql="update users set upass='123456' where uname='admin'"
sql="delete from users where uname='admin'"
sql="select * from users where uname='admin'"

# 4.执行sql语句
cursor.execute(sql)

# 5.获得数据/保存数据
# 查询时,获得数据
data = cursor.fetchone() # 获得第一行数据
data = cursor.fetchall() # 获得所有数据
# 增删改时,保存数据
conn.commit()

# 6.关闭数据库连接
conn.close()
#查询数据
import pymysql #1、导入模块
dbinfo={'host':'172.166.8.181','user':'root','password':'123456','db':'test'}
conn=pymysql.connect(**dbinfo) #2、连接数据库,**会把参数变成关键字参数形式
cursor=conn.cursor() #3、创建游标
sql="select * from users" #4、指定sql语句,可能会有多行
# res=cursor.execute(sql) #5、执行sql语句,但返回的是受影响行数
cursor.execute(sql)
# rs=cursor.fetchall() #6、获得并输出结果,fetchall表示取所有行,元组类型
# print(rs)
# for row in rs:
#     print(*row)
rs=cursor.fetchone() #读取第一行数据,结果是元组类型
print(rs)
print(*rs)
conn.close() #7、关闭数据库
'''
应用:初始化数据库
连接数据库,读取initsqls文件,把sql语句读出来,然后执行

initsqls文件内容如下:

--initsqls.txt(主要放增删改,修改update相对较少)
--删除admin,重新添加
delete from users where uname='admin'
insert into users(uname,password,name) values('admin','123456','管理员')

--用于重复注册的数据
delete from users where uname='zhsan'
insert into users(uname,password,name,gender) values('zhsan','123456','张三','男')

--用于成功注册的数据
delete from users where uname='lisi'
'''

# 方法1:线性编程
import pymysql
conn=pymysql.connect(host='172.166.100.59',user='root',password='123456',db='test')
cursor=conn.cursor() #游标只需要创建一次
#指定sql语句:从文件读
file=open('initsqls.txt','r',encoding='utf-8')
for row in file:
    if len(row.strip())>0 and not row.startswith('--'):
        # print(row, end='')
        cursor.execute(row)
file.close()
conn.commit()
conn.close()

# 方法2:用函数封装代码
'''
配置文件 db.conf 内容如下:

[formal] # 节点名1,表示第一个服务器,正式测试使用这个服务器
host=172.166.100.62
user=root
password=123456
db=test
[regress] # 节点名2,回归测试使用这个服务器
host=172.166.100.66
user=root
password=123456
db=reg_test
'''
import pymysql, configparser # configparser模块是配置文件读取工具
def read_db_conf(which_db):
    conf=configparser.ConfigParser()
    conf.read('db.conf', encoding='utf-8')
    host=conf.get(which_db, 'host')
    user=conf.get(which_db, 'user')
    password=conf.get(which_db, 'password')
    db=conf.get(which_db, 'db')
    dbinfo={'host':host,'user':user,'password':password,'db':db} #引号中的单词固定写法,不能写错,冒号右边是上面定义的变量
    return dbinfo
def conn_db(dbinfo): #连接数据库函数
    conn=pymysql.connect(**dbinfo)
    return conn
def read_sqls(sql_file_name): #读sql文件的函数
    sqls=[] #存最终的sql语句列表
    file=open(sql_file_name, 'r', encoding='utf-8')
    for row in file:
        if len(row.strip())>0 and not row.startswith('--'):
            sqls.append(row.strip())
    file.close()
    return sqls
def init_db(): #初始化数据库的函数
    dbinfo=read_db_conf('formal')
    conn=conn_db(dbinfo)
    cursor=conn.cursor()
    sqls=read_sqls('initsqls.txt')
    # print(sqls)
    for sql in sqls:
        cursor.execute(sql)
    conn.commit()
    conn.close()
init_db()

# 方法3:使用类封装代码
import configparser,pymysql
def read_sqls(sql_file_name):
    sqls=[]
    with open(sql_file_name,'r',encoding='utf-8') as file:
        for row in file:
            if len(row.strip())>0 and not row.startswith('--'):
                sqls.append(row.strip())
    return sqls
class DB:
    def read_db_conf(self,which_db):
        conf=configparser.ConfigParser()
        conf.read('db.conf',encoding='utf-8')
        host=conf.get(which_db,'host')
        user=conf.get(which_db,'user')
        passwd=conf.get(which_db,'password')
        db=conf.get(which_db,'db')
        self.dbinfo={'host':host, 'user':user, 'password':passwd, 'db':db}
    def __init__(self): # 构造方法连接数据库
        self.read_db_conf('formal')
        self.conn=pymysql.connect(**self.dbinfo)
    def init_db(self):
        with self.conn as conn: # 起别名
            cursor=conn.cursor()
            sqls=read_sqls('initsqls.txt')
            for sql in sqls:
                cursor.execute(sql)
            conn.commit()
db=DB()
db.init_db()
'''
mysql安装完成以后,禁止其他计算机访问,连接报错,可尝试一下命令解决

--查看mysql数据库的user表中的user用户可以在哪些计算机host上连接
select host,user from mysql.user;
--把mysql数据库(mysql数据库软件中的mysql数据库)中的user表中的root行host列改为%(任意计算机)
update  mysql.user  set  host=’%’  where  user=’root’;
--刷新权限,提交保存
flush  privileges;
'''
6. 读写 excel 文件

安装第三方模块 pandas openpyxl

import pandas,openpyxl
# 读取excel文件
data = pandas.read_excel('cases.xlsx',sheet_name=1,usecols=['列名1','列名2'],dtype={'列名1':str,'列名2':int}, keep_default_na=False) # data的类型是dataframe字典;sheet_name=1表示读取第2张sheet表,usecols表示要读取的列,dtype可指定各列数据格式,keep_default_na=False表示空数据不显示为nan,而是直接显示为空
data=data.values.tolist() # 将data转为列表

# 在excel中插入行
xlsfile=openpyxl.load_workbook('cases.xlsx') # 打开文件,载入工作簿
xlsfile.create_sheet('Sheet3') # 创建工作表Sheet3
sheet=xlsfile['Sheet3'] # 选择工作表
sheet.append([7,8,9]) # 追加行数据


# 修改excel中单元格的值,插入一列数据
xlsfile=openpyxl.load_workbook('cases.xlsx') # 打开文件,载入工作簿
sheet=xlsfile['Sheet1'] # 选择工作表
sheet.cell(3,5,'xxx') # 修改单元格数据,第3行第5列

# 插入一列
column=['colname', 'xxx', 'xxx', 'xxx'] # 数据个数注意与行数匹配
for i in range(len(column)): # i=0~3
    sheet.cell(i+1, 6, column[i]) # 通过循环遍历,修改第i+1行第6列的值
    
# 删除行
sheet.delete_rows(i) # 删除第i行
sheet.delete_rows(i,j) # 从第i行(含)开始删除共计j行
sheet.delete_cols(i) # 删除第i列

# 保存文件
xlsfile.save('cases.xlsx') # 保存文件

欢迎分享,转载请注明来源:内存溢出

原文地址:https://54852.com/langs/571567.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2022-04-09
下一篇2022-04-09

发表评论

登录后才能评论

评论列表(0条)

    保存