Python: 面向对象

Python 面向过程编程

主要内容:

  • 函数基本概念、形参、实参
  • 函数的位置参数、关键字参数、参数默认值
  • 函数的可变位置参数、可变关键字参数、Keyword-only 参数
  • 函数的位置传参、关键字传参、参数结构
  • 函数的 Return 语句及返回值
  • 嵌套函数、函数作用域、global、闭包、nonlocal
  • 参数缺省值的常见问题解决办法和__defaults__
  • LEGB原理
  • 函数执行流程和函数调用原理
  • 递归函数概念和递归性能分析
  • 匿名函数 Lambda 表达式概念及应用
  • 生成器对象和生成器函数及 yield from
  • 高阶函数概念和内建高阶函数应用
  • 函数柯里化、无参装饰器、带参装饰器
  • 装饰器的副作用及解决方法
  • Python 的参数注解和 Inspect 模块应用
  • Python 内置全局函数使用
  • Functools 模块重要函数解析

Python函数

函数

数学定义

  • y=f(x) ,y是x的函数,x是自变量。y=f(x0, x1, …, xn)

Python函数

  • 由若干语句组成的语句块、函数名称、参数列表构成,它是组织代码的最小单元
  • 完成一定的功能

函数的作用

  • 结构化编程对代码的最基本的 封装 ,一般按照功能组织一段代码
  • 封装的目的为了 复用 ,减少冗余代码
  • 代码更加简洁美观、可读易懂

函数的分类

  • 内建函数,如max()、reversed()等
  • 库函数,如math.ceil()等
  • 自定义函数,使用def关键字定义

函数定义

def 函数名(参数列表):
    函数体(代码块)
    [return 返回值]
  • 函数名就是标识符,命名要求一样
  • 语句块必须缩进,约定4个空格
  • Python的函数若没有return语句,会隐式返回一个None值
  • 定义中的参数列表称为 形式参数 ,只是一种符号表达(标识符),简称 形参

函数调用

  • 函数定义,只是声明了一个函数,它不能被执行,需要调用执行
  • 调用的方式,就是 函数名后加上小括号 ,如有必要在括号内填写上参数
  • 调用时写的参数是 实际参数 ,是实实在在传入的值,简称 实参
def add(x, y): # 函数定义,解析器扫过这段代码后,会立即生成一个函数对象,在内存堆中,用标识符add指代
    result = x + y # 函数体
    return result # 返回值

out = add(4,5) # 函数调用,可能有返回值,使用变量接收这个返回值
print(out) # print函数加上括号也是调用

print(callable(add), callabel(a))
# True, False

上面代码解释:

  • 定义一个函数add,及函数名是add,能接受2个形式参数
  • 该函数计算的结果,通过返回值返回,需要return语句
  • 调用时,通过函数名add后加2个实际参数,返回值可使用变量接收
  • 函数名也是标识符
  • 返回值也是值
  • 定义需要在调用前,也就是说调用时,已经被定义过了,否则抛NameError异常
  • 函数是 可调用的对象 ,callable(add)返回True

看看这个函数是不是通用的?体会一下Python函数的好处

  • 所有灵活性,是有代价的,双刃剑
  • python 简单,所以类型错误问题。形参和返回值都不要写明类型
  • python很多问题,都会在运行中显现出来,为时已晚

函数参数

函数在定义时要定义好形式参数,调用时也提供足够的实际参数,一般来说,形参和实参个数要一致(可变参数除外)。

实参传参方式

1、位置传参

定义时def f(x, y, z), 调用使用 f(1, 3, 5),按照参数定义顺序传入实参

2、关键字传参

定义时def f(x, y, z),调用使用 f(x=1, y=3, z=5),使用形参的名字来传入实参的方式,如果使用了形参名字,那么 传参顺序就可和定义顺序不同

要求位置参数必须在关键字参数之前 传入,位置参数是按位置对应的

范例

# coding: utf-8
# 传参,调用时,传入实际参数
#传参 就2种方式:
#1. 位置传参,和形参依次对应
#2. 关键字传参,按照参数名称对应,与顺序无关
#这两种可以混合使用
#位置传参,不能跟在 关键字传参之后

def add(x, y):
   print(x,y)
   return x + y

#调用时,传参,传入实参的简称
result = add(4, 5) # 位置传参
print(result)
#4 5
#9

add(x=4, y=5) #关键字传参keyword,按参数名称对应,与顺序无关

add(4, y=5) # 正确
#add(x=[4], y=(5,))
#add(y=5, 4) # 错误传参。位置传参,不能跟在关键字之后

print(add) #把这个标识符对应的对象的字符串表达形式给人看
print(id(add), hex(id(add)))
#: <function add at 0x000002AE0CFA6280>
#: 2946565300864 0x2ae0cfa6280

d = dict({'a':1}, b=2, a=3) # dict 中参数按位置传参、按关键字传参
print(d)
#: {'a': 3, 'b': 2}

切记:传参指的是调用时传入实参,就2种方式。

下面讲的都是形参定义。

形参缺省值

缺省值也称为默认值,可以在函数定义时,为形参增加一个缺省值。其作用:

  • 参数的默认值可以在未传入足够的实参的时候,对没有给定的参数赋值为默认值
  • 参数非常多的时候,并不需要用户每次都输入所有的参数,简化函数调用
def add(x=4, y=5):
    return x+y

测试调用 add()、add(x=5)、add(y=7)、add(6, 10)、add(6, y=7)、add(x=5, y=6)、add(y=5, x=6)、
#add(x=5, 6)、add(11, x=20) 不能多重赋值、add(y=8, 4) 传参时缺省值在后面

能否这样定义 def add(x, y=5) 或 def add(x=4,y) ? def add(x=4, y) 报错,缺省值在后面

范例:登录函数

# 定义一个函数login,参数名称为host、port、username、password
def login(host='localhost', port=3306, username='root', password='root'):
    print('mysql://{2}:{3}@{0}:{1}/'.format(host, port, username, password))

login()
login('127.0.0.1')
login('127.0.0.1', 3361, 'wayne', 'wayne')
login('127.0.0.1', username='wayne')
login(username='wayne', password='wayne', host='www.xxx.com')
可变参数

需求:写一个函数,可以对多个数累加求和

def sum(iterable):
    s = 0
    for x in iterable:
        s += x
    return s

print(sum([1,3,5]))
print(sum(range(4)))

上例,传入可迭代对象,并累加每一个元素。

也可以使用可变参数完成上面的函数。

# coding: utf-8
def fn1(*nums): # * 可变形参,可以收多个实参,多个实参被收集到一个元组对象中,元组不可变
    print(nums, type(nums))
    s = 0
    for x in nums:
        s += x
    return s

fn1(1,2,3) # 按照位置传参
#: (1, 2, 3) <class 'tuple'>
fn1()
#: () <class 'tuple'>

1、可变位置参数

  • 在形参前使用 * 表示该形参是可变位置参数,可以接受多个实参
  • 它将收集来的实参组织到一个tuple中

2、可变关键字参数

  • 在形参前使用 ** 表示该形参是可变关键字参数,可以接受多个关键字参数
  • 它将收集来的实参的名称和值,组织到一个dict中
# coding: utf-8
def showconfig(**kwargs): # *args **kwargs
    print(type(kwargs), kwargs) # 可变关键字参数收集关键字传参,收集成字典 kv,字典可变
    # 内部,你能传入变量名我们有要求,对kwargs处理,'username' in kwargs.keys()\pop(key)
    for k,v in kwargs.items():
        print('{}={}'.format(k,v), end=', ')

showconfig()
#: <class 'dict'> {}

showconfig(host='127.0.0.1', port=8080, username='wayne', password='xxx')

混合使用

可以定义为下列方式吗?
def showconfig(username, password, *args) # 只接受可变位置参数
def showconfig(username, password, **kwargs) # 只接受可变关键字参数
def showconfig(username, *args, **kwargs) #位置传参被args占用,关键字传参被kwargs占用
def showconfig(username, password, **kwargs, *args) # ? 报错,*args在前面
def showconfig(*args, **kwargs) # 接收所有参数

总结:

  • 有可变位置参数和可变关键字参数
  • 可变位置参数在形参前使用一个星号*
  • 可变关键字参数在形参前使用两个星号**
  • 可变位置参数和可变关键字参数都可以收集若干个实参,可变位置参数收集形成一个tuple,可变
  • 关键字参数收集形成一个dict
  • 混合使用参数的时候,普通参数需要放到参数列表前面,可变参数要放到参数列表的后面,可变位置参数需要在可变关键字参数之前

范例:

# coding: utf-8
def fn(x, y, *args, **kwargs):
    #print(x, y, args, kwargs, sep='\n', end='\n\n')
    print(x, y, args, kwargs)

# fn() fn(range(5)) 不能,range(5) 表示一个对象
fn(1,2), fn(1,2,3), fn(1,2,3,4), fn(1, 2, a=100)
#: 1 2 () {}
#: 1 2 (3,) {}
#: 1 2 (3, 4) {}
#: 1 2 () {'a': 100}

fn(3, 5, 7, 9, 10, a=1, b='abc')
fn(y=5, a=5, x=4)
fn(3, 5, a=1, b='abc')

#fn(x=3, y=8, 7, 9, a=1, b='abc') # ? 报错, 位置传参写在关键字之前
#fn(7, 9, y=5, x=3, a=1, b='abc') # ? 摄报错,重复传参

fn(x=3, y=8, 7, 9, a=1, b='abc'),错在位置传参必须在关键字传参之前

fn(7, 9, y=5, x=3, a=1, b='abc'),错在7和9已经按照位置传参了,x=3、y=5有重复传参了

keyword-only参数

先看一段代码

# keyword-only 仅仅能关键字传参的形参
# 在 * 号后的参数
def fn(*args, x, y, **kwargs): 
    print(x, y, args, kwargs, sep='\n', end='\n\n')

fn(3, 5) # 报错
fn(3, 5, 7) # 报错
fn(3, 5, a=1, b='abc') # 报错
fn(3, 5, y=6, x=7, a=1, b='abc')

在Python3之后,新增了keyword-only参数。

keyword-only参数:在形参定义时,在一个*星号之后,或一个可变位置参数之后,出现的普通参数,就已经不是普通的参数了,称为keyword-only参数。

def fn(*args, x):
    print(x, args, sep='\n', end='\n\n')

fn(3, 5) # 报错, 3 5 都传给args, x y 没有传参
fn(3, 5, 7) # 报错
fn(3, 5, x=7)

keyword-only参数,言下之意就是这个参数必须采用关键字传参。

可以认为,上例中,args可变位置参数已经截获了所有位置参数,其后的变量x不可能通过位置传参传入了。

思考:def fn(**kwargs, x) 可以吗? 不可以,直接语法错误

def fn(**kwargs, x):
    print(x, kwargs, sep='\n', end='\n\n')

可以认为,kwargs会截获所有关键字传参,就算写了x=5,x也没有机会得到这个值,所以这种语法不存在。

keyword-only参数另一种形式

'*' 星号后所有的普通参数都成了keyword-only参数。

def fn(*, x, y):
    print(x, y)
fn(x=6, y=7)
fn(y=8, x=9)

def fn2(a, *, x, y):
    print(a, x, y)
fn2(1, y=2, x=3)
#: 6 7
#: 9 8
#: 1 3 2
Positional-only参数

Python 3.8 开始,增加了最后一种形参类型的定义:Positional-only参数。(2019年10月发布3.8.0)

/ 之前接收位置传参。

# coding: utf-8
def fn(a, /):
    print(a, sep='\n')

fn(3)
#fn(a=4) # 错误,仅位置参数,不可以使用关键字传参

def fn(a, /, b):
    print(a, b)
fn(1, 2)
fn(1, b=2)
#fn(a=1, b=2) #报错

def fn(a, b=2, /):
   print(a, b)

fn()
#fn(1, b=3) #报错

范例:

# coding: utf-8

def fn(a, b, /, x, y, z=6, *args, m=4, n, **kwargs): # *号后面,只接收关键字传参。n 
    print(a, b)
    print(x, y, z)
    print(args)
    print(m, n)
    print(kwargs)

fn(1, 2, 3, 4, 5, 6, n=10)
#: 1 2
#: 3 4 5
#: (6,)
#: 4 10
#: {}

fn(1, 2, n=20, x=5, y=6, t=10
#1 2
#5 6 6
#()
#4 20
#{'t': 10}
参数的混合使用
# 可变位置参数、keyword-only参数、缺省值
def fn(*args, x=5):
    print(x)
    print(args)

fn() # 等价于fn(x=5)
fn(5) # 5给了args
fn(x=6)
fn(1,2,3,x=10)
# 普通参数、可变位置参数、keyword-only参数、缺省值
def fn(y, *args, x=5):
    print('x={}, y={}'.format(x, y))
    print(args)

fn() #报错,y 必须给
fn(5)
fn(5, 6) #6给了args
fn(x=6) #报错,y 没给
fn(1, 2, 3, x=10) #y=1, (2,3)给了args, x=10
fn(y=17, 2, 3, x=10) #语法错误,关键字在后面
fn(1, 2, y=3, x=10) #错误,重复传参
fn(y=20, x=30)
# 普通参数、缺省值、可变关键字参数
def fn(x=5, **kwargs):
    print('x={}'.format(x))
    print(kwargs)

fn()
fn(5)
fn(x=6)
fn(y=3, x=10)
fn(3, y=10)
fn(y=3, z=20)
参数规则

参数列表参数一般顺序是:positional-only参数、普通参数、缺省参数、可变位置参数、keyword-only参数(可带缺省值)、可变关键字参数。

注意:

  • 代码应该易读易懂,而不是为难别人
  • 请按照书写习惯定义函数参数
# a和b是仅位置参数,只能用在python3.8后
def fn(a, b, /, x, y, z=3, *args, m=4, n, **kwargs):
    print(a, b)
    print(x, y, z)
    print(m, n)
    print(args)
    print(kwargs)
    print('-' * 30)

def connect(host='localhost', user='admin', password='admin', port='3306',**kwargs):
    print('mysql://{}:{}@{}:{}/{}'.format(
        user, password, host, port, kwargs.get('db', 'test')
    ))

connect('127.0.0.1', 'admin', 'abcdef', debug=True, db='blog')
connect(db='cmdb') # 参数的缺省值把最常用的缺省值都写好了
connect(host='192.168.1.123', db='cmdb')
connect(host='192.168.1.123', db='cmdb', password='mysql')
  • 定义最常用参数为普通参数,可不提供缺省值,必须由用户提供。注意这些参数的顺序,最常用的先定义
  • 将必须使用名称的才能使用的参数,定义为keyword-only参数,要求必须使用关键字传参
  • 如果函数有很多参数,无法逐一定义,可使用可变参数。如果需要知道这些参数的意义,则使用可变关键字参数收集

参数解构

def add(x, y):
    print(x, y)
    return x + y

add(4, 5)
#add((4, 5)) # 可以吗? 不可以,元组算一个参数
t = 4, 5
add(t[0], t[1])

add(*t) # 参数解析,调用时,对实参前加入 * 号
add(*(4, 5)), add(*'ab'), add(*b'xy'), add(*[4, 5])
add(*{4, 5}) # 注意有顺序吗? 无序
add(*range(4, 6))

add(*{'a':10, 'b':11}) # 可以吗? 可以,相当key在解构 >3.6后记住了key的顺序,'ab'
add(**{'a':10, 'b':11}) # 可以吗? 不可以,** 是关键字解构。

#**{'x':1, 'y':2} #参数解构只能用在实参的传递的时候
add(**{'x':100, 'y':110}) # 可以吗?

参数解构:

  • 在给函数提供实参的时候,可以在可迭代对象前使用 * 或者 ** 来进行结构的解构,提取出其中所有元素作为函数的实参
  • 使用 * 解构成位置传参
  • 使用 ** 解构成关键字传参
  • 提取出来的元素数目要和参数的要求匹配
def add(*nums):
    result = 0
    for x in nums:
        result += x
    return result

add(1, 2, 3)
add(*[1, 3, 5])
add(*range(5))
add(*range(5), 100, *[20]) #add(0,1,2,3,4,100,20) # 展开后传参
# 3.8以后,下面就不可以使用字典解构后的关键字传参了
def add(x, y, /): # 仅位置形参
    print(x, y)
    return x + y

add(**{'x':10, 'y':11}) #报错。 仅位置传参

函数返回值

先看几个例子

# return语句之后可以执行吗?
def showplus(x):
    print(x)
    return x + 1
    print('~~end~~') # return之后会执行吗? 执行不到

showplus(5)

# 多条return语句都会执行吗
def showplus(x):
    print(x)
    return x + 1
    return x + 2 # 执行不到

showplus(5)

# 下例多个return可以执行吗?
def guess(x):
    if x > 3:
        return "> 3"
    else:
        return "<= 3"

print(guess(10))

# 下面函数执行的结果是什么
def fn(x):
    for i in range(x):
        if i > 3:
            return i
        else:
            print("{} is not greater than 3".format(x))

print(fn(5)) # 打印什么? 4
print(fn(3)) # 打印什么? 返回None

总结

  • Python函数使用return语句返回“返回值”
  • 所有函数都有返回值,如果没有return语句,隐式调用return None
  • return 语句并不一定是函数的语句块的最后一条语句
  • 一个函数可以存在多个return语句,但是只有一条可以被执行。如果没有一条return语句被执行到,隐式调用return None
  • 如果有必要,可以显示调用return None,可以简写为return
  • 如果函数执行了return语句,函数就会返回,当前被执行的return语句之后的其它语句就不会被执行了
  • 返回值的作用:结束函数调用、返回“返回值”

能够一次返回多个值吗?

def showvalues():
    return 1, 3, 5

showvalues() # 返回了多个值吗? 一个值
  • 函数不能同时返回多个值
  • return 1, 3, 5 看似返回多个值,隐式的被python封装成了一个元组
  • x, y, z = showlist() 使用解构提取返回值更为方便

练习

  1. 编写一个函数,能够接受至少2个参数,返回最小值和最大值
  2. 完成一个函数,可以接收输入的多个数(命令输入,数字间隔可以使用空格或逗号),每一次都能返回到目前为止的最大值、最小值。
  1. 编写一个函数,能够接受至少2个参数,返回最小值和最大值
# coding: utf-8

def fn(*args):
    if len(args)<2:  # 不优雅
        raise TypeError
    #pass

def get1(x, y, *args): #max, min
    return min(x, y, *args), max(x, y, *args)
get1(4, 5, *range(5))
#有什么问题?
#1 遍历了2次,解决方案1次遍历
#2 解构的问题

def get2(x, y, *args):
    ms, *_, mx = sorted((x,y,*args))
    return ms, mx

get2(4, 5, *range(5))

# get1 时间复杂度为 O(2n),get2 时间复杂度0(n^2)
# 测试使用无序的参数,sorted 内部做了优化

lst= list(range(10000))
import random
random.shuffle(lst)
random.shuffle(lst)
%%timeit
get1(4,5,*lst) #6xxms

%%timeit
get2(4,5,*lst) #1s

def get3(x, y, *args):
    ms, mx = min(args), max(args)
    return min(x,y,ms), max(x,y,mx)

%%timeit
get3(4,5,*lst) #4xxms

# 一次性遍历,不依赖内建函数
def get4(x, y, *args):
    min_, max_ = (y, x) if x > y else (x, y)
    for x in args:
        if x > max:
            max_ = x
        elif x < min_:
            min_ = x
    return min_, max_

%%timeit
get3(4,5,*lst) #3xxms
  1. 完成一个函数,可以接收输入的多个数(命令输入,数字间隔可以使用空格或逗号),每一次都能返回到目前为止的最大值、最小值。
# coding: utf-8
#input('>>>').replace(',', ' ').split() #列表

def double_values():
    mx = ms = None
    while True:
        x = input('>>>').strip()
        if x == '' or x == 'quit':
            break

        nums = [int(t) for t in x.replace(',', ' ').split()] #列表,有可能是空
        if nums: # 说明有成功转换的数据在列表中
            if mx is None: #不但是第一次,而且第一次有数据才进来
                mx = ms = nums[0]

            a, b = min(nums), max(nums)
            if b > mx:
                mx = b
            if a < ms:
                ms = a

            print(ms, mx)

返回值作用域

函数作用域***

作用域

一个标识符的可见范围,这就是标识符的作用域。一般常说的是变量的作用域

def foo():
    x = 100
print(x) # 可以访问到吗

上例中x不可以访问到,会抛出异常(NameError: name 'x' is not defined),原因在于函数是一个封装,它会开辟一个 作用域 ,x变量被限制在这个作用域中,所以在函数外部x变量 不可见

注意: 每一个函数都会开辟一个作用域

作用域分类
  • 全局作用域
    • 在整个程序运行环境中都可见
    • 全局作用域中的变量称为 全局变量 global
  • 局部作用域
    • 在函数、类等内部可见
    • 局部作用域中的变量称为 局部变量 ,其使用范围不能超过其所在局部作用域
    • 也称为本地作用域local
# 局部变量
def fn1():
    x = 1 # 局部作用域,x为局部变量,使用范围在fn1内

def fn2():
    print(x) # x能打印吗?可见吗?为什么? 
print(x) # x能打印吗?可见吗?为什么? 不可见,x 的可见范围就在fn1函数里。全局环境中没有定义x
# coding: utf-8
# 全局变量
x = 5 # 全局变量,也在函数外定义
def foo():
    y = x + 1
    print(x) # 可见吗?为什么? 可见。函数局部变量,对外不可见;外部对内可见,向内穿透
    print(y)

foo()
#: 5
#: 6
  • 一般来讲外部作用域变量可以在函数内部可见,可以使用
  • 反过来,函数内部的局部变量,不能在函数外部看到
函数嵌套

在一个函数中定义了另外一个函数

def outer():
    def inner():
        print("inner")
    inner()
    print("outer")
outer() # 可以吗? 可以
inner() # 可以吗? 内部函数不能在外部直接使用,因为它在函数外部不可见

内部函数inner不能在外部直接使用,会抛NameError异常,因为它在函数外部不可见。

其实,inner不过就是一个标识符,就是一个函数outer内部定义的变量而已。

嵌套结构的作用域

对比下面嵌套结构,代码执行的效果

# coding: utf-8
def outer1():
    o = 65
    def inner():
        print('inner', o, chr(o))
    inner()
    print('outer', o, chr(o))

outer1() # 执行后,打印什么
#: inner 65 A
#: outer 65 A

def outer2():
    o = 65
    def inner():
        o = 97
        print('inner', o, chr(o))
    inner()
    print('outer', o, chr(o))

outer2() # 执行后,打印什么
#: inner 97 a
#: outer 65 A

从执行的结果来看:

  • 外层变量在内部作用域可见
  • 内层作用域inner中,如果定义了 o = 97 ,相当于在当前函数inner作用域中重新定义了一个新的变量o,但是,这个o并不能覆盖掉外部作用域outer2中的变量o。只不过对于inner函数来说,其只能可见自己作用域中定义的变量o了
内建函数chr, ord
内建函数 函数签名 说明
chr chr(i) 通过unicode编码返回对应字符
ord ord(c) 获得字符对应的unicode
print(ord('中'), hex(ord('中')), '中'.encode(), '中'.encode('gbk'))

chr(20013) # '中'
chr(97)
一个赋值语句的问题

再看下面左右2个函数

左边函数: 正常执行,函数外部的变量在函数内部可见

右边函数: 执行错误吗,为什么?难道函数内部又不可见了?y = x + 1可以正确执行,可是为什么x += 1却不能正确执行?

仔细观察函数2返回的错误指向x += 1,原因是什么呢?

x = 5
def foo():
   x += 1

foo() # 报错如下, UnboundLocalError

原因分析:

  • x += 1 其实是 x = x + 1
  • 只要有"x="出现,这就是赋值语句。相当于在foo内部定义一个局部变量x,那么foo内部所有x都是这个局部变量x了
  • x = x + 1 相当于使用了局部变量x,但是这个x还没有完成赋值,就被右边拿来做加1操作了
x = 5
def foo(): # 函数被解释器解释,foo指向函数对象,同时解释器会理解x是什么作用域
    print(x) # x 在函数解析时就被解释器判定为局部变量
    x += 1 # x = x + 1
foo() # 调用时

如何解决这个常见问题?

global语句
# coding: utf-8
x = 5
def foo():
    global x # 全局变量。 将x声明为全局变量
    x += 1
    print(x)

foo()
#: 6
  • 使用global关键字的变量,将foo内的x声明为使用外部的全局作用域中定义的x
  • 全局作用域中必须有x的定义

如果全局作用域中没有x定义会怎样?

注意,下面试验如果在ipython、jupyter中做,上下文运行环境中有可能有x的定义,稍微不注意,就测试不出效果

# 有错吗?
def foo():
    global x
    x += 1
    print(x)

foo()
# 有错吗?
def foo():
    global x
    x = 10
    x += 1
    print(x)

foo()
print(x) # 可以吗

使用global关键字定义的变量,虽然在foo函数中声明,但是这将告诉当前foo函数作用域,这个x变量将使用外部全局作用域中的x。

即使是在foo中又写了 x = 10 ,也不会在foo这个局部作用域中定义局部变量x了。

使用了global,foo中的x不再是局部变量了,它是全局变量。

总结
  • x+=1 这种是特殊形式产生的错误的原因?先引用后赋值,而python动态语言是赋值才算定义,才能被引用。 解决办法,在这条语句前增加x=0之类的赋值语句,或者使用global 告诉内部作用域,去全局作用域查找变量定义
  • 内部作用域使用 x = 10 之类的赋值语句会重新定义局部作用域使用的变量x,但是,一旦这个作用域中使用 global 声明x为全局的,那么x=5相当于在为全局作用域的变量x赋值
global使用原则
  • 外部作用域变量会在内部作用域可见,但也不要在这个内部的局部作用域中直接使用,因为函数的目的就是为了封装,尽量与外界隔离
  • 如果函数需要使用外部全局变量,请尽量使用函数的形参定义,并在调用传实参解决
  • 一句话: 不用global 。学习它就是为了深入理解变量作用域
闭包***

自由变量 :未在本地作用域中定义的变量。例如定义在内层函数外的外层函数的作用域中的变量

闭包 :就是一个概念,出现在嵌套函数中,指的是 内层函数引用到了外层函数的自由变量 ,就形成了闭包。很多语言都有这个概念,最熟悉就是JavaScript

# coding: utf-8

def counter():
    c = [0]
    def inc():
        c[0] += 1
        return c[0]
    return inc #有意为之,就是要返回函数,函数用foo变量记住,c闭包必需保留

#出错吗?
#有闭包吗?

foo = counter() #? 可执行,不出错。foo 就是 inc,但是counter已经调用结束,内部局部变量按道理要消亡
print(foo(), foo()) #? 1 2 foo指向inc函数对象,没有被释放。c[0] 是闭包上的c
c = 100
print(foo()) #? 3 foo中的c是局部变量,随着counter()函数执行完,c标识符已经消亡了
def counter():
    c = [0]
    def inc():
        c[0] += 1 # 报错吗? 为什么 # line 4
        return c[0]
    return inc

foo = counter() # line 8
print(foo(), foo()) # line 9
c = 100
print(foo()) # line 11

上面代码有几个问题:

  • 第4行会报错吗?为什么
  • 第9行打印什么结果?
  • 第11行打印什么结果?

代码分析

  • 第8行会执行counter函数并返回inc对应的函数对象,注意这个函数对象并不释放,因为有foo记着
  • 第4行会报错吗?为什么
    • 不会报错,c已经在counter函数中定义过了。而且inc中的使用方式是为c的元素修改值,而不是重新定义c变量
  • 第9行打印什么结果?
    • 打印 1 2
  • 第11行打印什么结果?
    • 打印 3
    • 第9行的c和counter中的c不一样,而inc引用的是自由变量正是counter中的变量c
img_20240425_185828.png

这是Python2中实现闭包的方式,Python3还可以使用nonlocal关键字

再看下面这段代码,会报错吗?使用global能解决吗?

def counter():
    count = 0
    def inc():
        count += 1
        return count
    return inc

foo = counter()
print(foo(), foo())

上例一定出错,使用gobal可以解决

def counter():
    global count
    count = 0
    def inc():
        global count
        count += 1
        return count
    return inc

foo = counter()
print(foo(), foo())
count = 100
print(foo()) # 打印几?

上例 使用global解决,这是全局变量的实现,而不是闭包了

如果要对这个普通变量使用闭包,Python3中可以使用nonlocal关键字。

nonlocal语句

nonlocal:将变量标记为不在本地作用域定义,而是在 上级的某一级局部作用域 中定义,但 不能是全局作用域 中定义,否则报语法错误。

def counter(): #python3 中比较简单实现闭包方式
    count = 0
    def inc():
        nonlocal count # 声明变量count不是本地变量
        count += 1
        return count
    return inc

foo = counter()
print(foo(), foo())

count 是外层函数的局部变量,被内部函数引用。

内部函数使用nonlocal关键字声明count变量在上级作用域而非本地作用域中定义。

代码中内层函数引用外部局部作用域中的自由变量,形成闭包。

def func():
    nonlocal d
    d = 100
# 直接报语法错误,d 向外找就是全局了。nonlocal 不能找全局变量
Snipaste_2023-06-07_09-50-30.png

上例是错误的,nonlocal 声明变量 a 不在当前作用域,但是往外就是全局作用域了,所以错误。

默认值的作用域

# coding: utf-8

def foo(x=1):
    x += 1 #有问题吗?没有。x= 出现了,x就是foo函数的局部变量
    print(x)

foo()
foo()
#: 2
#: 2
# coding: utf-8

def foo1(y=[]): #[]是引用类型
    y.append(1) # y是局部变量,y参数不提供就使用缺省值,缺省值在哪里?保存在函数对象上
    print(y)

foo1()
foo1()
#print(y) # 报错 NameError,当前作用域没有y变量
#: [1]
#: [1, 1]


#不用缺省值
foo1([])
foo1([])
foo1([])
#: [1]
#: [1]
#: [1]

#查看缺省值
print(foo1.__defaults__) #缺省值保存在元组中,元组1个元素,是列表
#: ([1, 1],)

为什么第二次调用foo函数打印的是[1,1]?

  • 因为函数也是对象,python把函数的默认值放在了属性中,这个属性就伴随着这个函数对象的整个生命周期
  • 查看 foo.__defaults__ 属性
# coding: utf-8
def foo2(x='abc', y=123, z=[1,2]):
    x += '~'
    y += 100
    z.append(100)
    print(x, y, z)

print(foo2.__defaults__) #元组和顺序有关,顺序对应形参缺省值
foo2()
print(foo2.__defaults__) 
foo2()
#: ('abc', 123, [1, 2])
#: abc~ 223 [1, 2, 100]
#: ('abc', 123, [1, 2, 100])
#: abc~ 223 [1, 2, 100, 100]

属性 __defaults__ 中使用元组保存所有位置参数默认值,它不会因为在函数体内使用了它而发生改变

# coding: utf-8
def foo3(x=1, *args, m=10, n, **kwargs):
    print(x, m, n, args, kwargs)

print(foo3.__defaults__, foo3.__kwdefaults__) # kwdeaults 使用了字典
foo3(n=200)
  • 属性 __defaults__ 中使用元组保存所有位置参数默认值
  • 属性 __kwdefaults__ 中使用字典保存所有keyword-only参数的默认值
# coding: utf-8

def foo4(x=[]):
    x += [1]
    print(x)

print(1, foo4())
print(1, foo4())
print(foo4.__defaults__)
#: [1]
#: 1 None
#: [1, 1]
#: 1 None
#: ([1, 1],)

def foo5(x=[]):
    x = x + [1]
    print(x)

print(1, foo5())
print(1, foo5())
print(foo5.__defaults__)
#: [1]
#: 1 None
#: [1]
#: 1 None
#: ([],)
# coding: utf-8
x = []
print(1, id(x), x)
x += [1] #x.extend([1])
x += [1]
print(2, id(x), x)
#: 1 1788996419520 []
#: 2 1788996419520 [1, 1]

x = []
print(1, id(x), x)
x = x + [1]  # 生成一个全新的列表,覆盖x。
print(2, id(x), x)
x = x + [1]
print(3, id(x), x)
#: 1 1788996832768 []
#: 2 1788996845440 [1]
#: 3 1788996419520 [1, 1]
x += [1] 和 x = x + [1] 不一样
x += [1] 就地修改
x = x + [1] 生成一个新对象覆盖x

列表的 + 和 += 的区别:

  • +表示两个列表合并并返回一个全新的列表
  • +=表示,就地修改前一个列表,在其后追加后一个列表。就是extend方法

对于不可变类型 += 是什么? 可变类型 += 又是什么?

# coding: utf-8
a = list(range(2))
print(id(a), a)
a += [10] #扩展
print(id(a), a) #地址没变
a = a + [20] #生成新列表,覆盖
print(id(a), a) #地址变了
#: 2165479670400 [0, 1]
#: 2165479670400 [0, 1, 10]
#: 2165479656960 [0, 1, 10, 20]


a = tuple(range(2))
print(id(a), a)
a += (10,) #对于不可变类型元组来讲,将 += 转变为 a = a + (10,)
print(id(a), a) #地址变了
a = a + (20,)
print(id(a), a) #地址变了
#: 2165479670336 (0, 1)
#: 2165479519936 (0, 1, 10)
#: 2165479604768 (0, 1, 10, 20)

x = 1
x += 1 # x += 1 => x = x + 1
对于不可变类型 += 是转变成 = +

对于可变类型 += 分为就地修改和覆盖

变量名解析原则LEGB***

  • Local,本地作用域、局部作用域的local命名空间。函数调用时创建,调用结束消亡
  • Enclosing,Python2.2时引入了嵌套函数,实现了闭包,这个就是嵌套函数的外部函数的命名空间
  • Global,全局作用域,即一个模块的命名空间。模块被import时创建,解释器退出时消亡
  • Build-in,内置模块的命名空间,生命周期从python解释器启动时创建到解释器退出时消亡。例如print(open),print和open都是内置的变量

所以一个名词的查找顺序就是LEGB

Snipaste_2023-06-07_09-54-20.png

函数的销毁

定义一个函数就是生成一个函数对象,函数名指向的就是函数对象。

可以使用del语句删除函数,使其引用计数减1。

可以使用同名标识符覆盖原有定义,本质上也是使其引用计数减1。

Python程序结束时,所有对象销毁。

函数也是对象,也不例外,是否销毁,还是看引用计数是否减为0。

内建函数 函数签名 说明
iter iter(iterable) 把一个可迭代对象包装成迭代器
next next(iterable[,default]) 取迭代器下一个元素。如果已经取完,继续取抛StopIteration异常
reversed reversed(seq) 返回一个翻转元素的迭代器
enumerate enumerate(seq,start=0) 迭代一个可迭代对象,返回一个迭代器。每一个元素都是数字和元素构成的二元组

迭代器

  • 特殊的对象,一定是可迭代对象,具备可迭代对象的特征
  • 通过iter方法把一个可迭代对象封装成迭代器
  • 通过next方法,获取 迭代器对象的一个元素
  • 生成器对象,就是迭代器对象。但是迭代器对象未必是生成器对象

可迭代对象

  • 能够通过迭代一次次返回不同的元素的对象
    • 所谓相同,不是指值是否相同,而是元素在容器中是否是同一个,例如列表中值可以重复的,['a', 'a'],虽然这个列表有2个元素,值一样,但是两个'a'是不同的元素
  • 可以迭代,但是未必有序,未必可索引
  • 可迭代对象有:list、tuple、string、bytes、bytearray、range、set、dict、生成器、迭代器等
  • 可以使用成员操作符in、not in
  • 对于线性数据结构,in本质上是在遍历对象,时间复杂度为O(n)
lst = [1, 3, 5, 7, 9]

it = iter(lst) # 返回一个迭代器对象
print(next(it))
print(next(it))

for i, x in enumerate(it, 2):
    print(i, x)
#print(next(it)) # StopIteration
print()

for x in reversed(lst):
    print(x)

# 比较下面的区别,说明原因?
it = iter(lst)
print(1 in it)
print(1 in it)
print(1 in lst)
print(1 in lst)

匿名函数

python 中,使用 Lambda表达式构建匿名函数。

lambda : 0
# coding: utf-8
fn = lambda : 0  # python 不太推荐

def fn(x):
    x += 1
    return x


def fn(x):
    return x + 1

fn = lambda x: x + 1 # lambda中不能出现=,更不能出现 return,冒号后是一个表达式,表达式计算的结果作为该匿名函数的返回值


def add(x, y):
    return x + y

print( (lambda x,y: x + y)(4, 5) )
print( (lambda x,y=2: x + y)(x=3) )
print( (lambda *,x,y: x + y)(x=3,y=5) ) # lambda表达式,一般不这样用,用在高阶函数中。
#: 9
#: 5
#: 8

#print( [lambda *args: args](4, 5, 6) ) #报错
print( [lambda *args: args][0](4, 5, 6) )
#: (4, 5, 6)

g = (lambda *args: (x for x in args))(1,2,3) #生成器对象
print(next(g))
print(next(g))
#: 1
#: 2

import random
g2 = (lambda *args: {x%3 for x in args})(*[random.randint(1, 1000) for i in range(5)]) # 集合set会去重
print(g2)

lambda函数应用:

  • 高阶函数
# coding: utf-8

# 字符串比较
s = sorted(['a', 1, 'b', 12, 'c', 28], key=str) #对每一个元素做str(item),接收一个参数,返回一个字符串
print(s)
#: [1, 12, 28, 'a', 'b', 'c']

s = sorted(['a', 1, 'b', 12, 'c', 28], key=lambda item: str(item)) 
print(s)
#: [1, 12, 28, 'a', 'b', 'c']

#16进制比较大小
def a(x):
    return int(x, 16) if isinstance(x, str) else x
s = sorted(['a', 1, 'b', 12, 'c', 28], key=a) 
print(s)
#: [1, 'a', 'b', 12, 'c', 28]

s = sorted(['a', 1, 'b', 12, 'c', 28], key=lambda x: int(x, 16) if isinstance(x, str) else x) 
print(s)
#: [1, 'a', 'b', 12, 'c', 28]

print(int('a', 16)) #将16进制数 a 转变为10进制数,为10
# coding: utf-8
from collections import defaultdict
d = defaultdict(list)
d[0].extend((1,2,3))
print(d)
#: defaultdict(<class 'list'>, {0: [1, 2, 3]})

#等价于
from collections import defaultdict
d = defaultdict(lambda :[]) # (lambda :[])()
d[0].extend((1,2,3)) #: 0:[]  (lambda :list())() => []
print(d)
#: defaultdict(<function <lambda> at 0x00000297BC826280>, {0: [1, 2, 3]})
  • 使用lambda关键字定义匿名函数,格式为 lambda [参数列表]: 表达式
  • 参数列表不需要小括号。无参数就不写
  • 冒号用来分割参数列表和表达式部分
  • 不需要使用return。表达式的值,就是匿名函数的返回值。表达式中不能出现等号
  • lambda表达式(匿名函数) 只能写在一行上 ,也称为单行函数

递归函数

函数执行流程

函数的活动和栈有关。

栈是后进先出的数据结构。

栈是从底向顶端生长,栈中插入数据称为压栈、入栈,从栈顶弹出数据称为出栈。

C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。

函数的每一次调用都会创建一个独立的栈帧(Stack Frame)入栈。

因此,可以得到这样一句 不准确 的话: 哪怕是同一个函数两次调用,每一次调用都是独立的,这两次调用没什么关系。

注: 函数执行过程辅助网站: http://pythontutor.com/visualize.html,请不要过分依赖这个网站

# coding: utf-8
def foo1(b, b1=3):
    print("foo1 called", b, b1)

def foo2(c):
    foo3(c)
    print("foo2 called", c)

def foo3(d):
    print("foo3 called", d)

def main():
    print("main called")
    foo1(100,101)
    foo2(200)
    print("main ending")

main()
#: main called
#: foo1 called 100 101
#: foo3 called 200
#: foo2 called 200
#: main ending

foo3
foo2
main
<moudule>

递归

RECURSION

  • 函数直接或者间接调用自身就是递归
  • 递归需要有边界条件、递归前进段、递归返回段
  • 递归一定要有 边界条件
  • 当边界条件不满足的时候,递归前进
  • 当边界条件满足的时候,递归返回

斐波那契数列递归

斐波那契数列Fibonacci number: 1, 1, 2, 3, 5, 8, 13,…

如果设F(n) 为该数列的第n项( \(n \epsilon n^*\) ),那么这句话可以写成如下形式:F(n)=F(n-1)+F(n-2)

有F(0)=0, F(1)=1, F(n)=F(n-1)+F(n-2)

# coding: utf-8

def fib(n):
    #a, b = 1, 1
    a = b = 1
    for i in range(n-2): #fib(3) 循环一下
        a, b = b, a+b
    return b

print(fib(3))

使用递归

def fib2(n): #递归
    return fib2(n-1) + fib2(n-2) # 递归必需有边界,因为栈空间有限。

fib(3) # RecursionError 递归异常,maximum recursion depth exceeded, 递归层次太深
# 栈超界,Python 中为了防止栈溢出,提供了调用深度限制,默认1000,IPython 3000
# coding: utf-8
def fib2(n): #递归
    if n < 3:
        return 1
    return fib2(n-1) + fib2(n-2) # 递归必需有边界,因为栈空间有限

print(fib2(3))

递归要求

  • 递归一定要有退出条件,递归调用一定要执行到这个退出条件。没有退出条件的递归调用,就是无限调用
  • 递归调用的深度不宜过深
    • Python 对递归调用的深度做了限制,以保护解释器
    • 超过递归深度限制,抛出RecursionError: maxinum recursion depth exceeded 超出最大深度
    • sys.getrecursionlimit() 查看深度限制

递归效率

  • 循环稍微复杂一些,但是只要不是死循环,可以多次迭代直至算出结果。
  • fib函数代码极简易懂,但是只能获取到最外层的函数调用,内部递归结果都是中间结果。而且给定一个n都要进行近2n次递归,尝试越深,效率越低。为了获取斐波那契数列需要外面在套一个n次的循环就是更低了。
  • 递归还有深度限制,如果递归复杂,函数反复压栈,栈内存很快溢出了。

思考:这个极简的递归代码能否提高性能呢??

# coding: utf-8

def fib_v3(n, a=1, b=1):  #用递归调用次数来模拟循环次数
    if n < 3:
        return b # 累计计算结果
    a, b = b, a+b
    return fib_v3(n-1, a, b)

def fib_v3(n, a=1, b=1):  #用递归调用次数来模拟循环次数
    if n < 3:
        return b # 累计计算结果
    return fib_v3(n-1, b, a+b)

def fib_v3(n, a=1, b=1):  #用递归调用次数来模拟循环次数
    if n < 3:
        return b # 累计计算结果
    else:
        return fib_v3(n-1, b, a+b)

def fib_v3(n, a=1, b=1):  #用递归调用次数来模拟循环次数
    if n < 3:
        return b # 累计计算结果
    return b if n<3 else fib_v3(n-1, b, a+b)

for i in range(3, 10):
    print(i, fib_v3(i))
#: 3 2
#: 4 3
#: 5 5
#: 6 8
#: 7 13
#: 8 21
#: 9 34
def fib(n):
    #a, b = 1, 1
    a = b = 1
    for i in range(n-2): #fib(3) 循环一下
        a, b = b, a+b
    return b


def fib_v3(n, a=1, b=1):  #用递归调用次数来模拟循环次数
    if n < 3:
        return b # 累计计算结果
    return b if n<3 else fib_v3(n-1, b, a+b)

%%timeit n=101
fib(n) # 8.62us


%%timeit n=101
fib_v3(n) #28.5us 时间消耗在压栈

间接递归

def foo1():
    foo2()

def foo2():
    foo1()

foo1()

间接递归调用,是函数通过别的函数调用了自己,这样是递归。

只要是递归调用,不管是直接还是间接,都要注意边界返回问题。但是间接递归调用有时候是非常明显,代码调用复杂时,很难发现出现了递归调用,这是非常危险的。

所以,使用良好的代码规范来避免这种递归的发生。

总结

  • 递归是一种很自然的表达,符合逻辑思维
  • 递归相对运行效率低,每一次调用函数都要开辟栈帧
  • 递归有深度限制,如果递归层次太深,函数连续压栈,栈内存很快就溢出了
  • 如果是有限次数的递归,可以使用递归调用,或者使用循环代替,循环代码稍微复杂一些,但是只要不是死循环,可以多次迭代直至算出结果
  • 即使递归代码很简洁,但是 能不用则不用递归

练习

使用递归实现

  • 求n的阶乘
  • 将一个数逆序放入列表中,例如1234 => [4,3,2,1]
  • 解决猴子吃桃问题

    猴子第一天摘下若干个桃子,当即吃了一半,还不过瘾,又多吃了一个。第二天早上又将剩下的桃子吃掉一半,又多吃了一个。以后每天早上都吃了前一天剩下的一半零一个。到第 10 天早上想吃时,只剩下一个桃子了。求第一天共摘多少个桃子。

递归的解法,一般来说有2种:

  1. 一种如同数学公式
  2. 一种类似循环,相当于循环的改版,将循环迭代,变成了函数调用压栈
求n的阶乘
# coding: utf-8
#阶乘公式。三元表达式

def factorial(n):
#    if n == 1:
#        return 1
#    elif n == 2:
#        return 2
#    if n == 2:
#        return 2
    if n < 3:
        return n
    return n * factorial(n-1) #公式


def factorial(n):
    return n if n < 3 else n * factorial(n-1)

print(factorial(3))

#循环
def factorial(n=6):
    p = 1
    for i in range(1, n+1):
        p *= i
    return p

def factorial(n=6, p=1): #将上次结果带入
    if n == 1:
        return p
    return factorial(n-1, p * n)

def factorial(n=6, p=1): #将上次结果带入
    return p if n == 1 else factorial(n-1, p * n)

print(factorial(6))
解决猴子吃桃问题

猴子第一天摘下若干个桃子,当即吃了一半,还不过瘾,又多吃了一个。第二天早上又将剩下的桃子吃掉一半,又多吃了一个。以后每天早上都吃了前一天剩下的一半零一个。到第 10 天早上想吃时,只剩下一个桃子了。求第一天共摘多少个桃子。

# coding: utf-8

#循环版本
def  peach():
    total = 1
    for i in range(9):
        total = 2*(total + 1)
    return total

#递归
def peach(days=10):
    if days == 1:
        return 1
    return 2 * (peach(days-1) + 1)

print(peach())
#: 1534

注意这里必须是10

从10到2做了9次, return(peach(days-1) +1)*2 计算,但是还没有边界,再进入函数后立即触发等于1条件返回,所以实际上执行了9次计算,第10次只是为了进函数触发反弹的。

换种方式表达

# coding: utf-8
def peach(days=1):
    if days == 10:
        return 1
    return 2 * (peach(days+1) +1)

print(peach()) #第10下只是为了进入函数解底反弹的

def peach(days=9, p=1):
    if days == 0:
        return p
    return peach(days-1, 2*(p+1))
将一个数逆序放入列表中

例如1234 => [4,3,2,1]

# coding: utf-8
#内建函数实现
print(list(map(int, reversed(str(1234)))))
#: [4, 3, 2, 1]

x = 1234
while x != 0:
    x, y = divmod(x, 10)
    print(x, y)
    print(y, '~~')

#字符串递归切片
x = str(1234)

def revert(data):
    if not data:
        return []
    return [data[-1]] + revert(data[:-1])

print(revert(x))
#['4', '3', '2', '1']

#字符串
x = str(1234)

target = []

def revert(data):

    if not data:
        return
    target.append(data[-1])
    return revert(data[:-1])

revert(x)
print(target)
#['4', '3', '2', '1']
del target

#字符串
x = str(1234)
#target = []
def revert(data, target=[]):
    if not data:
        return target
    target.append(data[-1])
    return revert(data[:-1], target)
print(revert(x))
#['4', '3', '2', '1']

#数值
x = 123045
x = 100

def revert(data, target=[]):
    if data == 0:
        return target
    x, y = divmod(data, 10) #10 0; 1 0; 0 1
    target.append(y)
    return revert(x, target)

print(revert(x))


#数值
x = 123045

def revert(data, target=None):
    if target is None:
        target = []
    if data == 0:
        return target
    x, y = divmod(data, 10) #10 0; 1 0; 0 1
    target.append(y)
    return revert(x, target)

print(revert(x))
print(revert(x, None))
#[5, 4, 0, 3, 2, 1]
扁平化字典

例如,源字典 {'a':{'b':1, 'c':2}, 'd':{'e':3, 'f':{'g':4}}} 目标字典 {'a.c':2, 'd.e':3, 'd.f.g':4, 'a.b':1}

# coding: utf-8

d = {'a':{'b':1, 'c':2}, 'd':{'e':3, 'f':{'g':4}}}

target = {}

def flat_map(src:dict): # :dict参数注解,说明src的类型,但不强制
    for k, v in src.items():
        print(k)
        if isinstance(v, dict):
            print(v, 'recursion')
            flat_map(v)
        else:
            target[k] = v
print(flat_map(d))
print(target)
#{'b': 1, 'c': 2, 'e': 3, 'g': 4}


#改进
d = {'a':{'b':1, 'c':2}, 'd':{'e':3, 'f':{'g':4}}}
#d = {'h': 5}
#d = {'a': {'b':1}}
#d = {'d': {'f':{'g':4}}}
target = {}

def flat_map(src:dict, prefix=''): # :dict参数注解,说明src的类型,但不强制
    for k, v in src.items():
        if isinstance(v, dict):
            flat_map(v, prefix + k + '.')
        else:
            target[prefix + k] = v

print(flat_map(d))
print(target)
#None
#{'a.b': 1, 'a.c': 2, 'd.e': 3, 'd.f.g': 4}
del target

#改进
d = {'a':{'b':1, 'c':2}, 'd':{'e':3, 'f':{'g':4}}}
#target = {}
def flat_map(src:dict, target=None, prefix=''): # :dict参数注解,说明src的类型,但不强制
    if target is None:
        target = {}
    for k, v in src.items():
        if isinstance(v, dict):
            flat_map(v, target, prefix + k + '.')
        else:
            target[prefix + k] = v
    return target

print(flat_map(d))
#{'a.b': 1, 'a.c': 2, 'd.e': 3, 'd.f.g': 4}

#改进,使用都只需要传入一个参数就够了

d = {'a':{'b':1, 'c':2}, 'd':{'e':3, 'f':{'g':4}}}
def flat_map(src:dict): # :dict参数注解,说明src的类型,但不强制
    target = {}

    def _flat(s:dict, prefix=''):
        for k, v in s.items():
            if isinstance(v, dict):
                _flat(v, prefix + k + '.')
            else:
                target[prefix + k] = v

    _flat(src)
    return target

print(flat_map(d))
#{'a.b': 1, 'a.c': 2, 'd.e': 3, 'd.f.g': 4}

排序算法-直接插入排序

  • 插入排序
    • 每一趟都要把待排序数放到有序区中合适的插入位置
img_20240428_161728.png
开头的红色数字为哨兵,即待插入值
从第二个数字开始排序即9
第一趟,哨兵9,1和哨兵比较,1小,哨兵插入,本轮比较结束。1,9有序区,后面为无序区
第二趟,哨兵8,9和哨兵比较,大于哨兵9右移,1和哨兵比较,1小,哨兵插入本轮结束
以此类推,直至把最后一个数字入到哨兵并比较、插入完成

核心算法

  • 结果可为升序或降序排列,默认升序排列。以升序为例
  • 扩大有序区,减小无序区。图中绿色部分就是增大的有序区,黑色部分就是减小的无序区
  • 增加一个哨兵位,图中最左端红色数字,其中放置每一趟待比较数值
  • 将哨兵数值与有序区数值从右到左依次比较,找到哨兵位数值合适的插入点

算法实现

  • 增加哨兵位
    • 为了方便,采用列表头部索引 0 位置插入哨兵位
    • 每一次从有序区最右端后的下一个数,即无序区最左端的数放到哨兵位
  • 比较与挪动
    • 从有序区最右端开始,从右至左依次与哨兵比较
# coding: utf-8

nums = [1, 9, 8, 5, 6]
import random
random.shuffle(nums)
print(nums)

nums = [None] + nums # 前面加哨兵位
print(nums, nums[1:])
length = len(nums)
count_move = 0

for i in range(2, length): # 拿一个持排序数
    nums[0] = nums[i]  # 索引0位哨兵,索引1位假设的有序区,都跳过

    # 和有序区最右端开始比较,直到合适位置插入
    j = i - 1 # j = 2 [8, 1, 9, 8] i左边的那个数就是有序区末尾
    if nums[j] > nums[0]: #如果最右端数大于哨兵才需要挪动和插入
        while nums[j] > nums[0]: # 写挪动
            nums[j+1] = nums[j] #[8,1,?,9]
            j -= 1 #继续向左
            count_move += 1
        nums[j+1] = nums[0] # [8,1,8,9] 循环中多减了一次j

print(nums[1:], count_move)

总结

  • 最好情况,正好是升序排列,比较迭代n-1次
  • 最差情况,正好是降序排列,比较迭代1,2,…,n-1即n(n-1)/2,数据移动非常多
  • 使用两层嵌套循环,时间复杂度O(n^2),不要在大规模数据分析使用
  • 稳定排序算法
    • 如果待排序序列R中两元素相等,即Ri等于Rj,且i<j,那么排序后这个先后有顺序不变,这种排序算法就称为稳定排序
    • 已经学习过的排序算法哪些是稳定排序,考虑1、1、2排序
  • 优化
    • 如果比较操作耗时大的话,可以采用二分查找来提高效率,即二分查找插入排序

排序稳定性

  • 冒泡排序,相同数据不交换,稳定
  • 直接选择排序,相同数据前面的先选择到,排到有序区,不稳定
  • 直接插入排序,相同数据不移动,相对位置不变,稳定

生成器***

生成器generator

  • 生成器指的是生成器对象,可以由生成器表达式得到,也可以使用yield关键字编写一个生成器函数, 调用这个函数得到一个生成器对象
  • 生成器对象,是一个可迭代对象,是一个迭代器
  • 生成器对象,是延迟计算、惰性求值的

生成器函数

  • 函数体中包含yield语句的函数,就是生成器函数,调用后返回生成器对象
# coding: utf-8

m = (i for i in range(5))
print(next(m))
print(next(m))
print(next(m))
#: 0
#: 1
#: 2


def foo(): # 只要有yield都算生成器函数,每一次生成器函数调用都会产生一个全新的生成器对象
    for i in range(5):
        yield i #遇到yield后,把yield后的值返回出去,同时暂停当前函数执行

print(foo())
print(next(foo()))
print(next(foo()))
print(next(foo()))
#: <generator object foo at 0x000001CB449BBCF0>
#: 0
#: 0
#: 0

n = foo()
print(next(n))
print(next(n))
#: 0
#: 1

for i in n:
    print(i)
#2
#3
#4

for i in n:
    print(i)
#没报错

print(type(foo), type(foo()))
#<class 'function'> <class 'generator'>

普通函数调用,函数立即执行直到执行完毕。

生成器函数调用,并不会立即执行函数体,而是返回一个生成器对象,需要使用next函数来驱动这个生成器对象,或者使用循环来驱动。

生成器表达式和生成器函数都可以得到生成器对象,只不过生成器函数可以写更加复杂的逻辑。

生成器函数的执行

# coding: utf-8
def foo():
    print(1)
    yield 2
    print(3)
    yield 4
    print(5)
    yield 6
    return 7
    yield 8

g = foo() # g 是生成器对象

print(g)
print(next(g)) #no1 打印什么,返回什么。打印1,返回2
print(next(g)) #no2 打印什么,返回什么。打印3,返回4
print(next(g)) #no3 打印什么,返回什么。打印5,返回6
#print(next(g)) #no4 打印什么,返回什么。报错StopIteration,return 的7拿不到,return 函数完结了。
print(next(g, 'end')) #next 迭代器不希望报错,就给缺省值

#: <generator object foo at 0x0000027FE1AC5E40>
#: 1
#: 2
#: 3
#: 4
#: 5
#: 6
#: end
# coding: utf-8

def foo2():
    print(111)
    yield 'abc'
    print(222)
    yield  #什么都不写相当于 return

g = foo2()
print(g)
print(next(g))
print(next(g))
#print(next(g)) 报错StopIteration
#: <generator object foo2 at 0x00000220BC1F5E40>
#: 111
#: abc
#: 222
#: None
  • 在生成器函数中,可以多次yield,每执行一次yield后会暂停执行,把yield表达式的值返回
  • 再次执行会执行到下一个yield语句又会暂停执行
  • 函数返回
    • return 语句依然可以终止函数运行,但return语句的返回值不能获取到
    • return会导致当前函数返回,无法继续执行,也无法继续获取下一个值,抛出StopIteration异常
    • 如果函数没有显示的return语句,如果生成器函数执行到结尾(相当于执行了return None),一样会抛出StopIteration异常

生成器函数

  • 包含yield语句的生成器函数调用后,生成生成器对象的时候, 生成器函数的函数体不会立即执行
  • next(generator)会从函数的当前位置向后执行到之后碰到的第一个yield语句,会弹出值,并暂停函数执行
  • 再次调用next函数,和一条一样的处理过程
  • 继续调用next函数,生成器函数如果结束执行了(显示或隐式调用了return语句),会抛出StopIteration异常

生成器应用

无限循环
# coding: utf-8

def counter():
    i = 0
    while True:
        i += 1
        yield i

c = counter() # c generator object
print(next(c))
#: 1

for i in range(5):
    print(i, next(c))
#: 0 2
#: 1 3
#: 2 4
#: 3 5
#: 4 6
# coding: utf-8

def counter():
    def inc():
        i = 0
        while True:
            i += 1
            yield i

    c = inc()
    return next(c)

g = counter() # function
print(g)
print(g)

#: 1
#: 1
计数器
# coding: utf-8

def counter():
    def inc():
        i = 0
        while True:
            i += 1
            yield i

    c = inc()
    def fn():
        return next(c) #有闭包,c对应的对象不能释放
    return fn

foo2 = counter() # 返回什么
print(foo2())
print(foo2())

#: 1
#: 2
# coding: utf-8

def counter():
    def inc():
        i = 0
        while True:
            i += 1
            yield i

    c = inc()
#    def fn():
#        return next(c) #有闭包,c对应的对象不能释放
#    return fn
    return lambda : next(c)


f = counter() # 返回什么
print(f())
print(f())
print(f())

#: 1
#: 2
#: 3
斐波那契数列
# coding: utf-8
def fib():
    a = 0
    b = 1
    yield b
    while True:
        a, b = b, a+b
        yield b

f = fib()
for i in range(5):
    print(i+1, next(f))
#: 1 1
#: 2 1
#: 3 2
#: 4 3
#: 5 5
生成器交互

python 还提供了一个和生成器对象交互的方法send,该方法可以和生成器沟通。

# coding: utf-8
def bar():
    for i in range(3):
        x = yield 100
        print(x, '+++')

g = bar()
print(next(g)) # 不打印,返回100。yield 100 抛出后,暂停了。next 函数只能驱动迭代器
print(next(g)) #打印None +++,返回100
print(next(g)) #打印None +++,返回100
#print(next(g)) #打印None +++,报错 StopIteration


g = bar()
print(next(g)) 
print(g.send(1)) #打印1 +++,返回100。驱动,send的实参作为yield语句的表达式的返回值
print(g.send('abc')) #打印abc +++,返回100
# coding: utf-8
def counter():
    def inc():
        i = 0
        while True:
            i += 1
            response = yield i
            if response is not None:
                i = response

    c = inc()
    def fn(reset=False):
        return c.send(0) if reset else next(c)
    return fn

g = counter()
print(g)
print(g())
print(g(True)) # 重置 i = 0
#: <function counter.<locals>.fn at 0x0000029F98069550>
#: 1
#: 1

print(g())
#: 2
# coding: utf-8
def counter():
    def inc():
        i = 0
        while True:
            i += 1
            response = yield i
            if response is not None:
                i = response

    c = inc()
#    def fn(reset=False):
#        return c.send(0) if reset else next(c)
#    return fn
    return lambda reset=False, start=0: c.send(start) if reset else next(c)

g = counter()
print(g)
print(g())
print(g(True)) # 重置 i = 0
print(g())
#: <function counter.<locals>.<lambda> at 0x000002A189F39550>
#: 1
#: 1
#: 2
  • 调用send方法,就可以把send的实参传给yield语句做结果,这个结果可以在等式右边被赋值给其它变量
  • send和next一样可以推动生成器启动并执行
协程Coroutine
  • 生成器的高级用法
  • 它比进程、线程轻量级,是在用户空间调度函数的一个实现
  • python3 asyncio就是协程实现,已经加入到标准库
  • python3.5使用async、await关键字直接原生支持协程
  • 协程调度器实现思路

    有2个生成器A、B

    next(A)后,A执行到了yield语句暂停,然后去执行next(B),B执行到yield语句也暂停,然后再次调用next(A),再试用next(B)在,周而复始,就实现了调度的效果

    可以引入调度的策略实现切换的方式

  • 协程是一种非抢占式调度

yield from语法

从python3.3开始增加了yield from语法,使用 yield from iterable 等价于 for item in iterable

yield from就是一个简化语法的语法糖。

# coding: utf-8
def foo():
    for i in range(5): #对一个可迭代对象range对象进行yield它的每一个元素
        yield i

#等价于
def foo():
    yield from range(5)

g = foo()
print(g)
print(next(g))
print(next(g))
#: <generator object foo at 0x00000284F5655E40>
#: 0
#: 1

数据结构-非线性数据结构-树

定义: 树是非线性结构,是n个(n>=0)元素的集合

n为0时,称为空树

树中只有一个特殊的没有前驱的元素,称为树的根Root。

树中除了根结点外,其余元素只能有 一个前驱 ,可以有零个或多个后继。如果有多个前驱就不是树了,而是图。

递归定义:树T是n(n>=0)个元素的集合。n=0时,称为空树。

有且只有一个特殊元素根,剩余元素都可以被划分为m个互不相交的集合T1、T2、T3、…、Tm,而每一个集合都是树,称为T的子树Subtree。

子树也有自己的根。

名词解释
  • 结点:树中的数据元素
  • 结点的度degree:结点拥有的子树的数目称为度,记作d(v)
  • 叶子结点:结点的度为0,称为叶子结点leaf、终端结点、末端结点
  • 分支结点:结点的度不为0,称为非终端结点或分支结点
  • 分支:结点之间的关系
  • 内部结点:除根结点外的分支结点,当然也不包含叶子结点
  • 树的 是树内各结点的度的最大值。D结点度最大为3,树的度数就是3
img_20240511_203840.png
A,B,C,D,E,F,G,H,I,j 都叫树的结点

结点有度数,结点分几个叉就是几度。如A结点分2叉,度数为2

G,H,I,J,F都是叶子结点,它们度数为0,没有子结点

结点之间的关系,B是A的分支

内部结点,在根结点和叶子结点之间的部分

树的度数,树内哪个结点的度数最大就是树的度数。
  • 孩子(儿子Child)结点:结点的子树的根结点称为结点的孩子
  • 双亲(父Parent)结点:一个结点是它各子树的根结点的双亲
  • 兄弟(Sibling)结点:具有相同双亲结点的结点
  • 祖先结点:从根结点到该结点所经分支上所有结点。A、B、D都是G的祖先结点
  • 子孙结点:结点的所有子树上的结点都称为该结点的子孙。B的子孙是D,G,H,I
  • 结点的层次(Level):根节点为第一层,根的孩子为第二层,以此类推,记作L(v)。如A第一层,B,C第二层,D,E,F第三层
  • 树的深度(高度Depth):树的层次的最大值。上图的树深度为4
  • 堂兄弟:双亲在同一层的结点
  • 有序树:结点的子树是有顺序的(兄弟有大小,有先后次序),不能交换。
  • 无序树:结点的子树是有无序的,可以交换。
  • 路径:树中k个结点n1,n2,…,nk,满足ni是n(i+1)的双亲,成为n1到nk的一条路径。就是一条线串下来的,前一个都是后一个的父(前驱)结点。如A,B,D,I一条路径
  • 路径长度=路径上结点数-1,也是分支数。如A,B,D,I一条路径,路径长度为3
  • 森林:m(m>=0)棵不相交的树的集合
    • 对于结点而言,其子树的集合就是森林。A结点的2棵子树的集合就是森林。
特点
  • 唯一的根
  • 子树不相交
  • 除了根以外,每个元素只能有一个前驱,可以有零个或多个后继
  • 根结点没有双亲结点(前驱),叶子结点没有孩子结点(后继)
  • vi是vj的双亲,则L(vi)=L(vj)-1,也就是说双亲比孩子结点的层次小1

堂兄弟的双亲是兄弟关系吗?

堂兄弟定义是,双亲结点是同一层的节点。上图G和J是堂兄弟,因为他们的双亲结点D和E在第三层,依然是堂兄弟。因此,堂兄弟的双亲不一定是兄弟关系。

二叉树

概念
  • 每个结点最多2棵子树
  • 二叉树不存在度数大于2的结点
  • 它是有序树,左子树、右子树是顺序的,不能交换次序
  • 即使某个结点只有一棵子树,也要确定它是左子树还是右子树

二叉树的五种基本形态:

  • 空二叉树
  • 只有一个根结点
  • 根结点只有左子树
  • 根结点只有右子树
  • 根结点有左子树和右子树
斜树

左斜树,所有结点都只有左子树;右斜树,所有节点都只有右子树

满二叉树
  • 一棵二叉树的所有分支结构都存在左子树和右子树,并且所有叶子结点只存在在最下面一层
  • 同样深度二叉树中,满二叉树结点最多
  • k为深度( \(1 \leq k \leq n\) ),则结点总数为 \(2^k - 1\)
  • 如下图,一个深度为4的15个结点的满二叉树
img_20240511_213732.png
完全二叉树

完全二叉树Complete Binary Tree

  • 若二叉树的深度为k,二叉树的层数人1到k-1层的结点数都达到了最大个数,在第k层的所有结点都集中在最左边,这就是完全二叉树
  • 完全二叉树由满二叉树引出
  • 满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树
  • k为深度( \(1 \leq k \leq n\) ),则结点总数最大值为 \(2^k -1\) ,当达到最大值的时候就是满二叉树

下图三个树都是完全二叉树,最下一层的叶子结点都连续的集中在左边

img_20240511_220135.png
性质
  • 性质1:在二叉树的第i层上至多有 \(2^(i-1)\) 个结点(i>=1)
  • 性质2:深度为k的二叉树,至多有 \(2^k -1\) 个结点(k>=1)
    • 一层 2-1 = 1
    • 二层 4-1 = 1+2 = 3
    • 三层 8-1 = 1+2+4 = 7
  • 性质3:对任何一棵二叉树T,如果其终端结点数为n0,度数为2的结点为n2,则有n0=n2+1
    • 换句话说,就是叶子结点数-1就等于度数为2的结点数
    • 证明
      • 总结点数为n=n0+n1+n2,n1为度数为1的结点总数
      • 一棵树的分支数为n-1,因为除了根结点外,其余结点都有一个分支,即n0+n1+n2-1
      • 分支数还等于 n0*0+n1*1+n2*2 ,n2是2分支结点所以乘以2,2*n2+n1
      • 可得2*n2+n1=n0+n1+n2-1 => n2 = n0-1

其他性质:

  • 高度为k的二叉树,至少有k个结点
  • 含有n (n>=1) 的结点的二叉树高度至多为n。和上句一个意思
  • 含有n (n>=1) 的结点的二叉树高度至多为n,最小为math.cel( \(log_2(n+1)\) ),不小于对数值的最小整数,向上取整
    • 假设高度为h,2^h-1 = n => h = \(log_2(n+1)\) ,层次数是取整。如果是8个节点,3.1699就要向上取整为4,为4层
  • 性质4:具有n个结点的完全二叉树的深度为 \(int(log_2n)+1\) 或者math.cel( \(log_2(n+1)\) )
  • 性质5:
    • 如果有一棵n个结点的完全二叉树(深度为性质4),结点按照层序编号
    • 如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是int(i/2),向下取整。就是子节点的编号整除2得到的就是父结点的编号。父结点如果是i,那么左孩子结点就是2i,右孩子结点就是2i+1
    • 如果2i>n,则结点i无左孩子,即结点i为叶子结点;否则其左孩子结点在编号为2i
    • 如果2i+1>n,则结点i无右孩子,注意这里并不能说明结点i没有孩子;否则右孩子结点存在

树的遍历和堆排序

二叉树的遍历

遍历: 迭代所有元素一遍

树的遍历

对树中所有元素不重复地访问一遍,也称作扫描

广度优先遍历

  • 层序遍历
20230725175837977.png

深度优先遍历

  • 前序遍历
  • 中序遍历
  • 后序遍历

遍历序列

  • 将树中所有元素遍历一遍后,得到的元素的序列。将层次结构转换成了线性结构

层序遍历

  • 按照树的层次,从第一层开始,自左向右遍历元素
  • 遍历序列
    • ABCDEFGHI
深度优先遍历
  • 设树的根结点为D,左子树为L,右子树为R,且要求L一定在R之前,则有下面几种遍历方式:
    • 前序遍历,也叫先序遍历、也叫先根遍历,DLR
    • 中序遍历,也叫中根遍历,LDR
    • 后序遍历,也叫后根遍历,LRD

前序遍历DLR

  • 从根结点开始,先左子树后右子树
  • 每个子树内部依然是先根结点,再左子树后右子树。递归遍历
  • 遍历序列
    • A BDGH CEIF
20230725180126795.png

中序遍历LDR

  • 从根结点的左子树开始遍历,然后是根结点,再右子树
  • 每个子树内部,也是先左子树,后根结点,再右子树。对遍历
  • 遍历序列
    • 左图:GDHB A IECF
    • 右图:GDHB A EICF
20230725184518047-635470053.png

后序遍历LRD

  • 先左子树,后右子树,再根结点
  • 每个子树内部依然是先左子树,后右子树,再根结点。递归遍历
  • 遍历序列
    • GHDB IEFC A
20230725184606920-1703560985.png

打印树

origin = [30, 20, 80, 40, 50, 10, 60, 70, 90]
              90
      70              80
  40      50      10      60
30  20
#居中打印
import math

origin = [10, 20, 30]

def print_tree(array: list):
    length = len(array)
    depth = math.ceil(math.log2(length + 1)) #层数。 #ceil向上取整,log2取以2为底的对数

    index = 0
    for i in range(depth): #0 1 2 3
        for j in range(2 ** i): # 1, 2
            print(array[index], end=' ')
            index += 1
        print()

print_tree(origin)

#返回结果
# 10
# 20 30
#居中打印
import math

# origin = [10, 20, 30]
origin = list(range(10, 100, 10))

def print_tree(array: list):
    length = len(array)
    depth = math.ceil(math.log2(length + 1)) #层数。 #ceil向上取整,log2取以2为底的对数

    index = 0
    for i in range(depth): #0 1 2 3
        for j in range(2 ** i): # 1, 2
            print(array[index], end=' ')
            index += 1
            if index >= length:
                break
        print()

print(origin)
print_tree(origin)

#返回结果
# [10, 20, 30, 40, 50, 60, 70, 80, 90]
# 10 
# 20 30 
# 40 50 60 70 
# 80 90 
#居中打印
import math

# origin = [10, 20, 30]
origin = list(range(10, 100, 10))

def print_tree(array:list, unit_width=2):
    """
    第1层 结点数 2^1 - 1 = 1
    第2层 结点数 2^2 - 1 = 3
    第3层 结点数 2^3 - 1 = 7
    第4层 结点数 2^4 - 1 = 15
    """
    length = len(array) #长度
    depth = math.ceil(math.log2(length + 1)) #层数。 #ceil向上取整,log2取以2为底的对数
    width = 2 ** depth - 1 #宽度
    print(length, depth, width)

    index = 0
    for i in range(depth): #0 1 2 3
        for j in range(2 ** i): # 1, 2
            print(array[index], end=' ')
            index += 1
            if index >= length:
                break
        print()

print(origin)
print_tree(origin)

#返回结果
# [10, 20, 30, 40, 50, 60, 70, 80, 90]
# 9 4 15
# 10 
# 20 30 
# 40 50 60 70 
# 80 90 

居中打印

#居中打印
import math

# origin = [10, 20, 30]
origin = list(range(10, 100, 10))

def print_tree(array:list, unit_width=2):
    """
    第1层 结点数 2^1 - 1 = 1
    第2层 结点数 2^2 - 1 = 3
    第3层 结点数 2^3 - 1 = 7
    第4层 结点数 2^4 - 1 = 15
    """
    length = len(array) #长度
    depth = math.ceil(math.log2(length + 1)) #层数。 #ceil向上取整,log2取以2为底的对数
    width = 2 ** depth - 1 #宽度
    print(length, depth, width)

    index = 0
    for i in range(depth): #0 1 2 3
        for j in range(2 ** i): # 1, 2
            # print(array[index], end=' ')
            print("{:#^{}}".format(array[index], width * unit_width), end='')
            index += 1
            if index >= length:
                break
        print()

print(origin)
print_tree(origin)

#返回结果
# [10, 20, 30, 40, 50, 60, 70, 80, 90]
# 9 4 15
# ##############10##############
# ##############20############################30##############
# ##############40############################50############################60############################70##############
# ##############80############################90##############
#居中打印
import math

# origin = [10, 20, 30]
origin = list(range(10, 100, 10))

def print_tree(array:list, unit_width=2):
    """
    第1层 结点数 2^1 - 1 = 1
    第2层 结点数 2^2 - 1 = 3
    第3层 结点数 2^3 - 1 = 7
    第4层 结点数 2^4 - 1 = 15
    """
    length = len(array) #长度
    depth = math.ceil(math.log2(length + 1)) #层数。 #ceil向上取整,log2取以2为底的对数
    width = 2 ** depth - 1 #宽度
    print(length, depth, width)

    index = 0
    for i in range(depth): #0 1 2 3
        for j in range(2 ** i): # 1, 2
            # print(array[index], end=' ')
            print("{:#^{}}".format(array[index], width * unit_width), end=' '*unit_width)
            index += 1
            if index >= length:
                break
        width //= 2
        print()

print(origin)
print_tree(origin)

#返回结果
# [10, 20, 30, 40, 50, 60, 70, 80, 90]
# 9 4 15
# ##############10##############  
# ######20######  ######30######  
# ##40##  ##50##  ##60##  ##70##  
# 80  90  
print("{:^{}}".format(array[index], width * unit_width), end=' '*unit_width)

#               10                
#       20              30        
#   40      50      60      70    
# 80  90  

居中打印-补空格方法

#居中打印-补空格法
import math

origin = [None, 10, 20, 30, 40, 50, 60, 70, 80, 90]

def print_tree(array:list, unit_width=2):
    """
    第1层 结点数 2^1 - 1 = 1
    第2层 结点数 2^2 - 1 = 3
    第3层 结点数 2^3 - 1 = 7
    第4层 结点数 2^4 - 1 = 15

            前空格    间隔空格(元素之间)  切片
    每1层 4  7=2**3-1  ?    15=2**4-1 因为只有一个元素,不用关心 1:2 10
    每2层 3  3=2**2-1  7=2*3+1 2**3-1  2:4 20 30
    每3层 2  1=2**1-1  3=2*1+1 2**2-1  4:8 40 50 60 80
    每4层 1  0=2**0-1  1=2*0+1 2**1-1  8:16
    """
    length = len(array)#长度
    depth = math.ceil(math.log2(length)) #4
    width = 2 ** depth - 1 #宽度
    start = 1
    for i in range(depth, 0, -1): #4 3 2 1
        #解决前导空格
        pre = 2 ** (i - 1) - 1
        print(pre * unit_width * ' ', end='')
        interval = (2 * pre + 1) * unit_width * ' ' #间隔字符
        end = start + start
        print(interval.join(map(str, array[start:end])))
        start = end

print(origin)
print_tree(origin)

#返回结果
# [None, 10, 20, 30, 40, 50, 60, 70, 80, 90]
#               10
#       20              30
#   40      50      60      70
# 80  90
#居中打印-补空格法
import math

origin = [None, 10, 20, 30, 40, 50, 60, 70, 80, 90]

def print_tree(array:list, unit_width=2):
    """
    第1层 结点数 2^1 - 1 = 1
    第2层 结点数 2^2 - 1 = 3
    第3层 结点数 2^3 - 1 = 7
    第4层 结点数 2^4 - 1 = 15

            前空格    间隔空格(元素之间)  切片
    每1层 4  7=2**3-1  ?    15=2**4-1 因为只有一个元素,不用关心 1:2 10
    每2层 3  3=2**2-1  7=2*3+1 2**3-1  2:4 20 30
    每3层 2  1=2**1-1  3=2*1+1 2**2-1  4:8 40 50 60 80
    每4层 1  0=2**0-1  1=2*0+1 2**1-1  8:16
    """
    length = len(array)#长度
    depth = math.ceil(math.log2(length)) #4
    start = 1
    for i in range(depth, 0, -1): #4 3 2 1
        #解决前导空格
        pre = 2 ** (i - 1) - 1
        print(pre * unit_width * ' ', end='')
        interval = (2 * pre + 1) * unit_width * ' ' #间隔字符
        end = start + start
        print(interval.join(map(lambda x: "{:{}}".format(x, unit_width), array[start:end])))
        start = end

origin = [None] + list(range(10, 30))
print(origin)
print_tree(origin, 1)

#返回结果
# [None, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
#                10
#        11               12
#    13       14       15       16
#  17   18   19   20   21   22   23   24
# 25 26 27 28 29

堆排序Heap Sort

堆Heap
  • 堆是一个完全二叉树
  • 每个非叶子结点都要大于或者等于其左右孩子结点的值称为大顶堆
  • 每个非叶子结点都要小鱼或者等于其左右孩子结点的值称为小顶堆
  • 根结点一定是大顶堆中的最大值,一定是小顶堆中的最小值

大顶堆

  • 完全二叉树的每个非叶子结点都要大于或者等于其左右孩子结点的值称为大顶堆
  • 根结点一定是大顶堆中的最大值
              90
      70              80
  40      50      10      60
30  20

小顶堆

  • 完全二叉树的每个非叶子结点都要小于或者等于其左右孩子结点的值称为小顶堆
  • 根结点一定是小顶堆中的最小值
              10
      30              20
  60      80      40      50
90  70
算法实现

1.构建完全二叉树

  • 待排序数字为 30,20,80,40,50,10,60,70,90
  • 构建一个完全二叉树存放数据,并根绝性质5对元素编号,放入顺序的数据结构中
  • 构造一个列表为[0,30,20,80,40,50,10,60,70,90]
  • 核心算法

    对于堆排序的核心算法就是堆结点的调整

    • 度数为2的结点A,如果它的左右孩子结点的最大值比它大的,将这个最大值和该结点 交换
    • 度数为1的结点A,如果它的左孩子的值大于它,则 交换
    • 如果结点A被交换到新的位置,还需要和其孩子结点重复上面的过程

单节点代码

#               30
#       20              80
#   40      50      10      60
# 70  90
import math

def print_tree(array,unit_width=2):
    '''
     i  前空格      元素间
   1 3  7=2**3-1  0 2*7+1 2*前空格+1
   2 2  3=2**2-1  7=2**3+1
   3 1  1=2**1-1  3=2**1+1
   4 0  0=2**0-1  1=2*0+1
    '''
    length = len(array)
    start = 1
    # 因为使用时前面补0了,不然应该是math.ceil(math.log2(len(array)+1)
    depth = math.ceil(math.log2(length)) # 4
    # print(depth)
    for i in range(depth, 0, -1):
        pre = 2 ** (i - 1) - 1
        print(pre * unit_width * ' ', end="")
        interval = (2 * pre + 1) * unit_width * ' ' #间隔字符
        end = start + start
        print(interval.join(map(lambda x: "{:{}}".format(x, unit_width), array[start:end])))
        start = end


# 为了和编码对应,增加一个无用的0在首位
origin = [None, 30, 20, 80, 40, 50, 10, 60, 70, 90]

def heap_adjust(i, n, array:list):
    if 2*i <= n: #一定有左孩子节点
        lchild_index = 2 * i
        max_child_index = lchild_index # 假定孩子节点中左孩子目前最大
        if lchild_index + 1 <= n and array[lchild_index + 1] > array[lchild_index]: # 存在右孩子节点
            max_child_index = lchild_index + 1

        #和自己的根比较
        if array[max_child_index] > array[i]:
            array[i], array[max_child_index] = array[max_child_index], array[i]

heap_adjust(4, 9, origin)
print_tree(origin)

#返回结果
#               30
#       20              80
#   90      50      10      60
# 70  40
#               30
#       20              80
#   40      50      10      60
# 70  90
import math

def print_tree(array,unit_width=2):
    '''
     i  前空格      元素间
   1 3  7=2**3-1  0 2*7+1 2*前空格+1
   2 2  3=2**2-1  7=2**3+1
   3 1  1=2**1-1  3=2**1+1
   4 0  0=2**0-1  1=2*0+1
    '''
    length = len(array)
    start = 1
    # 因为使用时前面补0了,不然应该是math.ceil(math.log2(len(array)+1)
    depth = math.ceil(math.log2(length)) # 4
    # print(depth)
    for i in range(depth, 0, -1):
        pre = 2 ** (i - 1) - 1
        print(pre * unit_width * ' ', end="")
        interval = (2 * pre + 1) * unit_width * ' ' #间隔字符
        end = start + start
        print(interval.join(map(lambda x: "{:{}}".format(x, unit_width), array[start:end])))
        start = end


# 为了和编码对应,增加一个无用的0在首位
origin = [None, 30, 20, 80, 40, 50, 10, 60, 70, 90]
total = len(origin) - 1

import time
def heap_adjust(i, n, array:list):
    while 2*i <= n: #一定有左孩子节点
        lchild_index = 2 * i
        max_child_index = lchild_index # 假定孩子节点中左孩子目前最大
        if lchild_index + 1 <= n and array[lchild_index + 1] > array[lchild_index]: # 存在右孩子节点
            max_child_index = lchild_index + 1

        #和自己的根比较
        if array[max_child_index] > array[i]:
            array[i], array[max_child_index] = array[max_child_index], array[i]
            i = max_child_index
        # else:
        #     break
        time.sleep(1)
        print('-' * 5)

heap_adjust(total//2, total, origin)
# heap_adjust(1, 9, origin)
print_tree(origin)
print('-' * 30)

#origin变成
# -----
#               30
#       20              80
#   90      50      10      60
# 70  40

heap_adjust(4, 9, origin)
#死循环
# ------------------------------
# -----
# -----
# -----
#               30
#       20              80
#   40      50      10      60
# 70  90
import math

def print_tree(array,unit_width=2):
    '''
     i  前空格      元素间
   1 3  7=2**3-1  0 2*7+1 2*前空格+1
   2 2  3=2**2-1  7=2**3+1
   3 1  1=2**1-1  3=2**1+1
   4 0  0=2**0-1  1=2*0+1
    '''
    length = len(array)
    start = 1
    # 因为使用时前面补0了,不然应该是math.ceil(math.log2(len(array)+1)
    depth = math.ceil(math.log2(length)) # 4
    # print(depth)
    for i in range(depth, 0, -1):
        pre = 2 ** (i - 1) - 1
        print(pre * unit_width * ' ', end="")
        interval = (2 * pre + 1) * unit_width * ' ' #间隔字符
        end = start + start
        print(interval.join(map(lambda x: "{:{}}".format(x, unit_width), array[start:end])))
        start = end


# 为了和编码对应,增加一个无用的0在首位
origin = [None, 30, 20, 80, 40, 50, 10, 60, 70, 90]
total = len(origin) - 1


def heap_adjust(i, n, array:list):
    while 2*i <= n: #一定有左孩子节点
        lchild_index = 2 * i
        max_child_index = lchild_index # 假定孩子节点中左孩子目前最大
        if lchild_index + 1 <= n and array[lchild_index + 1] > array[lchild_index]: # 存在右孩子节点
            max_child_index = lchild_index + 1

        #和自己的根比较
        if array[max_child_index] > array[i]:
            array[i], array[max_child_index] = array[max_child_index], array[i]
            i = max_child_index
        else:
            break

heap_adjust(total//2, total, origin)
# heap_adjust(1, 9, origin)
print_tree(origin)
print('-' * 30)

目前为止也只是解决了 单个结点的调整 ,下面要使用循环来依次解决比起始结点编号小的结点。

2.构建大顶堆

  • 起点结点的选择
    • 从完全二叉树的最后一个结点的双亲结点开始,即最后一层的最右边叶子结点的父结点开始
    • 结点数为n,则起始结点的编号为n//2(性质5),由于构造了一个前置为0,所以编号和列表的 索引正好重合,但是,元素个数n等于列表长度减1.
  • 下一个结点的选择
    • 从起始结点开始向左找其同层结点,到头后再从上一层的最右边结点开始继续向左逐个查找,直至根结点
              30
      20              80
  40      50      10      60
70  90

3.大顶堆的目标

确保每个非叶子结点的都比其左右孩子结点的值大

              90
      70              80
  60      10      40      50
30  20
#               30
#       20              80
#   40      50      10      60
# 70  90
import math

def print_tree(array,unit_width=2):
    '''
     i  前空格      元素间
   1 3  7=2**3-1  0 2*7+1 2*前空格+1
   2 2  3=2**2-1  7=2**3+1
   3 1  1=2**1-1  3=2**1+1
   4 0  0=2**0-1  1=2*0+1
    '''
    length = len(array)
    start = 1
    # 因为使用时前面补0了,不然应该是math.ceil(math.log2(len(array)+1)
    depth = math.ceil(math.log2(length)) # 4
    # print(depth)
    for i in range(depth, 0, -1):
        pre = 2 ** (i - 1) - 1
        print(pre * unit_width * ' ', end="")
        interval = (2 * pre + 1) * unit_width * ' ' #间隔字符
        end = start + start
        print(interval.join(map(lambda x: "{:{}}".format(x, unit_width), array[start:end])))
        start = end


# 为了和编码对应,增加一个无用的0在首位
origin = [None, 30, 20, 80, 40, 50, 10, 60, 70, 90]
total = len(origin) - 1
print_tree(origin)
print('-' * 30)

def heap_adjust(i, n, array:list):
    while 2*i <= n: #一定有左孩子节点
        lchild_index = 2 * i
        max_child_index = lchild_index # 假定孩子节点中左孩子目前最大
        if lchild_index + 1 <= n and array[lchild_index + 1] > array[lchild_index]: # 存在右孩子节点
            max_child_index = lchild_index + 1

        #和自己的根比较
        if array[max_child_index] > array[i]:
            array[i], array[max_child_index] = array[max_child_index], array[i]
            i = max_child_index
        else:
            break

# heap_adjust(total//2, total, origin)
# # heap_adjust(1, 9, origin)
# print_tree(origin)
# print('-' * 30)

# 构建大顶堆、大根堆
def max_heap(n, array:list):
    for i in range(n // 2, 0, -1):
        heap_adjust(i, n, array)
    return array

max_heap(total, origin)
print_tree(origin)
print("=" * 30)

#返回结果
#               30
#       20              80
#   40      50      10      60
# 70  90
# ------------------------------
#               90
#       70              80
#   40      50      10      60
# 20  30
# ==============================

4.排序

  • 将大顶堆根结点逐个最大值和最后一个叶子结点交换,那么最后一个叶子结点就是最大值,将这个叶子结点排除在待排序结点之外
  • 从根结点开始(新的根结点),重新调整为大顶堆后,重复上一步
img_20250228_151738.png
  • 堆顶和最后一个结点交换,并排序最后一个结点
img_20250228_151816.png
img_20250228_152802.png
#               30
#       20              80
#   40      50      10      60
# 70  90
import math

def print_tree(array,unit_width=2):
    '''
     i  前空格      元素间
   1 3  7=2**3-1  0 2*7+1 2*前空格+1
   2 2  3=2**2-1  7=2**3+1
   3 1  1=2**1-1  3=2**1+1
   4 0  0=2**0-1  1=2*0+1
    '''
    length = len(array)
    start = 1
    # 因为使用时前面补0了,不然应该是math.ceil(math.log2(len(array)+1)
    depth = math.ceil(math.log2(length)) # 4
    # print(depth)
    for i in range(depth, 0, -1):
        pre = 2 ** (i - 1) - 1
        print(pre * unit_width * ' ', end="")
        interval = (2 * pre + 1) * unit_width * ' ' #间隔字符
        end = start + start
        print(interval.join(map(lambda x: "{:{}}".format(x, unit_width), array[start:end])))
        start = end


# 为了和编码对应,增加一个无用的0在首位
origin = [None, 30, 20, 80, 40, 50, 10, 60, 70, 90]
total = len(origin) - 1
print_tree(origin)
print('-' * 30)

def heap_adjust(i, n, array:list):
    while 2*i <= n: #一定有左孩子节点
        lchild_index = 2 * i
        max_child_index = lchild_index # 假定孩子节点中左孩子目前最大
        if lchild_index + 1 <= n and array[lchild_index + 1] > array[lchild_index]: # 存在右孩子节点
            max_child_index = lchild_index + 1

        #和自己的根比较
        if array[max_child_index] > array[i]:
            array[i], array[max_child_index] = array[max_child_index], array[i]
            i = max_child_index
        else:
            break

# heap_adjust(total//2, total, origin)
# # heap_adjust(1, 9, origin)
# print_tree(origin)
# print('-' * 30)

# 构建大顶堆、大根堆
def max_heap(n, array:list):
    for i in range(n // 2, 0, -1):
        heap_adjust(i, n, array)
    return array

max_heap(total, origin)
print_tree(origin)
print("=" * 30)

# 排序
def sort(n, array:list):
    while n > 1:
        array[1], array[n] = array[n], array[1] # 堆顶和最后一个结点交换
        n -= 1
        if n == 2 and array[n] >= array[n - 1]:
            break
        heap_adjust(1, n, array)
    return array

print_tree(sort(total, origin))
print("-"*30)
print(origin[1:])


#返回结果
#               30
#       20              80
#   40      50      10      60
# 70  90
# ------------------------------
#               90
#       70              80
#   40      50      10      60
# 20  30
# ==============================
#               10
#       20              30
#   40      50      60      70
# 80  90
# ------------------------------
# [10, 20, 30, 40, 50, 60, 70, 80, 90]

总结

  • 是利用堆性质的一种选择排序,在堆顶选出最大值或者最小值
  • 时间复杂度
    • 堆排序的时间复杂度为O(nlogn)
    • 由于堆排序对原始记录的排序状态并不敏感,因此它无论是最好、最坏和平均时间复杂度均为O(nlogn)

空间复杂度

  • 只是使用了一个交换用的空间,空间复杂度就是O(1)
  • 稳定性
  • 不稳定的排序算法

高阶函数与柯里化

高阶函数

一等公民
  • 函数在Python是一等公民(First-Class Object)
  • 函数也是对象,是可调用对象
  • 函数可以作为普通变量,也可以作为函数的参数、返回值
高阶函数

高阶函数(High-order Function)

  • 数学概念 y = f(g(x))
  • 在数学和计算科学中,高阶函数应当是至少满足下面一个条件的函数
    • 接受一个或多个函数作为参数
    • 输出一个函数
# coding: utf-8
def abc():
    pass

print(abc, type(abc), abc.__name__) #abc.__name__ 表示abc的名字是什么?是一个字符串。type(abc)中abc是标识符
#: <function abc at 0x0000020652EA6280> <class 'function'> abc

观察下面的函数定义,回答问题

# coding: utf-8

def counter(base):
    def inc(step=1):
        base += step
        return base
    return inc

#1 是高阶函数吗?为什么。是,return inc 输出函数
#2 有没有嵌套函数,有闭包吗?base += step,用到base,因为base是形参,形参就是局部变量
#3 代码对吗?如果不对,怎么改。不对


def counter(base):
    def inc(step=1):
        nonlocal base #声明base不是本地变量
        base += step
        return base
    return inc

c = counter(10)
print(c())
#: 11

#4 f1 == f2?
f1 = counter(10) #f1 -> inc 所指向的对象,inc标识符随着counter调用结束而消亡
#inc所指向
f2 = counter(10)
print(f1 == f2) #函数没法比较内容,代表f1 is f2
print(f1 is f2)
#: False
#: False
sorted函数原理
练习:自定义sort函数

仿照内建函数sorted,请自行实现一个sort函数(不使用内建函数),能够为列表元素排序。

思考:通过练习,思考sorted函数的实现原理,map、filter函数的实现原理

思考

  • 内建函数sorted函数,它返回一个新列表,使用reverse可以设置升序或降序,使用key可以设置一个用于比较的函数(自定义函数也要实现这些功能)
  • 新建一个列表,遍历原列表,和新列表中的当前值依次比较,决定待插入数插入到新列表的什么位置
sorted(iterable, *, key=None, reverse=False)
# coding: utf-8

def sort(iterable, *, key=None, reverse=False):
    newlist = []
    # 下面是算法,从原列表iterable中遍历元素,每一个元素逐个插入到newlist中合适的位置
    #生成一个升序或降序的newlist
    #如果可以,先实现reverse
    #如果可以,再实现 key

    return newlist #有序的列表



def sort(iterable, *, key=None, reverse=False):  #[1, 2, 3]
    newlist = []
    # 下面是算法,从原列表iterable中遍历元素,每一个元素逐个插入到newlist中合适的位置
    #生成一个升序或降序的newlist
    #如果可以,先实现reverse
    #如果可以,再实现 key

    for x in iterable: #x 1
        # 在newlist当中,从第一个元素开始,一定要保证有序,可以认为newlist就是有序区
        for i, y in enumerate(newlist):
            pass
        else:
            newlist.append(x) #第一个元素

    return newlist #有序的列表


#核心算法
def sort(iterable, *, key=None, reverse=False):  #[1, 2, 3]
    newlist = []
    # 下面是算法,从原列表iterable中遍历元素,每一个元素逐个插入到newlist中合适的位置
    #生成一个升序或降序的newlist
    #如果可以,先实现reverse
    #如果可以,再实现 key

    for x in iterable: #x 1
        # 在newlist当中,从第一个元素开始,一定要保证有序,可以认为newlist就是有序区
        for i, y in enumerate(newlist):

            #if x > y: # 降序
            if x < y: # 升序
                newlist.insert(i,x) # [2, 1]
                break
        else:
            newlist.append(x) #第一个元素

    return newlist #有序的列表

print(sorted(reversed(range(5))))
print(sort(reversed(range(5))))
#: [0, 1, 2, 3, 4]
#: [0, 1, 2, 3, 4]


#升序和降序
def sort(iterable, *, key=None, reverse=False):  #[1, 2, 3]
    newlist = []
    # 下面是算法,从原列表iterable中遍历元素,每一个元素逐个插入到newlist中合适的位置
    #生成一个升序或降序的newlist
    #如果可以,先实现reverse
    #如果可以,再实现 key

    for x in iterable: #x 1
        # 在newlist当中,从第一个元素开始,一定要保证有序,可以认为newlist就是有序区
        for i, y in enumerate(newlist):
            comp = x > y if reverse else x < y
            #if x > y: # 降序; if x < y: # 升序
            if comp:
                newlist.insert(i,x) # [2, 1]
                break
        else:
            newlist.append(x) #第一个元素

    return newlist #有序的列表

print(sort([5,4,3,2,1], reverse=True))
print(sort([5,4,3,2,1], reverse=False))
#: [5, 4, 3, 2, 1]
#: [1, 2, 3, 4, 5]

#key比较
def sort(iterable, *, key=None, reverse=False):  #[1, 2, 3]
    newlist = []
    # 下面是算法,从原列表iterable中遍历元素,每一个元素逐个插入到newlist中合适的位置
    #生成一个升序或降序的newlist
    #如果可以,先实现reverse
    #如果可以,再实现 key

    for x in iterable: #x 1
        # 在newlist当中,从第一个元素开始,一定要保证有序,可以认为newlist就是有序区
        cx = key(x)
        for i, y in enumerate(newlist):
            #cx = key(x)
            cy = key(y)
            comp = cx > cy if reverse else cx < cy #x > y: # 降序; x < y: # 升序
            if comp:
                newlist.insert(i,x) # [2, 1]
                break
        else:
            newlist.append(x) #第一个元素

    return newlist #有序的列表

print(sort(['1', 2, '3', '4'], key=str)) #会把元素一个个str(item)
print(sort(['1', 2, '3', '4'], key=lambda x: str(x))) #def fn(x) :pass fn(item)
print(sort(['1', 2, '3', '4'], key=int)) #会把元素一个个str(item)
print(sort(['a', 1, 2, 'b', 12, 'c', 32], key=lambda x: int(x, 16) if isinstance(x, str) else x)) #整数比较
#: ['1', 2, '3', '4']
#: ['1', 2, '3', '4']
#: ['1', 2, '3', '4']
#: [1, 2, 'a', 'b', 12, 'c', 32]
内建高阶函数
排序sorted

定义 sorted(iterable, *, key=None, reverse=False) -> list ,不在赘

# coding: utf-8
lst = [1,2,3,4,5]
sorted(lst, key=lambda x:6-x) #返回新列表
lst.sort(key=lambda x: 6-x) # 就地修改
过滤filter
  • 定义 filter(function, iterable)
  • 对可迭代对象进行遍历,返回一个迭代器
  • function参数是一个参数的函数,且返回值应当是bool类型,或其返回值等效布尔值
  • function参数如果是None,可迭代对象的每一个元素自身等效布尔值
# coding: utf-8
print(list(filter(None, [1,2,3,4,5])))
#: [1, 2, 3, 4, 5]
print(list(filter(None, range(-5, 5)))) #单单是 0 不见了,为什么?
#: [-5, -4, -3, -2, -1, 1, 2, 3, 4]
#如果filter第一个参数为None,可迭代对象的所有元素,按照本身的等效True或False,如果等效False,就不要了。
print(list(filter(lambda x: x%3==0, [1,4,7,9,-3,-12]))) #单参函数和sorted的key一样
#: [9, -3, -12]

#def filter(function or None, iterable): #签名,过滤,把符合条件的留下,把不符合的筛掉,返回惰性迭代器
#    pass

  • sorted 函数处理过一个列表后,排序生成的新列表和原列表元素个数一致吗? 一致的
  • filter 处理一个列表后,迭代出的元素个数和原列表一致吗?过滤,不一定一致
映射map

map 把一个列表的所有元素,一个都不能少,从一种形式转换到另一种形式

  • 定义 map(function, *iterable) -map object
  • 对多个可迭代对象的元素,按照指定的函数进行映射
  • 返回一个迭代器
# conding: utf-8
print(next(map(str, range(5))))
#: 0
print(list(map(str, range(5))))
#: ['0', '1', '2', '3', '4']
print(list(map(lambda x: chr(x+65), range(5))))
#: ['A', 'B', 'C', 'D', 'E']
print(list(map(lambda x: x, range(5))))
#: [0, 1, 2, 3, 4]


print(list(map(lambda x,y: (x,y), 'abcde', range(5))))
print(dict(map(lambda x,y: (x,y), 'abcde', range(5))))
print(dict(map(lambda x,y: (x,str(y+100)), 'abcde', range(5))))
#: [('a', 0), ('b', 1), ('c', 2), ('d', 3), ('e', 4)]
#: {'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4}
#: {'a': '100', 'b': '101', 'c': '102', 'd': '103', 'e': '104'}

柯里化**

  • 指的是将原来接受两个参数的函数变成新的接受一个参数的函数的过程。新的函数返回一个以原有第二个参数为参数的函数
  • z = f(x, y) 转换成 z = f(x)(y)的形式

范例:

# coding: utf-8
def add(x,y):
    return x + y

print(add(4, 5))

def add(x):
    def fn(y):
        return x + y
    return fn

add(4)(5)
fn = add(4)
result = fn(5)
print(result)
# coding: utf-8
#柯里化
#add(4)(5,6)
#add(4)(5)(6)
#add(4,5)(6)

def add(x):
    def fn(y,z):
        return x+y+z 
    return fn
print(add(4)(5,6))

def add(x, y):
    def inner(z):
        return x + y +z
    return inner
print(add(4,5)(6))

def add(x):
    def inner(y):
        def inner2(z):
            return x + y + z
        return inner2
    return inner

print(add(4)(5)(6))

装饰器

由来

演化装饰前

  1. 提供一个函数logger完成记录功能
# coding: utf-8
#装饰器,用来装饰的,一般来讲装饰函数或类、

def add(x, y): # 业务功能加法,增加一个功能:能不能记录一下调用参数
    print('add function called. x={}, y={}'.format(x, y)) #非业务功能代码写在业务代码中,不好
    #侵入式代码,剥离不易
    #如果换一种思维,A函数需要记录,B函数也要记录,记录功能也不属于A和B的业务功能,而且它是A和B的公用的功能
    #如果有C函数,也需要此功能怎么办?
    #抽取出记录功能,形成一个函数
    return x + y

def add(x, y):
    return x + y
def logger(fn): #随便哪个函数fn
    #print('调用前增强功能')
    print('{} function called. x={}, y={}'.format(fn.__name__, 4, 5))
    ret = fn(4, 5)
    #print('调用后增强功能')
    return ret

logger(add)
#: add function called. x=4, y=5
  1. 解决传参问题
# coding: utf-8
#装饰器,用来装饰的,一般来讲装饰函数或类、
# 解决参数问题
def add(x, y):
    return x + y
def logger(fn, x, y): #随便哪个函数fn
    #print('调用前增强功能')
    print('{} function called. x={}, y={}'.format(fn.__name__, x, y))
    ret = fn(x, y) #add(4, 5)
    #print('调用后增强功能')
    return ret

logger(add, x=10, y=11)

#
def add(x, y, *z):
    return x + y + sum(z)
def logger(fn, *args, **kwargs): #随便哪个函数fn
    #print('调用前增强功能')
    print('{} function called. {}, {}'.format(fn.__name__, args, kwargs))
    ret = fn(*args, **kwargs) # *args 变成位置传参, **kwargs 变成关键字传参
    #print('调用后增强功能')
    return ret

logger(add, x=10, y=11)
logger(add, 10, y=11)
logger(add, 4, 5, 6, 7)
#: add function called. (), {'x': 10, 'y': 11}
#: add function called. (10,), {'y': 11}
#: add function called. (4, 5, 6, 7), {}
  1. 柯里化
# coding: utf-8
#装饰器,用来装饰的,一般来讲装饰函数或类、
#柯里化
def add(x, y, *z):
    return x + y + sum(z)
def logger(fn):
    def wrapper(*args, **kwargs): #随便哪个函数fn
        #print('调用前增强功能')
        print('{} function called. {}, {}'.format(fn.__name__, args, kwargs))
        ret = fn(*args, **kwargs) # *args 变成位置传参, **kwargs 变成关键字传参
        #print('调用后增强功能')
        return ret
    return wrapper

logger(add)(x=10, y=11)
logger(add)(10, y=11)
logger(add)(4, 5, 6, 7)
#: add function called. (), {'x': 10, 'y': 11}
#: add function called. (10,), {'y': 11}
#: add function called. (4, 5, 6, 7), {}

调用

# coding: utf-8
#装饰器,用来装饰的,一般来讲装饰函数或类、
def add(x, y):
    return x + y 
def logger(fn): # fn <= add 指向的函数对象
    def wrapper(*args, **kwargs): #随便哪个函数fn
        #print('调用前增强功能')
        print('{} function called. {}, {}'.format(fn.__name__, args, kwargs))
        ret = fn(*args, **kwargs) # *args 变成位置传参, **kwargs 变成关键字传参
        #print('调用后增强功能')
        return ret
    return wrapper

newfunc = logger(add) # newfunc == wrapper fn <=add
result = newfunc(1, 2)
print(result)
#: add function called. (1, 2), {}
#: 3

变换

# coding: utf-8
#装饰器,用来装饰的,一般来讲装饰函数或类、
def add(x, y): # add 指向 -> addr1 内存地址
    return x + y 
def logger(fn): # fn <= add 指向的函数对象 logger -> addr2 内存地址
    def wrapper(*args, **kwargs): #随便哪个函数fn
        #wrapper -> addr3 内存地址,把fn对应的函数对象困在wrapper函数的属性上
        #addr3对象的属性上记录着闭包,fn指向addr1
        #print('调用前增强功能')
        print('{} function called. {}, {}'.format(fn.__name__, args, kwargs))
        ret = fn(*args, **kwargs) # fn是原来的函数,为什么?引用计数,闭包
        #print('调用后增强功能')
        return ret
    return wrapper

#newfunc = logger(add) # newfunc == wrapper fn <=add
#result = newfunc(1, 2)
add = logger(add) #右边先做,add目前是addr1,赋值即定义 add = wrapper |addr3|
result = add(1,200)
print(result)
#: add function called. (1, 200), {}
#: 201
  1. 引用装饰器,装饰器是一种语法。
# coding: utf-8
#装饰器,用来装饰的,一般来讲装饰函数或类、

def logger(fn): # fn <= add 指向的函数对象 logger -> addr2 内存地址
    def wrapper(*args, **kwargs): #随便哪个函数fn
        #wrapper -> addr3 内存地址,把fn对应的函数对象困在wrapper函数的属性上
        #addr3对象的属性上记录着闭包,fn指向addr1
        #print('调用前增强功能')
        print('{} function called. {}, {}'.format(fn.__name__, args, kwargs))
        ret = fn(*args, **kwargs) # fn是原来的函数,为什么?引用计数,闭包
        #print('调用后增强功能')
        return ret
    return wrapper

@logger #装饰器语法,等价于 add = logger(add) # @logger称为无参装饰器
def add(x, y): # add 指向 -> addr1 内存地址
    return x + y 

#add = logger(add) #右边先做,add目前是addr1,赋值即定义 add = wrapper |addr3|
result = add(1,200)
print(result)
#: add function called. (1, 200), {}
#: 201
@标识符
标识符指向一个函数,用一个函数来装饰它下面的函数,logger函数称为装饰器函数,add称为被装饰或被包装函数
logger习惯上称为wrapper
add习惯上 wrapped
本质上来看,无参装饰器 logger实际上是 等效为一个参数的函数

无参装饰器 logger
@logger 会把它下面紧挨着的函数的标识符提上来作为它的实参 xyz = logger(xyz)
def xyz():
    pass

无参装饰器

  • 上例的装饰器语法,称为无参装饰器
  • @符号后是一个函数
  • 虽然是 无参装饰器 ,但是@后的函数本质上是 单参函数
  • 上例的logger函数是一个高阶函数

范例:日志记录装饰器实现

# coding: utf-8
import datetime
import time

def logger(fn):
    def wrapper(*args, **kwargs):
        #print('调用前增强功能')
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        #print('调用后增强功能')
        delta = (datetime.datetime.now() - start).total_seconds()
        print("Function {} took {:.2f}s.".format(fn.__name__, delta))
        return ret
    return wrapper

@logger #等价于 add = logger(add) # @logger称为无参装饰器 add = wrapper
def add(x, y):
    time.sleep(2)
    return x + y 

result = add(1,200)
print(result)
#: Function add took 2.01s.
#: 201

装饰器本质

  • 习惯上add函数被称为被包装函数wrapped,增强它的函数称为包装器、包装函数wrapper
  • 包装的目的是增强,而不是破坏,采用非侵入式代码
  • 在原代码前或后加入增强代码
  • 装饰器如同画框,装饰器可以更换,如同更换画框,画不变。也可以画框不变,更换画。也可以画框外再加其它装饰

带参装饰器

文档字符串
  • Python文档字符串Ducumentation Strings
  • 在函数(类、模块)语句块的第一行,且习惯是多行的文本,所以多用三引号
  • 文档字符串也算是合法的一条语句
  • 惯例是首字母大写,第一行写概述,空一行,第三行写详描述
  • 可以使用特殊属性 __doc__ 访问这个文档
# coding: utf-8
def add(x,y):
    """Add function

    x int:
    y int:
    return int:
    """

    return x + y

print("{}'s doc = {}".format(add.__name__, add.__doc__))
print(help(add))


#add's doc = Add function
#
#    x int:
#    y int:
#    return int:
#    
#Help on function add in module __main__:
#
#add(x, y)
#    Add function
#    
#    x int:
#    y int:
#    return int:
#
#None
装饰器的文档字符串问题

范例:

# coding: utf-8
import datetime
import time

def logger(fn):
   def wrapper(*args, **kwargs): 
      """Wrapper function +++"""
      start = datetime.datetime.now()
      ret = fn(*args, **kwargs)
      delta = (datetime.datetime.now() - start).total_seconds()
      print("Function {} took {}s".format(fn.__name__, delta))

      return ret
   return wrapper

@logger
def add(x, y): #add = logger(add) => add = wrapper
   """Add function~~~"""
   time.sleep(2)
   return x + y

print(add.__name__, add.__doc__)
#: wrapper Wrapper function +++

被装饰后,发现add的函数名和文档都变了。如何解决?

函数也是对象,特殊属性也是属性,也可以被覆盖。现在访问add函数实际上是wrapper函数,所以使用原来定义的add函数的名称和文档属性覆盖wrapper的对应属性就可以了。

# coding: utf-8
import datetime
import time

#伪装 函数名字和文档

def logger(fn):
   def wrapper(*args, **kwargs): 
      """Wrapper function +++"""
      start = datetime.datetime.now()
      ret = fn(*args, **kwargs)
      delta = (datetime.datetime.now() - start).total_seconds()
      print("Function {} took {}s".format(fn.__name__, delta))

      return ret
   print(wrapper.__name__, '***', wrapper.__doc__)
   wrapper.__name__ = fn.__name__
   wrapper.__doc__ = fn.__doc__
   print(wrapper.__name__, '###', wrapper.__doc__)
   return wrapper

@logger
def add(x, y): #add = logger(add) => add = wrapper
   """Add function~~~"""
   time.sleep(2)
   return x + y

print(add.__name__, add.__doc__)
#: wrapper *** Wrapper function +++
#: add ### Add function~~~
#: add Add function~~~



#伪装 函数名字和文档

def logger(fn): #fn 被包装函数wrapped
   def wrapper(*args, **kwargs):  # 包装函数wrapper
      """Wrapper function +++"""
      start = datetime.datetime.now()
      ret = fn(*args, **kwargs)
      delta = (datetime.datetime.now() - start).total_seconds()
      print("Function {} took {}s".format(fn.__name__, delta))

      return ret

   def copy_properties(dst, src):
      print(dst.__name__, '***', dst.__doc__)
      dst.__name__ = src.__name__
      dst.__doc__ = src.__doc__
      #其他属性覆盖略去
      print(dst.__name__, '###', dst.__doc__)

   copy_properties(wrapper, fn)
   return wrapper

@logger
def add(x, y): #add = logger(add) => add = wrapper
   """Add function~~~"""
   time.sleep(2)
   return x + y

print(add.__name__, add.__doc__)
#: wrapper *** Wrapper function +++
#: add ### Add function~~~
#: add Add function~~~



#属性拷贝通用函数

def copy_properties(dst, src):
   print(dst.__name__, '***', dst.__doc__)
   dst.__name__ = src.__name__
   dst.__doc__ = src.__doc__
   #其他属性覆盖略去
   print(dst.__name__, '###', dst.__doc__)

def logger(fn): #fn 被包装函数wrapped
   def wrapper(*args, **kwargs):  # 包装函数wrapper
      """Wrapper function +++"""
      start = datetime.datetime.now()
      ret = fn(*args, **kwargs)
      delta = (datetime.datetime.now() - start).total_seconds()
      print("Function {} took {}s".format(fn.__name__, delta))

      return ret

   copy_properties(wrapper, fn)
   return wrapper

@logger
def add(x, y): #add = logger(add) => add = wrapper
   """Add function~~~"""
   time.sleep(2)
   return x + y

print(add.__name__, add.__doc__)
带参装饰器

柯里化

# coding: utf-8
import datetime
import time

def copy_properties(dst): #柯里化
   def _copy(src):
      dst.__name__ = src.__name__
      dst.__doc__ = src.__doc__
      #其他属性覆盖略去
   return _copy

def logger(fn): #fn 被包装函数wrapped
   #@copy_properties(fn) #带参装饰器
   def wrapper(*args, **kwargs):  # 包装函数wrapper
      """Wrapper function +++"""
      start = datetime.datetime.now()
      ret = fn(*args, **kwargs)
      delta = (datetime.datetime.now() - start).total_seconds()
      print("Function {} took {}s".format(fn.__name__, delta))

      return ret

   #copy_properties(wrapper, fn)
   copy_properties(wrapper)(fn) # _copy(fn)
   return wrapper

@logger
def add(x, y): #add = logger(add) => add = wrapper
   """Add function~~~"""
   time.sleep(2)
   return x + y

print(add.__name__, add.__doc__)
#: add Add function~~~

带参装饰器

# coding: utf-8
import datetime
import time

#def copy_properties(dst): #柯里化
#   def _copy(src):
#      dst.__name__ = src.__name__
#      dst.__doc__ = src.__doc__
#      #其他属性覆盖略去
#   return _copy

#def copy_properties(src): #柯里化,适应装饰器的使用调整一下形参

#   def _copy(dst):
#      dst.__name__ = src.__name__
#      dst.__doc__ = src.__doc__
#      #其他属性覆盖略去
#   return _copy

def copy_properties(src): #柯里化,适应装饰器的使用调整一下形参
   def _copy(dst):
      dst.__name__ = src.__name__
      dst.__doc__ = src.__doc__
      return dst  #这一句返回值特别重要
      #其他属性覆盖略去
   return _copy
def logger(fn): #fn 被包装函数wrapped
   #@copy_properties(fn) #带参装饰器 #等价于 wrapper = copy_properties(wrapper)(fn)
   @copy_properties(fn) #带参装饰器 #等价于 wrapper = copy_properties(fn)(wrapper)
   #装饰器语法是把下面的函数的标识符提上来作为实参,即copy_properties(fn)(wrapper) -> _copy(wrapper) => wrapper = wrapper
   def wrapper(*args, **kwargs):  # 包装函数wrapper
      """Wrapper function +++"""
      start = datetime.datetime.now()
      ret = fn(*args, **kwargs)
      delta = (datetime.datetime.now() - start).total_seconds()
      print("Function {} took {}s".format(fn.__name__, delta))

      return ret

   #copy_properties(wrapper)(fn) # _copy(fn)
   #copy_properties(fn)(wrapper) # _copy(wrapper)
   return wrapper

@logger
def add(x, y): #add = logger(add) => add = wrapper
   """Add function~~~"""
   time.sleep(2)
   return x + y

print(add.__name__, add.__doc__)

#报错
#Traceback (most recent call last):
#  File "<stdin>", line 41, in <module>
#AttributeError: 'NoneType' object has no attribute '__name__'
#因为@copy_properties(fn) 即 copy_properties(fn)(wrapper) -> _copy(wrapper) => wrapper = wrapper
#_copy(wrapper)返回None,None是没有__name__和__doc__的
#解决 _copy(dst): 返回 return dst

@copy_properties(fn) 这种在装饰器后面跟着参数的装饰器称为 带参装饰器

logger设定一个阈值,对执行时长超过阈值的记录一下。

# coding: utf-8
import datetime
import time

def copy_properties(src): #柯里化
   def _copy(dst):
      dst.__name__ = src.__name__
      dst.__doc__ = src.__doc__
      return dst  #这一句返回值特别重要
      #其他属性覆盖略去
   return _copy
def logger(fn): #fn 被包装函数wrapped
   @copy_properties(fn) #带参装饰器 #等价于 wrapper = copy_properties(fn)(wrapper)
   #copy_properties(fn)(wrapper) -> _copy(wrapper) => wrapper = wrapper
   def wrapper(*args, **kwargs):  # 包装函数wrapper
      """Wrapper function +++"""
      start = datetime.datetime.now()
      ret = fn(*args, **kwargs)
      delta = (datetime.datetime.now() - start).total_seconds()
      duration = 5 # s
      if delta > duration:
         print("Function {} took {}s. Slow".format(fn.__name__, delta))
      else:
         print("Function {} took {}s. Fast".format(fn.__name__, delta))
      return ret
   #wrapper = copy_properties(fn)(wrapper) # _copy(wrapper)
   return wrapper

@logger
def add(x, y): #add = logger(add) => add = wrapper
   """Add function~~~"""
   time.sleep(6)
   return x + y

add(4, 5)
#: Function add took 6.012711s. Slow

#将阈值提出来,柯里化
def copy_properties(src): #柯里化
   def _copy(dst):
      dst.__name__ = src.__name__
      dst.__doc__ = src.__doc__
      return dst  #这一句返回值特别重要
      #其他属性覆盖略去
   return _copy

def logger(duration):
   def _logger(fn): #fn 被包装函数wrapped
      @copy_properties(fn) #带参装饰器 #等价于 wrapper = copy_properties(fn)(wrapper)
      #copy_properties(fn)(wrapper) -> _copy(wrapper) => wrapper = wrapper
      def wrapper(*args, **kwargs):  # 包装函数wrapper
         """Wrapper function +++"""
         start = datetime.datetime.now()
         ret = fn(*args, **kwargs)
         delta = (datetime.datetime.now() - start).total_seconds()
         #duration = 5 # s
         #if delta > duration:
         #   print("Function {} took {}s. Slow".format(fn.__name__, delta))
         #else:
         #   print("Function {} took {}s. Fast".format(fn.__name__, delta))
         print("Function {} took {}s. {}".format(fn.__name__, delta, "Slow" if delta > duration else "Fast"))

         return ret
      #wrapper = copy_properties(fn)(wrapper) # _copy(wrapper)
      return wrapper
   return _logger

@logger(5) # add = logger(5)(add) -> add = _logger(add) -> add = wrapper
def add(x, y): #add = logger(add) => add = wrapper
   """Add function~~~"""
   time.sleep(2)
   return x + y

add(4, 5)
#: Function add took 6.005283s. Slow

如果超出阈值,记录,为了灵活,我们对超出阈值的信息使用一个函数记录

# coding: utf-8
import datetime
import time

def copy_properties(src): #柯里化
   def _copy(dst):
      dst.__name__ = src.__name__
      dst.__doc__ = src.__doc__
      return dst  #这一句返回值特别重要
      #其他属性覆盖略去
   return _copy

def logger(duration, output=lambda name, delta: print('{} took {:.2f}s. slow'.format(name, delta))):
   def _logger(fn): #fn 被包装函数wrapped
      @copy_properties(fn) #带参装饰器 #等价于 wrapper = copy_properties(fn)(wrapper)
      #copy_properties(fn)(wrapper) -> _copy(wrapper) => wrapper = wrapper
      def wrapper(*args, **kwargs):  # 包装函数wrapper
         """Wrapper function +++"""
         start = datetime.datetime.now()
         ret = fn(*args, **kwargs)
         delta = (datetime.datetime.now() - start).total_seconds()
         #如果超出阈值,记录,为了灵活,我们对超出阈值的信息使用一个函数记录
         if delta > duration:
            output(fn.__name__, delta) #发网络、记录文件、控制台。函数名、delta

         return ret
      #wrapper = copy_properties(fn)(wrapper) # _copy(wrapper)
      return wrapper
   return _logger

@logger(1) # add = logger(1, output)(add) -> add = _logger(add) -> add = wrapper
def add(x, y): #add = logger(add) => add = wrapper
   """Add function~~~"""
   time.sleep(2)
   return x + y

add(4, 5)
#: add took 2.01s. slow
属性更新

上例中copy_properties是通用功能,标准库中functools已经提供了

functools.update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)

  • 类似copy_properties功能
  • wrapper包装函数、被更新者,wrapped被包装函数、数据源
  • 元组WRAPPER_ASSIGNMENTS中要被覆盖的属性,有模块名 __module__ ,名称 __name__ ,限定名 __qualname__ ,文档 __doc__ ,参数注解 __annotations__
  • 元组WRAPPER_UPDATES中要被更新的属性, __dict__ 属性字典
  • 增加一个 __wrapped__ 属性,保留着wrapped函数
# coding: utf-8
import datetime
import time
from functools import update_wrapper

def copy_properties(src): #柯里化
   def _copy(dst):
      dst.__name__ = src.__name__
      dst.__doc__ = src.__doc__
      return dst  #这一句返回值特别重要
      #其他属性覆盖略去
   return _copy

def copy_properties(wrapper, wrapped): #柯里化 update_wrapper(wrapper, wrapped,
   assigned = ('__name__', '__doc__')
   wrapper.__name__ = wrapped.__name__
   wrapper.__doc__ = wrapped.__doc__

   wrapper.__wrapped__ = wrapped #在函数对象上自定义一个特殊属性,记录被包装者fn

   return wrapper 

def logger(duration, output=lambda name, delta: print('{} took {:.2f}s. slow'.format(name, delta))):
   def _logger(fn): #fn 被包装函数wrapped
      #@copy_properties(fn) #带参装饰器 #等价于 wrapper = copy_properties(fn)(wrapper)
      #opy_properties(fn)(wrapper) -> _copy(wrapper) => wrapper = wrapper
      def wrapper(*args, **kwargs):  # 包装函数wrapper
         """Wrapper function +++"""
         start = datetime.datetime.now()
         ret = fn(*args, **kwargs)
         delta = (datetime.datetime.now() - start).total_seconds()
         #如果超出阈值,记录,为了灵活,我们对超出阈值的信息使用一个函数记录
         if delta > duration:
            output(fn.__name__, delta) #发网络、记录文件、控制台。函数名、delta

         return ret
      #wrapper = copy_properties(fn)(wrapper) # _copy(wrapper)
      update_wrapper(wrapper, fn)
      return wrapper
   return _logger

@logger(1) # add = logger(1, output)(add) -> add = _logger(add) -> add = wrapper
def add(x, y): #add = logger(add) => add = wrapper
   """Add function~~~"""
   time.sleep(2)
   return x + y

print(add.__name__, add.__doc__)
#: add Add function~~~
#add(4, 5)

functools模块提供了一个wraps装饰器函数,本质上调用的是update_wrapper,它就是一个属性复制函数

wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)

  • 缺少wrapper
# coding: utf-8
import datetime
import time
from functools import update_wrapper, wraps

def copy_properties(src): #wraps 偏函数实现
   def _copy(dst):
      dst.__name__ = src.__name__
      dst.__doc__ = src.__doc__
      return dst  #这一句返回值特别重要
      #其他属性覆盖略去
   return _copy

def copy_properties(wrapper, wrapped): #柯里化 update_wrapper(wrapper, wrapped,
   assigned = ('__name__', '__doc__')
   wrapper.__name__ = wrapped.__name__
   wrapper.__doc__ = wrapped.__doc__

   wrapper.__wrapped__ = wrapped #在函数对象上自定义一个特殊属性,记录被包装者fn

   return wrapper 

def logger(duration, output=lambda name, delta: print('{} took {:.2f}s. slow'.format(name, delta))):
   def _logger(fn): #fn 被包装函数wrapped
      #@copy_properties(fn) #带参装饰器 #等价于 wrapper = copy_properties(fn)(wrapper)
      #opy_properties(fn)(wrapper) -> _copy(wrapper) => wrapper = wrapper
      @wraps(fn) #wraps = wraps(fn)(wrapper) 也就是wraps(fn)必须返回等效于一个单参函数
      def wrapper(*args, **kwargs):  # 包装函数wrapper
         """Wrapper function +++"""
         start = datetime.datetime.now()
         ret = fn(*args, **kwargs)
         delta = (datetime.datetime.now() - start).total_seconds()
         #如果超出阈值,记录,为了灵活,我们对超出阈值的信息使用一个函数记录
         if delta > duration:
            output(fn.__name__, delta) #发网络、记录文件、控制台。函数名、delta

         return ret
      #wrapper = copy_properties(fn)(wrapper) # _copy(wrapper)
      #update_wrapper(wrapper, fn)
      return wrapper
   return _logger

@logger(1) # add = logger(1, output)(add) -> add = _logger(add) -> add = wrapper
def add(x, y): #add = logger(add) => add = wrapper
   """Add function~~~"""
   time.sleep(2)
   return x + y

print(add.__name__, add.__doc__)
#: add Add function~~~
#add(4, 5)
总结
  • @之后不是一个单独的标识符,是一个函数调用
  • 函数调用的返回值又是一个函数,此函数是一个无参装饰器
  • 带参装饰器,可以有任意个参数
    • @func()
    • @func(1)
    • @func(1,2)

装饰器进阶

# coding: utf-8
import datetime
from functools import wraps

def logger(fn): #logger全局标识符,一个函数对象
   @wraps(fn) #wrapper = wraps(fn)(wrapper) => wrapper = wrapper
   def wrapper(*args, **kwargs):  #logger执行时,临时创建内部函数对象,如同局部变量一样
      """Wrapper function +++"""
      start = datetime.datetime.now()
      ret = fn(*args, **kwargs)
      delta = (datetime.datetime.now() - start).total_seconds()
      print("Function {} took {}s".format(fn.__name__, delta))
      return ret
   return wrapper

@logger #add = logger(add) => add = wrapper1 此wrapper非彼wrapper
def add(x, y): #内存中产生一个全局对象add函数对象
   """Add function~~~"""
   return x + y

@logger #sub = logger(sub) => sub = wrapper2 # 函数的每一次调用之间没什么关系
def sub(x, y):
   "Sub function +++"
   return x - y

print(add.__name__, sub.__name__)
#: add sub
  • add函数、sub函数执行过吗?logger什么时候执行?logger执行过几次?
    • add,sub函数没执行过,打印属性不代表调用,add(4,5)这种才是调用。 @logger时执行,logger 执行过2次
  • wraps装饰器执行过几次? 2次
  • wrapper的 __name__ 等属性被覆盖过几次? 2次,各是个的
  • add.__name__ 打印什么名称? add
  • sub.__name__ 打印什么名称? sub

参考:

装饰器超详细教学,用尽毕生所学给你解释清楚,以后再也不迷茫了!

注解annotation

Python 是动态语言,变量可以随时被赋值并改变类型,也就是说Python的变量是运行时决定的。

# coding: utf-8
def add(x, y):
    return x + y

print(add(4, 5))
print(add('ab', 'cd'))
print(add([10], [11]))

print(add(4, 'abc')) #不到运行时,无法判断类型是否正确

动态语言缺点:

  • 难发现:由于不能做任何类型检查,往往到了运行时问题才显现出来,或到了线上运行时才暴露出来
  • 难使用:函数使用者看到函数时,并不知道函数设计者的意图,如果没有详尽的文档,使用者只能猜测数据的类型。对于一个新的API,使用都往往不知道该怎么使用,对于返回的类型更是不知道该怎么使用

动态类型对类型的约束不强,在小规模开发危害不大,但是随着python的广泛使用,这种缺点确实对大项目的开发危害非常大。

如何解决这种问题?

  • 文档字符串。对函数、类、模块使用详尽的使用描述、举例,让使用者使用帮助就能知道使用方式。但是,大多数项目管理不严格,可能文档不全,或者项目发生变动后,文档没有更新等等。
  • 类型注解:函数注解、变量注解

函数注解

# coding: utf-8
def add(x:int, y) -> int:  #函数注解也是非强制性类型要求,仅仅是一种声明
    """

    :param x:
    :param y:
    :return:
    """

    return x + y

help(add)
print(add(4,5))
print(add((4,), (5,)))

函数注解

  • 3.5版本引入
  • 对函数的形参和返回值的类型说明
  • 只是对函数形参类型、返回值类型做的辅助的说明,非强制类型约束
  • 第三方工具,例如Pycharm就可以根据类型注解进行代码分析,发现隐藏的Bug
  • 函数注解存放在函数的属性 __annotations__ 中,字典类型

类型注解

# coding: utf-8
#3.6

i:int = 123
i = 'abc' #非强制约束
s:str = 200
k:str = 300 #非强制
print(i,s,k)
  • 3.6版本引入
  • 对变量类型说明,非强制约束
  • 第三方工具可以进行类型分析和推断

类型检查

函数传参经常传错,如何检查?

# coding: utf-8
def add(x:int, y) -> int:  #函数注解也是非强制性类型要求,仅仅是一种声明
    """

    :param x:
    :param y:
    :return:
    """

    # 这种检查是通用的功能,是n个函数都可以拥有的功能,而且检查是非业务代码---典型的装饰器该做的
    if isinstance(x, int) and isinstance(y, int): #检查方案,硬编码
        r = x + y
        return r 
    else:
        print('error')
        raise TypeError #built-in 加载时,加载内建函数、异常类

print(add('abc', 'xyz'))

可以在函数内部写isinstance来判断参数类型是否正确,但是检查可以看做不是业务代码,写在里面就是侵入式代码。那如何更加灵活的检查呢?

  • 非侵入代码
  • 动态获取待检查的函数的参数类型注解
  • 当函数调用传入实参时,和类型注解比对

能否使用函数的 __annotations__ 属性吗?虽然python 3.6之后,字典记录了录入序,但是我们还是要认为字典字段是无有顺序的。那如何和按位置传实参对应呢?

使用inspect模块

inspect模块

# coding: utf-8
import inspect

def add(x, y):
    return x + y

print(inspect.isfunction(add))
print(inspect.isgenerator(i for i in range(10)))
print(inspect.isclass(add))
print(inspect.isbuiltin(print))
print(inspect.ismodule(inspect))
print(inspect.signature(add)) #获取函数对象的签名信息
#: True
#: True
#: False
#: True
#: True
#: (x, y)


def add(x:int, y:int) -> int:
    return x + y

sig = inspect.signature(add) #获取签名
print(sig)
print(add.__annotations__)
print(sig.return_annotation) #返回值注解
#: (x: int, y: int) -> int
#: {'x': <class 'int'>, 'y': <class 'int'>, 'return': <class 'int'>}
#<class 'int'>

print(sig.parameters) #属性,所有参数 #OrderedDict有序字典,记录了kv的录入序
#(x形参的名称字符串, Parameter类型的对象)
#OrderedDict([('x', <Parameter "x: int">), ('y', <Parameter "y: int">)])

for k,v in sig.parameters.items():
    print(type(k), k)
    print(type(v), v)
#<class 'str'> x
#<class 'inspect.Parameter'> x: int
#<class 'str'> y
#<class 'inspect.Parameter'> y: int
  • inspect.isfunction(add),是否是函数
  • inspect.ismethod(add),是否是类的方法
  • inspect.isgenerator(add),是否是生成器对象
  • inspect.isgeneratorfunction(add),是否是生成器函数
  • inspect.isclass(add),是否是类
  • inspect.ismodule(inspect),是否是模块
  • inspect.isbuiltin(print),是否是内建对象

还有很多is函数,需要时查阅inspect模块帮助

inspect.signature(callable, *, follow_wrapped=True)

  • 获取可调用对象的签名
  • 3.5增加 follow_wrapped ,如果使用functools的wraps或 update_wrapperfollow_wrapped 为True跟进被包装函数的 __wrapped__ ,也就是去获取真正的被包装函数的签名

inspect.Parameter

  • 4个属性
    • name,参数名,字符串
    • default,缺少值
    • annotation,类型注解
    • kind,类型
      • POSITIONAL_ONLY ,只接受仅位置传参
      • POSITIONAL_OR_KEYWORD ,可以接受关键字或位置传参
      • VAR_POSITIONAL ,可变位置参数,对应 *args
      • KEYWORD_ONLY ,对应 * 或者 *args之后出现的非可变关键字形参,只接受关键传参
      • VAR_KEYWORD ,可变关键字参数,对应 **kwargs
    • empty,特殊类,用来标记default属性或annotation属性为空
# coding: utf-8
#3.8
def a(x:int, /, y:int=5, *args, m:int=10, n:str, **kwargs):
    print(x,y,m,n,args,kwargs)

import inspect

sig = inspect.signature(a)
print(sig)

for k,v in sig.parameters.items():
    print(k, v.name, v.default, v.annotation, type(v.annotation), v.kind, sep='\t')
#: (x: int, /, y: int = 5, *args, m: int = 10, n: str, **kwargs)
#: x    x       <class 'inspect._empty'>        <class 'int'>   <class 'type'>  POSITIONAL_ONLY
#: y    y       5       <class 'int'>   <class 'type'>  POSITIONAL_OR_KEYWORD
#: args args    <class 'inspect._empty'>        <class 'inspect._empty'>        <class 'type'>  VAR_POSITIONAL
#: m    m       10      <class 'int'>   <class 'type'>  KEYWORD_ONLY
#: n    n       <class 'inspect._empty'>        <class 'str'>   <class 'type'>  KEYWORD_ONLY
#: kwargs       kwargs  <class 'inspect._empty'>        <class 'inspect._empty'>        <class 'type'>  VAR_KEYWORD

参数类型检查

有如下函数

# coding: utf-8
def add(x, y:int=7) -> int:
    return x + y

add(4, 5)
add('abc', 'xyz')

请检查用户的输入是否符合参数类型注解的要求

分析:

  • 调用时,用户才会传实参,才能判断实参是否符合类型要求
  • 调用时,让用户感觉上还是调用原函数
  • 如果类型不符,提示用户

先实现对add函数的参数类型提取

# coding: utf-8
import inspect


def check(fn):
    print(fn.__annotations__) #这是字典,无序

def add(x:int, y:int) -> int:  #函数注解也是非强制性类型要求,仅仅是一种声明
    """

    :param x:
    :param y:
    :return:
    """

    # 这种检查是通用的功能,是n个函数都可以拥有的功能,而且检查是非业务代码---典型的装饰器该做的
    if isinstance(x, int) and isinstance(y, int): #检查方案,硬编码
        r = x + y
        return r 
    else:
        print('error')
        raise TypeError #built-in 加载时,加载内建函数、异常类

sig = inspect.signature(add)
params = sig.parameters #录入序
print(params)
#: OrderedDict([('x', <Parameter "x: int">), ('y', <Parameter "y: int">)])
for k,v in params.items():
    print(type(k), k)
    print(type(v), v.name, v.default, v.kind, v.annotation)
#: <class 'str'> x
#: <class 'inspect.Parameter'> x <class 'inspect._empty'> POSITIONAL_OR_KEYWORD <class 'int'>
#: <class 'str'> y
#: <class 'inspect.Parameter'> y <class 'inspect._empty'> POSITIONAL_OR_KEYWORD <class 'int'>

#print(add('abc', 'xyz'))
# coding: utf-8
import inspect


def check(fn):
    sig = inspect.signature(add)
    params = sig.parameters #录入序
    print(params)
    #: OrderedDict([('x', <Parameter "x: int">), ('y', <Parameter "y: int">)])
    for k,v in params.items():
        print(type(k), k)
        print(type(v), v.name, v.default, v.kind, v.annotation)

def add(x:int, y:int) -> int:  #函数注解也是非强制性类型要求,仅仅是一种声明
    # 这种检查是通用的功能,是n个函数都可以拥有的功能,而且检查是非业务代码---典型的装饰器该做的
    if isinstance(x, int) and isinstance(y, int): #检查方案,硬编码
        r = x + y
        return r 
    else:
        print('error')
        raise TypeError #built-in 加载时,加载内建函数、异常类

check(add)
print(add(4, 5))
#: OrderedDict([('x', <Parameter "x: int">), ('y', <Parameter "y: int">)])
#: <class 'str'> x
#: <class 'inspect.Parameter'> x <class 'inspect._empty'> POSITIONAL_OR_KEYWORD <class 'int'>
#: <class 'str'> y
#: <class 'inspect.Parameter'> y <class 'inspect._empty'> POSITIONAL_OR_KEYWORD <class 'int'>
#: 9

先解决按位置传参

# coding: utf-8
import inspect
from functools import wraps


def check(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):

        sig = inspect.signature(fn)
        params = sig.parameters #录入序
        print(params)
        #: OrderedDict([('x', <Parameter "x: int">), ('y', <Parameter "y: int">)])
        for k,v in params.items():
            print(type(k), k)
            print(type(v), v.name, v.default, v.kind, v.annotation)

        print(args, kwargs)
        #OrereDict(
        #[('x', <Parameter "x: int">), ('y', <Parameter "y: int">)])
        #(4, 5)
        #优先解决 按位置传参,也就是args
        values = list(params.values()) #[x, y]
        print(values)
        for i,x in enumerate(args):
            if isinstance(x, values[i].annotation):
                print(values[i].name, x, 'ok')
            else:
                print(values[i].name, x, 'error')

        ret = fn(*args, **kwargs)
        return ret
    return wrapper

@check #add = check(add)
def add(x:int, y:int) -> int:  #函数注解也是非强制性类型要求,仅仅是一种声明
    # 这种检查是通用的功能,是n个函数都可以拥有的功能,而且检查是非业务代码---典型的装饰器该做的
    r = x + y
    return r 

add(4, 5) #感觉上就跟使用原来的add一样,logger的使用,wrapper(4, 5) => add(4, 5)
#: OrderedDict([('x', <Parameter "x: int">), ('y', <Parameter "y: int">)])
#: <class 'str'> x
#: <class 'inspect.Parameter'> x <class 'inspect._empty'> POSITIONAL_OR_KEYWORD <class 'int'>
#: <class 'str'> y
#: <class 'inspect.Parameter'> y <class 'inspect._empty'> POSITIONAL_OR_KEYWORD <class 'int'>
#: (4, 5) {}
#: [<Parameter "x: int">, <Parameter "y: int">]
#: x 4 ok
#: y 5 ok

解决关键字传参

# coding: utf-8
import inspect
from functools import wraps


def check(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):

        sig = inspect.signature(fn)
        params = sig.parameters #录入序
        print(params)
        #: OrderedDict([('x', <Parameter "x: int">), ('y', <Parameter "y: int">)])
        for k,v in params.items():
            print(type(k), k)
            print(type(v), v.name, v.default, v.kind, v.annotation)

        print(args, kwargs)
        #OrereDict(
        #[('x', <Parameter "x: int">), ('y', <Parameter "y: int">)])
        #(4, 5)
        #优先解决 按位置传参,也就是args
        values = list(params.values()) #[x, y]
        print(values)
        #: [<Parameter "x: int">, <Parameter "y: int">]
        for i,x in enumerate(args):
            if isinstance(x, values[i].annotation):
                print(values[i].name, x, 'ok')
            else:
                print(values[i].name, x, 'error')

        #关键字传参 {'x': 4, 'y': 5} kwargs params
        for k, v in kwargs.items():
            if isinstance(v, params[k].annotation):
                print(params[k].name, v, 'ok')
            else:
                print(params[k].name, v, 'error')

        ret = fn(*args, **kwargs)
        return ret
    return wrapper

@check #add = check(add)
def add(x:int, y:int) -> int:  #函数注解也是非强制性类型要求,仅仅是一种声明
    # 这种检查是通用的功能,是n个函数都可以拥有的功能,而且检查是非业务代码---典型的装饰器该做的
    r = x + y
    return r 

#add(4, 5) #感觉上就跟使用原来的add一样,logger的使用,wrapper(4, 5) => add(4, 5)
#: OrderedDict([('x', <Parameter "x: int">), ('y', <Parameter "y: int">)])
#: <class 'str'> x
#: <class 'inspect.Parameter'> x <class 'inspect._empty'> POSITIONAL_OR_KEYWORD <class 'int'>
#: <class 'str'> y
#: <class 'inspect.Parameter'> y <class 'inspect._empty'> POSITIONAL_OR_KEYWORD <class 'int'>
#: (4, 5) {}
#: [<Parameter "x: int">, <Parameter "y: int">]
#: x 4 ok
#: y 5 ok

add(y=5, x=4)
#: OrderedDict([('x', <Parameter "x: int">), ('y', <Parameter "y: int">)])
#: <class 'str'> x
#: <class 'inspect.Parameter'> x <class 'inspect._empty'> POSITIONAL_OR_KEYWORD <class 'int'>
#: <class 'str'> y
#: <class 'inspect.Parameter'> y <class 'inspect._empty'> POSITIONAL_OR_KEYWORD <class 'int'>
#: () {'y': 5, 'x': 4}
#: [<Parameter "x: int">, <Parameter "y: int">]
#: y 5 ok
#: x 4 ok

#add(y='xyz', x='abc')
#: y xyz error
#: x abc error

add函数中x没有函数注解时

# coding: utf-8
import inspect
from functools import wraps


def check(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):

        sig = inspect.signature(fn)
        params = sig.parameters #录入序
        values = list(params.values()) #[x, y]
        print(values)
        #: [<Parameter "x: int">, <Parameter "y: int">]
        for i,x in enumerate(args):
            if values[i].annotation is not values[i].empty and not isinstance(x, values[i].annotation):
                print(values[i].name, x, 'error')

        for k, v in kwargs.items():
            if params[k].annotation is not inspect._empty and not isinstance(v, params[k].annotation):
                print(params[k].name, v, 'error')

        ret = fn(*args, **kwargs)
        #返回值检查
        return ret
    return wrapper

@check #add = check(add)
def add(x, y:int) -> int:  #函数注解也是非强制性类型要求,仅仅是一种声明
    # 这种检查是通用的功能,是n个函数都可以拥有的功能,而且检查是非业务代码---典型的装饰器该做的
    r = x + y
    return r 

#add(4, 5) #感觉上就跟使用原来的add一样,logger的使用,wrapper(4, 5) => add(4, 5)
#add(y=5, x=4)
#add(y='xyz', x='abc')

add(4, y=5)

functools模块

reduce

  • functools.reduce(function, iterable[, initial])
  • 就是减少的意思
  • 初始值没有提供就在可迭代对象中取一个
# coding: utf-8
from functools import reduce

print(sum) #: <built-in function sum> sum对可迭代对象求和
s = sum(range(10))
print(s)

s = reduce(lambda x: x, range(10))
print(s) #TypeError: <lambda>() takes 1 positional argument but 2 were given

从上面的异常推断lambda应该2个参数

# coding: utf-8
from functools import reduce

s = reduce(lambda x,y: print(x,y), range(10))
print(s)#把上一次结果带入下一次,但第一次例外
#0 1
#None 2
#None 3
#None 4
#None 5
#None 6
#None 7
#None 8
#None 9
#None #返回值为None

s = reduce(lambda x,y: 100, range(10))
print(s)
#100


s = reduce(lambda x,y: (x,y), range(10))
print(s)
#(((((((((0, 1), 2), 3), 4), 5), 6), 7), 8), 9)

def fn(x, y):
    print(x, y)
    return x + y

s = reduce(fn, range(10))
print(s)
#0 1
#1 2
#3 3
#6 4
#10 5
#15 6
#21 7
#28 8
#36 9
#45


s = reduce(fn, range(10), 100) # 100 是初值给x
print(s)
#100 0
#100 1
#101 2
#103 3
#106 4
#110 5
#115 6
#121 7
#128 8
#136 9
#145

sum只能求和,reduce能做更加复杂的迭代计算

范例:5的阶乘

# coding: utf-8
from functools import reduce 
print(reduce(lambda x,y: x*y, range(1,6)))
#: 120

partial

偏函数

  • 把函数部分参数固定下来,相当于为部分的参数添加固定的默认值,形成一个新函数,并返回这个新函数
  • 这个新函数是对原函数的封装
# coding: utf-8
def add(x, y):
    print(x, y)
    return x + y

from functools import partial

newfunc = partial(add, y=5) #相当于固定了y的参数,相当于给了y缺少值

print(newfunc(4), newfunc(x=6), newfunc(y=10, x=11), newfunc(11, y=10))
#: 4 5
#: 6 5
#: 11 10
#: 11 10
#: 9 11 21 21

#print(newfunc(3, 4)) #相当于add(3,4,y=5)
#TypeError: add() got multiple values for argument 'y'

#什么会报错? 查看函数签名
import inspect
print(inspect.signature(add))
print(inspect.signature(newfunc))
#: (x, y)
#: (x, *, y=5)

newadd = partial(add, 10) #相当于x=10
print(newadd(11))
#: 10 11
#: 21
#print(newadd(20,21)) #add(10,20,21) TypeError: add() takes 2 positional arguments but 3 were given
#print(newadd(x=4)) #add(10, ?, x=4) TypeError: add() got multiple values for argument 'x'
print(inspect.signature(newadd))
#(y)
# coding: utf-8
def add(x, y, *args):
    print(x, y, args)
    return x + y + sum(args)

from functools import partial
import inspect

newadd = partial(add, 1)
print(inspect.signature(newadd)) #: (y, *args)

newadd = partial(add, 1, 2)
print(inspect.signature(newadd)) #: (*args)

newadd = partial(add, y=2)
print(inspect.signature(newadd)) #: (x, *, y=2)
#newadd(1,3,5,7,9) #报错,y固定了,后面args就没法给了 TypeError: add() got multiple values for argument 'y'

newadd = partial(add, 1,2,3,4,5)
print(inspect.signature(newadd)) #: (*args)
newadd()
newadd(10)
newadd(1, 2)
#:1 2 (3, 4, 5)
#: 1 2 (3, 4, 5, 10)
#: 1 2 (3, 4, 5, 1, 2)


newadd = partial(add, y=20, x=10)
print(inspect.signature(newadd)) #: (*, x=10, y=20)
newadd()
#: 10 20 ()
#newadd(1, 2) #报错 TypeError: add() got multiple values for argument 'y'
newadd(x=1, y=2)
#1 2 ()
#newadd(x=3, y=4, 5, 6, 7) #报错 关键字传参不能放在位置传参之前 SyntaxError: positional argument follows keyword argument
#newadd(5, 6, 7, x=3, y=4) #报错 TypeError: add() got multiple values for argument 'y'
partial本质

python帮助文档检索partial,找到(in module functools),查看等价对象

def partial(func, /, *args, **keywords):
    def newfunc(*fargs, **fkeywords):
        newkeywords = {**keywords, **fkeywords}
        return func(*args, *fargs, **newkeywords)
    newfunc.func = func
    newfunc.args = args
    newfunc.keywords = keywords
    return newfunc
# coding: utf-8
from functools import partial

def partial(func, /, *args, **keywords): #add, (), {x:10, y:20}
    def newfunc(*fargs, **fkeywords): #newfunc诞生了
        newkeywords = {**keywords, **fkeywords}
        return func(*args, *fargs, **newkeywords)
    newfunc.func = func #指向原函数
    newfunc.args = args #args kwargs分别记录了传入的实参
    newfunc.keywords = keywords
    return newfunc

def add(x, y, *args):
    print(x, y, args)
    return x + y + sum(args)

newadd = partial(add, y=20, x=10) #返回新函数
print(newadd.func, hex(id(add)))
#: <function add at 0x0000026283C36280> 0x26283c36280

#分析如何调用
newadd(4, 5) #add(4,5,x=10, y=20)

def partial(func, /, *args, **keywords): #add, (), {x:10, y:20}
    def newfunc(*fargs, **fkeywords): #newfunc诞生了 newfunc(4, 5) (4,5) {}
        newkeywords = {**keywords, **fkeywords} #{x:10, y:20}
        #如果你固定的时候,关键字传参,后期还可以更新它
        #如果固定了按位置传参,将前后2次位置传参都解开,把关键字传参也解开
        return func(*args, *fargs, **newkeywords) #retrun add(4,5, x=10, y=20)
    newfunc.func = func #指向原函数
    newfunc.args = args #args kwargs分别记录了传入的实参
    newfunc.keywords = keywords
    return newfunc

functools中wraps采用了partial偏函数

def update_wrapper(wrapper,
                   wrapped,
                   assigned = WRAPPER_ASSIGNMENTS,
                   updated = WRAPPER_UPDATES):
    pass
    return wrapper

def wraps(wrapped,
          assigned = WRAPPER_ASSIGNMENTS,
          updated = WRAPPER_UPDATES):
    newfunc = partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated) #固定了3参
    return newfunc

wraps(fn) => newfunc 有一个参数wrapper
wraps(fn)(wrapper) => newfunc(wrapper)
return func(*args, *fargs, **newkeywords)
return update_wrapper(wrapper, wrapped=fn, assigned=assigned, updated=updated)

lru_cache

@functools.lru_cache(maxsize=128, typed=False)

  • lru即Least-recently-used,最近最少使用。cache缓存
  • 如果maxsize设置为None,则禁用LRU功能,并且缓存可以无限制增长。当maxsize是二的幂时,LRU功能执行得最好
  • 如果typed设置为True,则不同类型的函数参数将单独缓存。例如,f(3)和f(3.0)将被视为具有不同结果的不同调用
  • Python3.8简化了调用,可以使用
# coding: utf-8
import functools

@functools.lru_cache
def add():
    pass

#等价于
@functools.lru_cache(128)
def add():
    pass
# coding: utf-8
from functools import lru_cache
import time


@lru_cache() # maxsize = 128 # 如果是None 无限大 typed=False
def add(x=4, y=5):
    print('-' * 30)
    time.sleep(2)
    return x + y

print(1, add(4, 5)) #建立了调用信息(输入)和结果(输出)的对应关系,就要用字典?key hash
print(2, add(4, 5)) #第2次和第3次根本没有调用add,没有计算,用到了这个字典,缓存cache,缓存命中
print(3, add(4, 5))
print(3, add(4.0, 5.0)) #和add(4,5)等价,命中了。如果typed=True就不等价了,会计算
print(4, add(4, 6)) #计算了
print(5, add()) #相当于add(4, 5) , 又计算了
print(6, add(4)) #相当于add(4, 5) , 又计算了
print('=' * 30)
print(7, add(x=4, y=5)) #计算了
print(8, add(y=5, x=4)) #计算了
print(9, add(4, y=5)) #计算了

#------------------------------
#1 9
#2 9
#3 9
#3 9
#------------------------------
#4 10
#------------------------------
#5 9
#------------------------------
#6 9
#==============================
#------------------------------
#7 9
#------------------------------
#8 9
#------------------------------
#9 9

应用

# coding: utf-8
from functools import lru_cache

@lru_cache #斐波那契数列 fib(b) = fib(n-1) + fib(n-2) 这个版本背过
def fib(n): #n >= 1
    return 1 if n <3 else fib(n-1) + fib(n-2)

print(fib(35))
#: 9227465

#为什么算这么快? 空间换时间。计算的结果都被缓存下来,这次只是命中了。
lru_cache 本质
  • 内部使用了一个字典

key是怎么来的?

# coding: utf-8
from functools import lru_cache 

@lru_cache #fn = lru_cache(fn)
def fn():
    pass

@lru_cache() # fn = lru_cache()(fn) # fn = lru_cache(128, False)(fn)
# add = lru_cache()(add) => add decorating_function(add)
#add = wrapper => _lru_cache_wrapper(add, maxsize, typed, _CacheInfo)
def fn():
    pass

def lru_cache(maxsize=128, typed=False):
    if isinstance(maxsize, int):
        # Negative maxsize is treated as 0
        if maxsize < 0:
            maxsize = 0
    elif callable(maxsize) and isinstance(typed, bool): #python 3.8 增加的代码
        # The user_function was passed in directly via the maxsize argument
        user_function, maxsize = maxsize, 128 #
        wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)
        return update_wrapper(wrapper, user_function)
    elif maxsize is not None:
        raise TypeError(
            'Expected first argument to be an integer, a callable, or None')
    def decorating_function(user_function):
        wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)
        return update_wrapper(wrapper, user_function)

    return decorating_function

...

def _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo):
    # Constants shared by all lru cache instances:
    sentinel = object()          # unique object used to signal cache misses
    make_key = _make_key         # build a key from the function arguments
...

    elif maxsize is None:

        def wrapper(*args, **kwds):
            # Simple caching without ordering or size limit
            nonlocal hits, misses
            key = make_key(args, kwds, typed)

...
def _make_key(args, kwds, typed,
             kwd_mark = (object(),),
             fasttypes = {int, str},
             tuple=tuple, type=type, len=len):
    key = args
    if kwds:
        key += kwd_mark
        for item in kwds.items():
            key += item
    if typed:
        key += tuple(type(v) for v in args)
        if kwds:
            key += tuple(type(v) for v in kwds.values())
    elif len(key) == 1 and type(key[0]) in fasttypes:
        return key[0]
    return _HashedSeq(key)

  • key 是由 _make_key 函数构造出来
# coding: utf-8
from functools import _make_key

print(_make_key((4, 5), {}, False))
print(_make_key((4,), {'y': 5}, False))
print(_make_key((), {'x':4, 'y': 5}, False))
print(_make_key((4, 5), {}, True))
#: [4, 5]
#: [4, <object object at 0x0000020BD8EC4E90>, 'y', 5]
#: [<object object at 0x0000020BD8EC4E90>, 'x', 4, 'y', 5]
#: [4, 5, <class 'int'>, <class 'int'>]
应用
# coding: utf-8
from functools import lru_cache

@lru_cache #斐波那契数列 fib(b) = fib(n-1) + fib(n-2) 这个版本背过
def fib(n): #n >= 1
    return 1 if n <3 else fib(n-1) + fib(n-2)

print(fib(35))
#: 9227465

#为什么算这么快? 空间换时间。计算的结果都被缓存下来,这次只是命中了。
# coding: utf-8
from functools import lru_cache
import datetime

#maxsize cache = {} 字典的大小上限,kv对有个50个上限,LRU
@lru_cache(50) #斐波那契数列 fib(b) = fib(n-1) + fib(n-2) 这个版本背过
def fib(n): #n >= 1
    return 1 if n <3 else fib(n-1) + fib(n-2)

for i in [35, 101]: 
    start = datetime.datetime.now()
    print(fib(i))
    delta = (datetime.datetime.now() - start).total_seconds()
    print(i, delta)

#: 9227465
#: 35 0.0
#: 573147844013817084101
#: 101 0.0

#为什么算这么快? 空间换时间。计算的结果都被缓存下来,这次只是命中了。

斐波那契数列 有循环版、递归版、生成器版、递归加lru_cache版本

什么时候用缓存?

  • 不变。恒久不变,一段时间内,不变化,给定一个值,返回一个恒久不变值;幂等性,给定一个输入,一定返回一个不变的结果
  • 计算代价高。但是,给你一个输入,得到的结果需要时间长,空间换时间,输入key -> value字典
  • 使用频度。在一段时间内,使用频率高,空间换时间,kv
总结

lru_cache装饰器应用:

  • 使用前提
    • 同样的函数参数一定得到同样的结果,至少是一段时间内,同样输入得到同样结果
    • 计算代价高,函数执行时间很长
    • 需要多次执行,每一次计算代价高
  • 本质是建立函数调用的参数到返回值的映射
  • 缺点
    • 不支持缓存过期,key无法过期、失效
    • 不支持清除操作
    • 不支持分布式,是一个单机的缓存
  • 适应场景,单机上需要空间换时间的地方,可以缓存来将计算变成快速的查询

学习 lru_cache 可以让我们了解缓存背后的原理

练习

实现Base64编码

https://en.wikipedia.org/wiki/Base64

用6位就可以表达所有的数值。

img_20240605_083033.png

边界:最大的值不能超过6位

# coding: utf-8
#base64编码
import base64

#s = 'abc' # 内部用的unicode
#print(base64.b64encode(s)) #报错TypeError: a bytes-like object is required, not 'str'

s = b'abc' # 内部用的unicode b'YWJj' #内存中大端 0x616263
print(base64.b64encode(s))
#: b'YWJj'

#打印base64码表
from string import ascii_lowercase, ascii_uppercase
alphabet = ascii_uppercase + ascii_lowercase + '0123456789+/'
print(alphabet)
#: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

base64转换原理

0x 61         62         63  big
0b 0110 0001  0110 0010  0110 0011       #3*8 24bits
0b   011000   010110    001001   100011  #变成6位一段
0b 00011000 00010110  00001001 00100011  #变成6位一段,前面补2个0对数值没有影响
0x 18       16        9        23        #转成16进制,再查对应base64码表
   24       22        9        35        #10进制表达
   Y        W         J        j         #查base64码表对应的值

3*8 24bits -> 4*6bits

先拿到第一个字符

# coding: utf-8

import base64

alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"


#print(8 >> 1, 0b1000 >> 1) #0b0100
#print(8 >> 2, 0b1000 >> 2) #0b10
#print(0b1000000000 >> 5)
##: 4 4
##: 2 2
##: 16

def b64encode(src:str, encoding='utf-8') ->bytes:
    #3字节断开,4 * 6 -> 4 * 8 bits前面补零,移位计算正好,简单效率高
    if isinstance(src, str):
        _src = src.encode(encoding=encoding)
    else:
        return None # 故意返回None就是告诉你,我不返回bytes类型数据b''

    #print(_src >> 18) #: b'abc' 0x61 62 63 b'\x616263' bytes类型不支持移位,整数支持。
    print(_src) #: b'abc' 0x61 62 63 b'\x616263' bytes类型不支持移位,整数支持。
    # 整数支持 >>
    b = int.from_bytes(_src, 'big') #bytes转int
    print(b, hex(b), b >> 18) #24 - 18 = 6 # 返回int类型,至少认为你是一个字节的
    #0b 011000 >>18  010110  001001 100011
    #0b 00011000
    #0x 18
    index = b >> 18
    #: 6382179 0x616263 24
    # index  base64 中使用index找到字符
    print(alphabet[index]) #: Y 处理的第一个字符出来了。

print(b64encode('abc'))

拿到第二第三个字符

# coding: utf-8
print(8 >> 1, 0b1000 >> 1) #0b0100
print(8 >> 2, 0b1000 >> 2) #0b10
print(0b1000000000 >> 5)
#: 4 4
#: 2 2
#: 16

print(0b1 << 1) # 0b10 左移1位
#: 2

# 位与
# x & 0x0000000000000000000111111
# 3 & 0x3F

#0b 011000 010110  001001 100011  >> 12 #abc先右移12位
#0b 011000 010110 & 0x3F #去掉前6位,位与0x3F,拿到最后6位
#0b 000000 111111 #0x3F
#ob 010110 #结果拿到后6位,即第二个字符


#第三个字符

#0b 011000 010110  001001 100011  >> 6#先右移6位
#0b 011000 010110  001001 & 0x3F  #位与0x3F,拿到最后6位
#0b 000000 000000  111111 #0x3F
#0b 001001 #9
# coding: utf-8

import base64

alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"


#print(8 >> 1, 0b1000 >> 1) #0b0100
#print(8 >> 2, 0b1000 >> 2) #0b10
#print(0b1000000000 >> 5)
##: 4 4
##: 2 2
##: 16

def b64encode(src:str, encoding='utf-8') ->bytes:
    #3字节断开,4 * 6 -> 4 * 8 bits前面补零,移位计算正好,简单效率高
    if isinstance(src, str):
        _src = src.encode(encoding=encoding)
    else:
        return None # 故意返回None就是告诉你,我不返回bytes类型数据b''

    #print(_src >> 18) #: b'abc' 0x61 62 63 b'\x616263' bytes类型不支持移位,整数支持。
    print(_src) #: b'abc' 0x61 62 63 b'\x616263' bytes类型不支持移位,整数支持。
    # 整数支持 >>
    b = int.from_bytes(_src, 'big') #bytes转int
    print(b, hex(b), b >> 18) #24 - 18 = 6 # 返回int类型,至少认为你是一个字节的
    #0b 011000 >>18  010110  001001 100011
    #0b 00011000
    #0x 18
    index = b >> 18 & 0x3F #可以不写& 0x3F,无用功
    #: 6382179 0x616263 24
    # index  base64 中使用index找到字符
    print(alphabet[index]) #: Y 处理的第一个字符出来了。

    index = b >> 12 & 0x3F
    print(alphabet[index]) #: W 处理的第二个字符出来了。

    index = b >> 6 & 0x3F
    print(alphabet[index]) #: J 处理的第三个字符出来了。

    index = b >> 0 & 0x3F
    print(alphabet[index]) #: J 处理的最后字符出来了。

print(b64encode('abc'))

找规律

# coding: utf-8
import base64

alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

#print(8 >> 1, 0b1000 >> 1) #0b0100
#print(8 >> 2, 0b1000 >> 2) #0b10
#print(0b1000000000 >> 5)
##: 4 4
##: 2 2
##: 16

def b64encode(src:str, encoding='utf-8') ->bytes:
    #3字节断开,4 * 6 -> 4 * 8 bits前面补零,移位计算正好,简单效率高
    if isinstance(src, str):
        _src = src.encode(encoding=encoding)
    else:
        return None # 故意返回None就是告诉你,我不返回bytes类型数据b''

    print(_src) #: b'abc' 0x61 62 63 b'\x616263' bytes类型不支持移位,整数支持。
    # 整数支持 >>
    b = int.from_bytes(_src, 'big') #bytes转int

    for i in range(18, -1, -6):
        #print(b, hex(b), b >> 18) #24 - 18 = 6 # 返回int类型,至少认为你是一个字节的
        #index = b >> i & 0x3F #无用功 18
        index = b >> i if i = 18 else b >> i & 0x3F #处理无用功问题
        # index  base64 中使用index找到字符
        print(alphabet[index]) #: Y 处理的第一个数字出来了。

print(b64encode('abc'))
#: b'abc'
#: Y
#: W
#: J
#: j
#: None
# coding: utf-8
import base64

alphabet = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

#print(8 >> 1, 0b1000 >> 1) #0b0100
#print(8 >> 2, 0b1000 >> 2) #0b10
#print(0b1000000000 >> 5)
##: 4 4
##: 2 2
##: 16

def b64encode(src:str, encoding='utf-8') ->bytes:
    #3字节断开,4 * 6 -> 4 * 8 bits前面补零,移位计算正好,简单效率高
    if isinstance(src, str):
        _src = src.encode(encoding=encoding)
    else:
        return None # 故意返回None就是告诉你,我不返回bytes类型数据b''

    ret = bytearray() #目标
    print(_src) #: b'abc' 0x61 62 63 b'\x616263' bytes类型不支持移位,整数支持。
    # 整数支持 >>
    b = int.from_bytes(_src, 'big') #bytes转int

    for i in range(18, -1, -6):
        #print(b, hex(b), b >> 18) #24 - 18 = 6 # 返回int类型,至少认为你是一个字节的
        #index = b >> i & 0x3F #无用功 18
        index = b >> i if i == 18 else b >> i & 0x3F #处理无用功问题
        # index  base64 中使用index找到字符
        print(alphabet[index]) #: Y 处理的第一个字符出来了。bytes的索引返回整数。返回字符对应的整数出来
        ret.append(alphabet[index]) #ret.append()追加的是字节,不是字符串,需要把alphabet变量换成bytes类型,bytes的索引返回整数
    return ret

print(b64encode('abc'))
#: b'abc'
#: 89
#: 87
#: 74
#: 106
#: bytearray(b'YWJj')
# coding: utf-8
import base64

alphabet = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

#print(8 >> 1, 0b1000 >> 1) #0b0100
#print(8 >> 2, 0b1000 >> 2) #0b10
#print(0b1000000000 >> 5)
##: 4 4
##: 2 2
##: 16

def b64encode(src, encoding='utf-8') ->bytes:
    #3字节断开,4 * 6 -> 4 * 8 bits前面补零,移位计算正好,简单效率高
    if isinstance(src, str):
        _src = src.encode(encoding=encoding)
    elif isinstance(src, (bytes, bytearray)):
        _src = bytes(src)
    else:
        return None # 故意返回None就是告诉你,我不返回bytes类型数据b''

    ret = bytearray() #目标
    print(_src) #: b'abc' 0x61 62 63 b'\x616263' bytes类型不支持移位,整数支持。
    # 整数支持 >>
    b = int.from_bytes(_src, 'big') #bytes转int

    for i in range(18, -1, -6):
        #print(b, hex(b), b >> 18) #24 - 18 = 6 # 返回int类型,至少认为你是一个字节的
        #index = b >> i & 0x3F #无用功 18
        index = b >> i if i == 18 else b >> i & 0x3F #处理无用功问题
        # index  base64 中使用index找到字符
        print(alphabet[index]) #: Y 处理的第一个字符出来了。bytes的索引返回整数。返回字符对应的整数出来
        ret.append(alphabet[index]) #ret.append()追加的是字节,不是字符串,需要把alphabet变量换成bytes类型,bytes的索引返回整数
    return bytes(ret) #返回bytes类型

print(b64encode('abc'))
#: b'abc'
#: 89
#: 87
#: 74
#: 106
#: b'YWJj'

3个字节转base64正好每6位一组;2个字节转base64缺少的补0,全0用=等号表示;4个字节一样,余一个字节,缺少补0.

img_20240607_083706.png
# coding: utf-8
import base64
s = b'abcd'
print(base64.b64encode(s))
print()

alphabet = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

#print(8 >> 1, 0b1000 >> 1) #0b0100
#print(8 >> 2, 0b1000 >> 2) #0b10
#print(0b1000000000 >> 5)
##: 4 4
##: 2 2
##: 16

#b'abcd'
def b64encode(src, encoding='utf-8') ->bytes:
    #3字节断开,4 * 6 -> 4 * 8 bits前面补零,移位计算正好,简单效率高
    if isinstance(src, str):
        _src = src.encode(encoding=encoding)
    elif isinstance(src, (bytes, bytearray)):
        _src = bytes(src)
    else:
        return None # 故意返回None就是告诉你,我不返回bytes类型数据b''

    ret = bytearray() #目标
    length = len(_src) #字符串长度

    for offset in range(0, length, 3):
        #print(_src[offset:offset+3])
        triple = _src[offset:offset+3]

        print(triple, len(triple)) #: b'abc' 0x61 62 63 b'\x616263' bytes类型不支持移位,整数支持。
        #如果不满足3个字符,补0代替
        r = 3 - len(triple)
        if r:
            triple += b'\x00' * r  #null 字符用ascii码表示\x00
        print(triple, len(triple))

        # 整数支持 >>
        b = int.from_bytes(triple, 'big') #bytes转int

        for i in range(18, -1, -6):
            #print(b, hex(b), b >> 18) #24 - 18 = 6 # 返回int类型,至少认为你是一个字节的
            #index = b >> i & 0x3F #无用功 18
            index = b >> i if i == 18 else b >> i & 0x3F #处理无用功问题
            print(index)
            # index  base64 中使用index找到字符
            ret.append(alphabet[index]) #ret.append()追加的是字节,不是字符串,需要把alphabet变量换成bytes类型,bytes的索引返回整数
            print(ret)
        if r:
            for i in range(r):   #range(2) 0, 1 -1, -2
                ret[-i-1] = 0x3D  # = 等号的ascii码0x3d

    return bytes(ret) #返回bytes类型

print(b64encode(s))
#有很多base64编过码的字符串,可以看到没有=,为什么? 因为有的去掉了。
b'YWJjZA=='

b'abc' 3
b'abc' 3
24
bytearray(b'Y')
22
bytearray(b'YW')
9
bytearray(b'YWJ')
35
bytearray(b'YWJj')
b'd' 1
b'd\x00\x00' 3
25
bytearray(b'YWJjZ')
0
bytearray(b'YWJjZA')
0
bytearray(b'YWJjZAA')
0
bytearray(b'YWJjZAAA')
b'YWJjZA=='
# coding: utf-8
import base64
s = b'abcd'
print(base64.b64encode(s))
print()

alphabet = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

#print(8 >> 1, 0b1000 >> 1) #0b0100
#print(8 >> 2, 0b1000 >> 2) #0b10
#print(0b1000000000 >> 5)
##: 4 4
##: 2 2
##: 16

#b'abcd'
def b64encode(src, encoding='utf-8') ->bytes:
    #3字节断开,4 * 6 -> 4 * 8 bits前面补零,移位计算正好,简单效率高
    if isinstance(src, str):
        _src = src.encode(encoding=encoding)
    elif isinstance(src, (bytes, bytearray)):
        _src = bytes(src)
    else:
        return None # 故意返回None就是告诉你,我不返回bytes类型数据b''

    ret = bytearray() #目标
    length = len(src) #字符串长度

    for offset in range(0, length, 3):
        #print(_src[offset:offset+3])
        triple = _src[offset:offset+3]

        print(triple, len(triple)) #: b'abc' 0x61 62 63 b'\x616263' bytes类型不支持移位,整数支持。
        #如果不满足3个字符,补0代替
        r = 3 - len(triple)
        if r:
            triple += b'\x00' * r  #null 字符用ascii码表示\x00
        print(triple, len(triple))

        # 整数支持 >>
        b = int.from_bytes(triple, 'big') #bytes转int

        for i in range(18, -1, -6):
            #print(b, hex(b), b >> 18) #24 - 18 = 6 # 返回int类型,至少认为你是一个字节的
            #index = b >> i & 0x3F #无用功 18
            index = b >> i if i == 18 else b >> i & 0x3F #处理无用功问题
            print(index)
            # index  base64 中使用index找到字符
            ret.append(alphabet[index]) #ret.append()追加的是字节,不是字符串,需要把alphabet变量换成bytes类型,bytes的索引返回整数
            print(ret)
        if r:
            # for i in range(r):   #range(2) 0, 1 -1, -2
            #     ret[-i-1] = 0x3D  # = 等号的ascii码0x3d
            ret[-r:] = b'=' * r    #b'YWJjZA=='
            #ret = ret[:-r]  #  b'YWJjZA'               
    return bytes(ret) #返回bytes类型

print(b64encode(s))
#有很多base64编过码的字符串,可以看到没有=,为什么? 因为有的去掉了。
b'YWJjZA=='

b'abc' 3
b'abc' 3
24
bytearray(b'Y')
22
bytearray(b'YW')
9
bytearray(b'YWJ')
35
bytearray(b'YWJj')
b'd' 1
b'd\x00\x00' 3
25
bytearray(b'YWJjZ')
0
bytearray(b'YWJjZA')
0
bytearray(b'YWJjZAA')
0
bytearray(b'YWJjZAAA')
b'YWJjZA=='

最终

# coding: utf-8
import base64

alphabet = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

#b'abcd'
def b64encode(src, encoding='utf-8') ->bytes:
    #3字节断开,4 * 6 -> 4 * 8 bits前面补零,移位计算正好,简单效率高
    if isinstance(src, str):
        _src = src.encode(encoding=encoding)
    elif isinstance(src, (bytes, bytearray)):
        _src = bytes(src)
    else:
        return None # 故意返回None就是告诉你,我不返回bytes类型数据b''

    ret = bytearray() #目标
    length = len(_src) #字符串长度

    for offset in range(0, length, 3):
        triple = _src[offset:offset+3]

        r = 3 - len(triple)
        if r:
            triple += b'\x00' * r  #null 字符用ascii码表示\x00

        # 整数支持 >>
        b = int.from_bytes(triple, 'big') #bytes转int
        for i in range(18, -1, -6):
            index = b >> i if i == 18 else b >> i & 0x3F 
            ret.append(alphabet[index]) 
        if r:
            ret[-r:] = b'=' * r    #b'YWJjZA=='
            #ret = ret[:-r]  #  b'YWJjZA'               
    return bytes(ret) #返回bytes类型

for s in ['a', '`', 'ab', 'abc', '', '哈啊c', 'aa']:
    print(base64.b64encode(s.encode()))
    print(base64.urlsafe_b64encode(s.encode()))  #url字符中,用-减号替代+加号,用_下划线替代/斜杠
    print(b64encode(s))
    print()
b'YQ=='
b'YQ=='
b'YQ=='

b'YA=='
b'YA=='
b'YA=='

b'YWI='
b'YWI='
b'YWI='

b'YWJj'
b'YWJj'
b'YWJj'

b''
b''
b''

b'5ZOI5ZWKYw=='
b'5ZOI5ZWKYw=='
b'5ZOI5ZWKYw=='

b'YWE='
b'YWE='
b'YWE='

实现一个cache装饰器,实现可过期被清除的功能

简化设计,函数的形参定义不包含可变位置参数、可变关键词参数和keyword-only参数,可以不考虑缓存大小,也不用考虑缓存满了之后的换出问题。

#进阶
def add(x=4, y=5):
    time.sleep(3)
    return x + y

以下6种,可以认为是同一种调用
print(1, add(4,5))
print(2, add(4))
print(3, add(y=5))
print(4, add(x=4, y=5))
print(5, add(y=5, x=4))
print(6, add( ))

缓存应用场景

1. 同样的函数参数一定得到同样的结果,至少是一段时间内,同样输入得到同样的结果
2. 计算代价高,函数执行时间长
3. 需要多次执行,每一次执行代价高

Cache广泛的应用在企业级应用,地位极其重要。使用缓存可以提高查询速度,用内存空间换查询、加载数据的时间。

数据结构选择

输入一致,输出一致。

基于上面分析,此数据结构应用是字典。
通过一个key,对应一个value
key是参数列表组成的结构,value是函数返回值

本次的难点在于key如何处理

Key的存储

数据结构选取字典,那么在python中key必须是hashable。

key能适应位置传参和关键字传参提供的实参。
- 位置传参本身就是有顺序的
- 关键字传参可以无序,可以考虑使用字典收集。无序如何解决?
- OrderedDict可以吗?可以,它记录key的录入顺序
- 不用OrderedDict可以吗?可以,对字典kv对按照key排序

key的异同

#进阶
def add(x=4, y=5):
    time.sleep(3)
    return x + y

以下6种,可以认为是同一种调用
print(1, add(4,5))
print(2, add(4))
print(3, add(y=5))
print(4, add(x=4, y=5))
print(5, add(y=5, x=4))
print(6, add( ))
可以认为
- 以上都不同。因为参数传输方式不同,或参数顺序不同,或不传实参而使用缺省值
- 1、4、5参数相同,因为实参相同,只是传参方式不同、参数顺序不同;2、3、6都不同,因为传参不一样
- 以上都相同。本质上最后使用的值都是x=4, y=5

lru_cache对以上6种都认为不相同。源码中,单独处理了位置传参和关键字传参。

如何实现以上6种都一样呢?

由于使用了缺省值,所以考虑使用inspect模块。

key的要求

key就必须是hashable。

由于key是实参或缺省值凑在一起的,所以必须不包含不可hash类型数据。lru_cache无法实现,我们
也无法实现。所以,对于实参中有不可hash类型数据的,不可以实现缓存
import functools
import time

def add1(x, y):
    return y

print(add1([], 5))  #返回5

@functools.lru_cache()
def add(x, y=5):
    time.sleep(3)
    return y

print(add(4))  #返回5

add([], 5)  #报错,key是不可hash的
#: TypeError: unhashable type: 'list'

key算法设计

inspect模块获取函数签名后,取parameters,这是一个有序字典,会保存所有参数的信息。

构建一个字典target,按照位置顺序从args中依次对应参数名和传入的实参,组成kv对,
存入target字典中。

kwargs所有值update到target字典中

使用缺省值,需要从签名中提取。

调用方式

普通的函数调用可以,但是过于明显,最好类似lru_cache的方式,让调用者无察觉的使用缓存。
构建装饰器函数。
代码模板如下:
# coding: utf-8
import time
from functools import wraps

#cache = {} #全局变量,可以,但是不好
def my_cache(fn):
    local_cache = {}  #每装饰一个函数,都有此函数的独立的cache
    @wraps(fn)
    def wrapper(*args, **kwargs):
        # makekey 处理key
        # 字典放哪?
        #cache = {} #函数调用时才产生的,不要放这
        ret = fn(*args, **kwargs)

        return ret
    return wrapper

@my_cache
def add(x=4, y=5):   #add = wrapper add = my_cache(add)
    print('=' * 30)
    time.sleep(3)
    return x + y

print(1, add(4,5))
print(3, add(y=5))
print(4, add(x=4, y=5))

目标

def add(x=4, y=5): 
    print('=' * 30)
    time.sleep(3)
    return x + y

print(1, add(4,5))
print(2, add(4))
print(3, add(y=5))
print(4, add(x=4, y=5))
print(5, add(y=5, x=4))
print(6, add( ))
代码实现
# coding: utf-8
import time
from functools import wraps

#cache = {} #全局变量,可以,但是不好
def my_cache(fn):
    local_cache = {}  #每装饰一个函数,都有此函数的独立的cache
    @wraps(fn)
    def wrapper(*args, **kwargs):
        # makekey 处理key
        # 字典放哪?
        #cache = {} #函数调用时才产生的,不要放这
        ret = fn(*args, **kwargs)

        return ret
    return wrapper

@my_cache
def add(x=4, y=5):   #add = wrapper add = my_cache(add)
    print('=' * 30)
    time.sleep(3)
    return x + y

print(1, add(4,5))
print(3, add(y=5))
print(4, add(x=4, y=5))
# coding: utf-8
#参数处理

import time
from functools import wraps
import inspect

def my_cache(fn):
    local_cache = {}  #每装饰一个函数,都有此函数的独立的cache
    @wraps(fn)
    def wrapper(*args, **kwargs):
        # makekey 处理key
        sig = inspect.signature(fn)
        params = sig.parameters  # 返回的OrderedDict
        print(args, kwargs)

        # 位置传参,变量名的对应
        names  = list(params.keys())
        for v in args:
            print(v)

        ret = fn(*args, **kwargs)

        return ret
    return wrapper

@my_cache
def add(x=4, y=5):   #add = wrapper add = my_cache(add)
    print('=' * 30)
    time.sleep(3)
    return x + y

print(1, add(4,5))
#print(3, add(y=5))
#print(4, add(x=4, y=5))

#输出
#: (4, 5) {}
#: 4
#: 5
#: ==============================
#: 1 9
# coding: utf-8
#参数处理- 位置传参 跟名称对应

import time
from functools import wraps
import inspect

def my_cache(fn):
    local_cache = {}  #每装饰一个函数,都有此函数的独立的cache
    @wraps(fn)
    def wrapper(*args, **kwargs):
        # makekey 处理key
        target = {} #key 字典无序


        sig = inspect.signature(fn)
        params = sig.parameters  # 返回的OrderedDict
        print(args, kwargs)

        # 位置传参,变量名的对应
        # names  = list(params.keys())
        # for i, v in enumerate(args):
        #     print(i,v, names[i])
        #     target[names[i]] = v
        target.update(zip(params.keys(), args))
        # update(二元组的可迭代对象,k1=v1, k2=v2) update(mapping, k1=v1, k2=v2)


        print(target)

        ret = fn(*args, **kwargs)

        return ret
    return wrapper

@my_cache
def add(x=4, y=5):   #add = wrapper add = my_cache(add)
    print('=' * 30)
    time.sleep(3)
    return x + y

print(1, add(4,5))
#print(3, add(y=5))
#print(4, add(x=4, y=5))

#输出
#: (4, 5) {}
#: {'x': 4, 'y': 5}
#: ==============================
#: 1 9
# coding: utf-8
#参数处理-关键字传参

import time
from functools import wraps
import inspect

def my_cache(fn):
    local_cache = {}  #每装饰一个函数,都有此函数的独立的cache
    @wraps(fn)
    def wrapper(*args, **kwargs):
        # makekey 处理key
        target = {} #key 字典无序


        sig = inspect.signature(fn)
        params = sig.parameters  # 返回的OrderedDict
        print(args, kwargs)

        # 位置传参,变量名的对应
        # names  = list(params.keys())
        # for i, v in enumerate(args):
        #     print(i,v, names[i])
        #     target[names[i]] = v
        #target.update(zip(params.keys(), args))
        # update(二元组的可迭代对象,k1=v1, k2=v2) update(mapping, k1=v1, k2=v2)

        #关键字传参,乱序
        # for k,v in kwargs.items():
        #     target[k] = v
        #target.update(kwargs)
        target.update(zip(params.keys(), args), **kwargs)  #将target.update(zip(params.keys(), args)) 和 target.update(kwargs)合一起

        print(target)

        ret = fn(*args, **kwargs)

        return ret
    return wrapper

@my_cache
def add(x=4, y=5):   #add = wrapper add = my_cache(add)
    print('=' * 30)
    time.sleep(1)
    return x + y

print(1, add(4,5))
print(3, add(y=5))
print(4, add(x=4, y=5))

#输出
#(4, 5) {}
#{'x': 4, 'y': 5}
#==============================
#1 9
#() {'y': 5}
#{'y': 5}
#==============================
#3 9
#() {'x': 4, 'y': 5}
#{'x': 4, 'y': 5}
#==============================
#4 9
# coding: utf-8
#参数处理-缺省值

import time
from functools import wraps
import inspect

def my_cache(fn):
    local_cache = {}  #每装饰一个函数,都有此函数的独立的cache
    @wraps(fn)
    def wrapper(*args, **kwargs):
        # makekey 处理key
        target = {} #key 字典无序


        sig = inspect.signature(fn)
        params = sig.parameters  # 返回的OrderedDict
        print(args, kwargs)

        # 位置传参,变量名的对应
        # names  = list(params.keys())
        # for i, v in enumerate(args):
        #     print(i,v, names[i])
        #     target[names[i]] = v
        #target.update(zip(params.keys(), args))
        # update(二元组的可迭代对象,k1=v1, k2=v2) update(mapping, k1=v1, k2=v2)

        #关键字传参,乱序
        # for k,v in kwargs.items():
        #     target[k] = v
        #target.update(kwargs)
        target.update(zip(params.keys(), args), **kwargs)  #将target.update(zip(params.keys(), args)) 和 target.update(kwargs)合一起

        #缺省值 add(y=5) add()
        # for k in (params.keys() - target.keys()):
        #     target[k] = params[k].default
        target.update(map(lambda k:(k, params[k].default), params.keys() - target.keys()))

        # for k,v in params.items():
        #     if k not in target.keys():
        #         target[k] = v.default
        # target.update({k:v.default for k,v in params.items() if k not in target.keys()})

        print(target)

        ret = fn(*args, **kwargs)

        return ret
    return wrapper

@my_cache
def add(x=4, y=5):   #add = wrapper add = my_cache(add)
    print('=' * 30)
    time.sleep(1)
    return x + y

print(1, add(4,5))
print(3, add(y=5))
print(4, add(x=4, y=5))

#输出
#(4, 5) {}
#{'x': 4, 'y': 5}
#==============================
#1 9
#() {'y': 5}
#{'y': 5, 'x': 4}
#==============================
#3 9
#() {'x': 4, 'y': 5}
#{'x': 4, 'y': 5}
#==============================
#4 9
# coding: utf-8
#排序

import time
from functools import wraps
import inspect

def my_cache(fn):
    local_cache = {}  #每装饰一个函数,都有此函数的独立的cache
    @wraps(fn)
    def wrapper(*args, **kwargs):
        # makekey 处理key
        target = {} #key 字典无序


        sig = inspect.signature(fn)
        params = sig.parameters  # 返回的OrderedDict
        print(args, kwargs)

        # 位置传参,变量名的对应
        # names  = list(params.keys())
        # for i, v in enumerate(args):
        #     print(i,v, names[i])
        #     target[names[i]] = v
        #target.update(zip(params.keys(), args))
        # update(二元组的可迭代对象,k1=v1, k2=v2) update(mapping, k1=v1, k2=v2)

        #关键字传参,乱序
        # for k,v in kwargs.items():
        #     target[k] = v
        #target.update(kwargs)
        target.update(zip(params.keys(), args), **kwargs)  #将target.update(zip(params.keys(), args)) 和 target.update(kwargs)合一起

        #缺省值 add(y=5) add()
        # for k in (params.keys() - target.keys()):
        #     target[k] = params[k].default
        target.update(map(lambda k:(k, params[k].default), params.keys() - target.keys()))

        # for k,v in params.items():
        #     if k not in target.keys():
        #         target[k] = v.default
        # target.update({k:v.default for k,v in params.items() if k not in target.keys()})

        print(target)

        print(sorted(target.items()))

        ret = fn(*args, **kwargs)

        return ret
    return wrapper

@my_cache
def add(x=4, y=5):   #add = wrapper add = my_cache(add)
    print('=' * 30)
    time.sleep(1)
    return x + y

print(1, add(4,5))
print(3, add(y=5))
print(4, add(x=4, y=5))

#输出
#(4, 5) {}
#{'x': 4, 'y': 5}
#[('x', 4), ('y', 5)]
#==============================
#1 9
#() {'y': 5}
#{'y': 5, 'x': 4}
#[('x', 4), ('y', 5)]
#==============================
#3 9
#() {'x': 4, 'y': 5}
#{'x': 4, 'y': 5}
#[('x', 4), ('y', 5)]
#==============================
#4 9
# coding: utf-8
#去掉多余注释

import time
from functools import wraps
import inspect

def my_cache(fn):
    local_cache = {}  #每装饰一个函数,都有此函数的独立的cache
    @wraps(fn)
    def wrapper(*args, **kwargs):
        # makekey 处理key
        target = {} #key 字典无序

        sig = inspect.signature(fn)
        params = sig.parameters  # 返回的OrderedDict
        print(args, kwargs)

        # 位置传参 和 关键字传参
        target.update(zip(params.keys(), args), **kwargs)  #将target.update(zip(params.keys(), args)) 和 target.update(kwargs)合一起
        #缺省值 add(y=5) add()
        target.update(map(lambda k:(k, params[k].default), params.keys() - target.keys()))

        key = tuple(sorted(target.items()))
        print(key)

        ret = fn(*args, **kwargs)

        return ret
    return wrapper

@my_cache
def add(x=4, y=5):   #add = wrapper add = my_cache(add)
    print('=' * 30)
    time.sleep(1)
    return x + y

print(1, add(4,5))
print(3, add(y=5))
print(4, add(x=4, y=5))

#输出
#(4, 5) {}
#(('x', 4), ('y', 5))
#==============================
#1 9
#() {'y': 5}
#(('x', 4), ('y', 5))
#==============================
#3 9
#() {'x': 4, 'y': 5}
#(('x', 4), ('y', 5))
#==============================
#4 9
# coding: utf-8
#如何知道缓存了

import time
from functools import wraps
import inspect

def my_cache(fn):
    local_cache = {}  #每装饰一个函数,都有此函数的独立的cache
    @wraps(fn)
    def wrapper(*args, **kwargs):
        # makekey 处理key
        target = {} #key 字典无序

        sig = inspect.signature(fn)
        params = sig.parameters  # 返回的OrderedDict
        print(args, kwargs)

        # 位置传参 和 关键字传参
        target.update(zip(params.keys(), args), **kwargs)  #将target.update(zip(params.keys(), args)) 和 target.update(kwargs)合一起
        #缺省值 add(y=5) add()
        target.update(map(lambda k:(k, params[k].default), params.keys() - target.keys()))

        key = tuple(sorted(target.items()))

        if key not in local_cache.keys():
            local_cache[key] = fn(*args, **kwargs)

        return key, local_cache[key]
    return wrapper

@my_cache
def add(x=4, y=5):   #add = wrapper add = my_cache(add)
    print('=' * 30)
    time.sleep(3)
    return x + y

print(1, add(4,5))
print(3, add(y=5))
print(4, add(x=4, y=5))

#输出
#: (4, 5) {}
#: ==============================
#: 1 ((('x', 4), ('y', 5)), 9)
#: () {'y': 5}
#: 3 ((('x', 4), ('y', 5)), 9)
#: () {'x': 4, 'y': 5}
#: 4 ((('x', 4), ('y', 5)), 9)
多个装饰器
# coding: utf-8
#时间计算

import time, datetime
from functools import wraps
import inspect

def logger(fn):
    print('=' * 30)
    @wraps(fn)
    def wrapper(*args, **kwargs):
        print(1111)
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        delta = (datetime.datetime.now() - start).total_seconds()
        print(fn.__name__, delta)
        print(4444)
        return ret
    return wrapper

def my_cache(fn):
    print('-' * 30)
    local_cache = {}  #每装饰一个函数,都有此函数的独立的cache
    @wraps(fn)
    def wrapper(*args, **kwargs):
        print(2222)
        # makekey 处理key
        target = {} #key 字典无序
        sig = inspect.signature(fn)
        params = sig.parameters  # 返回的OrderedDict

        # 位置传参 和 关键字传参
        target.update(zip(params.keys(), args), **kwargs)  #将target.update(zip(params.keys(), args)) 和 target.update(kwargs)合一起
        #缺省值 add(y=5) add()
        target.update(map(lambda k:(k, params[k].default), params.keys() - target.keys()))
        key = tuple(sorted(target.items()))

        if key not in local_cache.keys():
            local_cache[key] = fn(*args, **kwargs)

        print(3333)
        return key, local_cache[key]
    return wrapper

@logger  # 2 add = logger(add)
@my_cache # 1 add = my_cache(add)
#如果有多个装饰器话,装饰过程,由近及远
def add(x=4, y=5):   #add = my_cache(add) => add = wrapper
    print('~' * 30)
    time.sleep(3)
    return x + y

print(1, add(4,5))
print(3, add(y=5))
print(4, add(x=4, y=5))

#输出
#------------------------------
#==============================
#1111
#2222
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#3333
#add 3.000647
#4444
#1 ((('x', 4), ('y', 5)), 9)
#1111
#2222
#3333
#add 7.9e-05
#4444
#3 ((('x', 4), ('y', 5)), 9)
#1111
#2222
#3333
#add 3.8e-05
#4444
#4 ((('x', 4), ('y', 5)), 9)

多个装饰器,谁在装饰谁? 怎么判断?

离得近的先装饰,离得远的后装饰。可以在装饰器函数里print()不同输出来判断。

调用时从外层向内层调用。先logger再my_cache再add

过期功能实现

清除的时机

设置kv失效时间
1 到点过期
2 相对于当前多少秒过期
  2.1 记录起始时间和持续时长 now - start > duration
  2.2 只记录到点时间  now > end
统一设定5秒过期;也可以对每一个kv对单独设定过期时间

过期时间存储

过期时间放在哪里? kye中,value中?

放在key中无法匹配key了,因为你不知道存储key时的时间,就无法匹配。所以适合存储在value中。

value设置时间
1 key => (v, createtimestamp)
  适合key过期时间都是统一的设定

2 key => (v, createtimestamp, duration)
  duration是过期时间,这样每一个key就可以单独控制过期时间。在这种设计中,-1可以表示永远
  不过期,0可以表示立即过期,正整数表示持续一段时间过期
# coding: utf-8
#value 记录过期时间

import time, datetime
from functools import wraps
import inspect

def logger(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        delta = (datetime.datetime.now() - start).total_seconds()
        print(fn.__name__, delta)
        return ret
    return wrapper

def my_cache(duration=5):
    def _cache(fn):
        local_cache = {}  #每装饰一个函数,都有此函数的独立的cache
        @wraps(fn)
        def wrapper(*args, **kwargs):
            # makekey 处理key
            target = {} #key 字典无序
            sig = inspect.signature(fn)
            params = sig.parameters  # 返回的OrderedDict

            # 位置传参 和 关键字传参
            target.update(zip(params.keys(), args), **kwargs)  #将target.update(zip(params.keys(), args)) 和 target.update(kwargs)合一起
            #缺省值 add(y=5) add()
            target.update(map(lambda k:(k, params[k].default), params.keys() - target.keys()))
            key = tuple(sorted(target.items()))

            if key not in local_cache.keys():
                local_cache[key] = fn(*args, **kwargs), datetime.datetime.now().timestamp()

            return key, local_cache[key]
        return wrapper
    return _cache

@logger  # 2 add = logger(add)
@my_cache() # 1 add = my_cache(5)(add)
#如果有多个装饰器话,装饰过程,由近及远
def add(x=4, y=5):   #add = my_cache(add) => add = wrapper
    print('~' * 30)
    time.sleep(3)
    return x + y

print(1, add(4,5))
print(3, add(y=5))
print(4, add(x=4, y=5))

#输出
如何消除过期时间?
1 懒处理,不满不管;过期了,什么时候用cache,遍历清除
    大小128个kv,提交一个数据,没满,不管,新增或覆盖
        提交一个数据,满了,遍历,但是发现一个过期的,删除过期的,增加这个新kv。退出

2 用一个新的线程经常遍历字典,移除过期的kv。多线程问题,加锁,效率较低。
# coding: utf-8
#清除过期cache

import time, datetime
from functools import wraps
import inspect

def logger(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        delta = (datetime.datetime.now() - start).total_seconds()
        print(fn.__name__, delta)
        return ret
    return wrapper

def my_cache(duration=5):
    def _cache(fn):
        local_cache = {}  #每装饰一个函数,都有此函数的独立的cache
        @wraps(fn)
        def wrapper(*args, **kwargs):
            # 清除cache
            expire_keys  = []
            for k, (_, stamp) in local_cache.items(): # item => (k, v) => (k, (result, stamp))
                now = datetime.datetime.now().timestamp()
                if now - stamp > duration:  # now -stamp 值的范围? 正数 负数 还是 0 ?
                    #有可能出现负数,出错的时候。时间乱掉了
                    #部署环境中,极有可能出现时间乱掉了,可能出现负数,多机部署都需要开启时间同步服务
                    expire_keys.append(k)
            for k in expire_keys:
                local_cache.pop(k)

            # makekey 处理key
            target = {} #key 字典无序
            sig = inspect.signature(fn)
            params = sig.parameters  # 返回的OrderedDict

            # 位置传参 和 关键字传参
            target.update(zip(params.keys(), args), **kwargs)  #将target.update(zip(params.keys(), args)) 和 target.update(kwargs)合一起
            #缺省值 add(y=5) add()
            target.update(map(lambda k:(k, params[k].default), params.keys() - target.keys()))
            key = tuple(sorted(target.items()))

            if key not in local_cache.keys():
                local_cache[key] = fn(*args, **kwargs), datetime.datetime.now().timestamp()

            return key, local_cache[key]
        return wrapper
    return _cache

@logger  # 2 add = logger(add)
@my_cache(duration=1) # 1 add = my_cache(5)(add)
#如果有多个装饰器话,装饰过程,由近及远
def add(x=4, y=5):   #add = my_cache(add) => add = wrapper
    print('~' * 30)
    time.sleep(3)
    return x + y

print(1, add(4,5))
time.sleep(3)
print(2, add(4))
print(3, add(y=5))
print(4, add(x=4, y=5))
print(5, add(y=5, x=4))
print(6, add( ))

#输出
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#add 3.000532
#1 ((('x', 4), ('y', 5)), (9, 1736095337.504812))
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#add 3.000323
#2 ((('x', 4), ('y', 5)), (9, 1736095343.505797))
#add 0.000101
#3 ((('x', 4), ('y', 5)), (9, 1736095343.505797))
#add 5e-05
#4 ((('x', 4), ('y', 5)), (9, 1736095343.505797))
#add 4.1e-05
#5 ((('x', 4), ('y', 5)), (9, 1736095343.505797))
#add 3.5e-05
#6 ((('x', 4), ('y', 5)), (9, 1736095343.505797))

抽象函数

# coding: utf-8
# 清除过期cache

import time, datetime
from functools import wraps
import inspect


def logger(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        delta = (datetime.datetime.now() - start).total_seconds()
        print(fn.__name__, delta)
        return ret
    return wrapper

def _clear(cache,duration):
    # 清除cache
    expire_keys = []
    for k, (_, stamp) in cache.items():  # item => (k, v) => (k, (result, stamp))
        now = datetime.datetime.now().timestamp()
        if now - stamp > duration:  # now -stamp 值的范围? 正数 负数 还是 0 ?
            # 有可能出现负数,出错的时候。时间乱掉了
            # 部署环境中,极有可能出现时间乱掉了,可能出现负数,多机部署都需要开启时间同步服务
            expire_keys.append(k)
    for k in expire_keys:
        cache.pop(k)

def _make_key(fn, args, kwargs):
    # makekey 处理key
    target = {}  # key 字典无序
    sig = inspect.signature(fn)
    params = sig.parameters  # 返回的OrderedDict

    # 位置传参 和 关键字传参
    target.update(zip(params.keys(), args),
                  **kwargs)  # 将target.update(zip(params.keys(), args)) 和 target.update(kwargs)合一起
    # 缺省值 add(y=5) add()
    target.update(map(lambda k: (k, params[k].default), params.keys() - target.keys()))
    return tuple(sorted(target.items()))

def my_cache(duration=5):
    def _cache(fn):
        local_cache = {}  # 每装饰一个函数,都有此函数的独立的cache
        @wraps(fn)
        def wrapper(*args, **kwargs):
            _clear(local_cache, duration)
            key = _make_key(fn, args, kwargs)
            if key not in local_cache.keys():
                local_cache[key] = fn(*args, **kwargs), datetime.datetime.now().timestamp()
            return key, local_cache[key]
        return wrapper
    return _cache

@logger  # 2 add = logger(add)
@my_cache(duration=1)  # 1 add = my_cache(5)(add)
# 如果有多个装饰器话,装饰过程,由近及远
def add(x=4, y=5):  # add = my_cache(add) => add = wrapper
    print('~' * 30)
    time.sleep(3)
    return x + y

print(1, add(4, 5))
time.sleep(3)
print(2, add(4))
print(3, add(y=5))
print(4, add(x=4, y=5))
print(5, add(y=5, x=4))
print(6, add())

#输出
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#add 3.000611
#1 ((('x', 4), ('y', 5)), (9, 1736096540.518239))
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#add 3.000949
#2 ((('x', 4), ('y', 5)), (9, 1736096546.519906))
#add 0.000248
#3 ((('x', 4), ('y', 5)), (9, 1736096546.519906))
#add 0.000143
#4 ((('x', 4), ('y', 5)), (9, 1736096546.519906))
#add 0.000114
#5 ((('x', 4), ('y', 5)), (9, 1736096546.519906))
#add 0.000128
#6 ((('x', 4), ('y', 5)), (9, 1736096546.519906))

装饰器的用途

装饰器是AOP面向切面编程 Aspect Oriented Programming的思想的体现。

面向对象往往需要通过继承或者组合依赖等方式调用一些功能,这些功能的代码往往可能在多个类中出现,
例如logger功能代码。这样造成代码的重复,增加了耦合。logger的改变影响所有使用它的类或方法。

而AOP在需要的类或方法上切下,前后的切入点可以加入增强的功能。让调用者和被调用者解耦。

这是一种不修改原来的业务代码,给程序动态添加功能的技术。
例如logger函数就是对业务函数增加日志的功能,而业务函数中应该把与业务无关的日志功能剥离干净。

装饰器的应用场景

  • 日志、监控、权限、审计、参数检查、路由等处理。这些功能与业务功能无关,是很多业务都需要的公有的功能,所以适合独立出来,需要的时候,对目标对象进行增强。

Python 面向对象编程

主要内容

  • 面向对象的分析与设计 OOA&D
  • 面向对象三要素
  • 类的实现方法
  • 类属性定义
  • 类方法和静态方法
  • 类成员的本质
  • 类实例化含义
  • 实例仳的过程
  • Self 的理解与使用
  • 实例属性
  • 子类与继承
  • 子类实例化与构造器调用
  • Super 的理解与使用
  • Overload 与 Override
  • 新式类和古典类
  • 多重继承
  • 多重继承的歧义与解决
  • 多继承应用 Mixin
  • 成员访问控制

语言的分类

面向机器 -抽象成机器指令,机器容易理解

  • 代表:汇编语言

面向过程

  • 问题规模小,可以步骤化,按部就班处理
  • 代表:C语言

面向对象OOP

  • 随着计算机需要解决的问题的规模扩大,情况越来越复杂。需要很多人、很多部门协作,面向过程编程不太合适了
  • 代表:C++、Java、Python等

面向对象

什么是面向对象?

面向对象是一种认识世界、分析世界的方法论。将万事万物抽象为各种对象

类class

  • 类是抽象的概念,是万事万物的抽象,是一类事物的共同特征的集合
  • 用计算机语言来描述类,是 属性方法 的集合

对象instance、object

  • 对象是类的具象,是一个实体
  • 对于我们每个人这个个体,都是抽象概念人类的不同的 实体

范例

你吃鱼
你,就是对象;鱼,也是对象;吃就是动作
你是具体的人,是具体的对象。你属于人类,人类是个抽象的概念,是无数具体人的个体的抽象。
鱼,也是具体的对象,就是你吃了具体的鱼。这条鱼属于鱼类,鱼类是无数的鱼抽象出来的概念。
吃,是动作,也是操作,也是方法,这个吃是你的动作,也就是人类具体的方法。如果反过来,鱼吃人。吃就是鱼类的动作了。
吃,这个动作,很多动物都具有的动作,人类和鱼类都属于动物类,而动物类是抽象的概念,是动物都有
吃的动作,但是吃法不同而已。
你驾驶汽车,这个车也是车类的具体的对象(实例),驾驶这个动作是鱼类不具有的,是人类具有的方法。
  • 属性: 它是对象状态的抽象,用数据结构来描述
  • 操作: 它是对象行为的抽象,用操作名和实现该操作的方法来描述
每个人都是人类的一个单独的实例,都有自己的名字、身高、体重等信息,这些信息是个人的属性,
但是,这些信息不能保存在人类中,因为人类是抽象的概念,不能保留每个具体的个体的值。
而人类的实例,是具体的人,他可以储存这些具体的属性,而且可以不同人有不同的属性。

哲学

  • 一切皆对象
  • 对象是数据和操作的封装
  • 对象是独立的,但是对象之间可以相互作用
  • 目前OOP是最接近人类认知的编程范式

面向对象3要素

1. 封装

  • 组装:将数据和操作组装到一起
  • 隐藏数据:对外只暴露一些接口,通过接口访问对象。比如驾驶员使用汽车,不需要了解汽车的机动原理,会用就行

2. 继承

  • 多复用,继承来的就不用自己写了
  • 多继承少修改,OCP(Open-closed Principle),使用继承来改变,来体现个性

3. 多态

  • 面向对象编程最灵活的地方,动态绑定
人类就是封装;
人类继承自动物类,孩子继承父母特征。分为单一继承、多继承;
多态,继承自动物类的人类、猫类的操作“吃”不同。

封装

封装就是定义类,将属性和操作组织在一起。

Python类定义

class ClassName:
    语句块
  1. 必须使用class关键字
  2. 类名强烈建议使用 大驼峰 命名方式,即每个单词首字母大写。其本质就是一个标识符
  3. 类定义完成后,就产生了一个 类对象 ,绑定到了标识符ClassName上

范例

class Person: #__name__ 定义执行之后,会得到一个类对象
    """An example of class"""
    def showme(self): # 一个函数定义在类中,习惯称为类方法。成员方法
        "Showme method"
        #方法不能直接在外部调用,需要借助限定名
        return __name__ #?

#取类的相关属性
print(Person) #返回类对象 <class '__main__.Person'>
print(Person.__name__) #返回类名,str Person 成员属性 double under双下划线
print(Person.__class__, type(Person)) #对类对象取type,两种写法是一回事。<class 'type'> <class 'type'>
print(Person.__doc__) #类的文档 An example of class
print(Person.showme) #<function Person.showme at 0x00000173E7C07A60>
print(Person.showme.__qualname__) #限定名 Person.showme
print(Person.showme.__doc__) #文档 Showme method
#类的成员都是通过.运算符访问的,所以类的成员也可以叫类的属性或者类的变量,只是有的存的数据有的存的函数对象
#.运算符,成员运算符,类的成员或实例的成员
#到目前为止,有Person实例吗?没有实例

tom = Person() #实例化

举例

class Person:
    """An exapmle class"""
    x = 'abc' # 类属性

    def showme(self): # 方法,也是类属性
        return __name__ #返回类的名称

print(Person)
print(Person.__name__) #类名字
print(Person.__doc__) #类文档
print(Person.showme) #类属性

类及类属性

  • 类对象, 类也是对象,类的定义执行后会生成一个类对象
  • 类的属性, 类定义中的变量和类中定义的 方法 都是类的属性
  • 类变量, 上例中x是类MyClass的变量

Person中,x、showme都是类的属性, __name____doc__ 是类的特殊属性

showme方法是类的属性,如同 人类的方法 ,但是 每一个具体的人才能吃东西 ,也就是说 是人的 实例 能调用的方法。

showme是 方法method ,本质上就是普通的函数对象function,它一般要求至少有一个参数。第一个形式参数可以是 self(self只是个惯用标识符,不推荐修改),这个参数位置就留给了self。

self 指代当前实例本身

实例化

a = Person() # 实例化

使用上面的语法,在类对象名称后面加上一个括号,就调用类的实例化方法,完成实例化。

实例化就真正创建一个该类的对象(实例)。例如

tom = Person() #不同的实例
jack = Person() #不同的实例

上面的tom、jack 都是Person类的实例,通过实例化生成了2个 不同的 实例。

每次实例化后获得 不同的实例 ,即使是使用同样的参数实例化,也得到不一样的对象。

Python类实例化后,会自动调用 __init__ 方法。这个方法第一个形式参数必须留给self,其它参数随意。

范例

class Person: #__name__ 定义执行之后,会得到一个类对象
    """An example of class"""
    x = 'abc'
    def showme(self): # 一个函数定义在类中,习惯称为类方法。成员方法
        "Showme method"
        #方法不能直接在外部调用,需要借助限定名
        print('showme ++++++')
        return __name__ #?

    def __init__(self, name, age=18): #self永远指向当前实例自身,java\c++\javascript用的是this
        print('init ~~~~~~~')
        self.name = name #self ,当前实例,实例属性
        self.age = age

#以下都是在访问类属性
print(Person) #返回类对象 <class '__main__.Person'>
print(Person.__name__) #返回类名,str Person 成员属性 double under双下划线
print(Person.__class__, type(Person)) #对类对象取type,两种写法是一回事。<class 'type'> <class 'type'>
print(Person.__doc__) #类的文档 An example of class
print(Person.showme) #<function Person.showme at 0x00000173E7C07A60>
print(Person.showme.__qualname__) #限定名 Person.showme
print(Person.showme.__doc__) #文档 Showme method
print(Person.x) #类的属性 abc

tom = Person('Tom') #init ~~~~~~~
jerry = Person('Tom') #init ~~~~~~~
print(tom.name, tom.age) #Tom 18
print(jerry.name, jerry.age) #Tom 18
print(id(tom), id(jerry)) #不是同一个对象 1728622263472 1728624259984
构造的2个阶段
  1. 构造实例过程,实例化。如生产线上生成一辆车。调用魔术方法 __new__ 返回一个实例
  2. 初始化过程,初始化。如对这新车做出场配置。魔术方法 __init__ 方法中内容就是出场配置

但是需要注意的是,很多人习惯混用这些词,来代表两个阶段的总称。

范例

class Person: #__name__ 定义执行之后,会得到一个类对象
    """An example of class"""
    x = 'abc'
    def showme(self): # 一个函数定义在类中,习惯称为类方法。成员方法
        "Showme method"
        #方法不能直接在外部调用,需要借助限定名
        print('showme ++++++')
        return __name__ #?

    def __init__(self, name, age=18): #self永远指向当前实例自身,java\c++\javascript用的是this
        print('init ~~~~~~~')
        self.name = name #self ,当前实例,实例属性
        self.age = age

#Python中实例化分2个阶段
#1 构造实例过程,实例化。如生产线上生成一辆车。调用魔术方法 __new__ 返回一个实例
#2 初始化过程,初始化。如对这新车做出场配置。魔术方法 __init__ 方法中内容就是出场配置
#不严谨叫法,混用这些词

tom = Person('Tom', 20) #等式右边做2个阶段的事情:实例化和初始化
jerry = Person('Tom') #init ~~~~~~~
print(tom.name, tom.age) #Tom 20
print(jerry.name, jerry.age) #Tom 18
print(id(tom), id(jerry)) #不是同一个对象 1728622263472 1728624259984
__init__方法

有些人把Python的 __init__ 方法称为构造方法或者构造器。

Person() 实例化后,要初始化,要调用的是 __init__(self) 方法,可以不定义,如果没有定义会在实例化 后 隐式 调用其父类的。

作用:对实例进行 初始化

__init___ 方法不能有返回值,只能 return None

class MyClass:
    def __init__(self):
        print('init')

print(MyClass) # 不会调用
print(MyClass()) # 调用__init__
a = MyClass() # 调用__init__
  • 初始化函数可以多个参数,请注意第一个位置必须是self,例如 __init__(self, name, age)
class Person: #__name__ 定义执行之后,会得到一个类对象
    """An example of class"""

    def __init__(self, name, age=18): #self永远指向当前实例自身,java\c++\javascript用的是this
        print('init ~~~~~~~')
        self.name = name #self ,当前实例,实例属性
        self.age = age

    def showage(self):
        print("{} is {}".format(self.name, self.age))

tom = Person('Tom', 20) #等式右边做2个阶段的事情:实例化和初始化
jerry = Person('jerry') #init ~~~~~~~

print(tom.name, tom.age) #Tom 20
print(jerry.name, jerry.age) #Tom 18

#使用限定名访问showage
tom.showage() #类的实例tom来访问实例属性,可以。self 指定 tom 返回Tom is 20
jerry.showage() #jerry is 18
jerry.age += 1
实例对象instance
  • 上例中,Person类实例化后一定会获得一个类的实例,就是 实例对象
  • 上例中的tom、jerry就是Person类的实例

__init__ 方法的第一参数 self 就是指代某一个实例自身

  • 执行 Person('Tom' 20) 时,调用 __name__() 方法。 self.name就是tom对象的name, name是保存在了tom对象上,而不是Person类上。所以,称为 实例变量
  • 类实例化后,得到一个实例对象,调用方法时采用tom.showage()的方式, 但是showage方法的形参需要一个形参self,我们并没有提供,并没有报错,为什么?
    • 因为是通过tom点来做的,tom虽然是限定名,但也已经说了是哪个实例在调用, 这个self就是tom,tom.showage()调用时,由解释器把方法的调用者tom实例作为第一参数self的实参传入。
方法绑定

采用tom.showage()的方法调用时,实例对象会 绑定 到方法上。这个self就是tom, 指向当前调用该方法的实例本身

tom.showage()调用时,会把方法的调用者tom实例作为第一参数self的实参传入 __init__() 方法.

范例

class Person: #__name__ 定义执行之后,会得到一个类对象
    """An example of class"""

    def __init__(self, name, age=18): #self永远指向当前实例自身,java\c++\javascript用的是this
        print('init ~~~~~~~')
        self.name = name #self ,当前实例,实例属性
        self.age = age

    def showage(self):
        print("{} is {}".format(self.name, self.age))

tom = Person('Tom', 20) #等式右边做2个阶段的事情:实例化和初始化
jerry = Person('jerry') #init ~~~~~~~

print(tom.showage)
#用实例.showage(showage定义在类中,showage是类属性),python会得到一个绑定bound method
#将tom实例绑定到了类属性showage方法上了
#当你调用该方法时,解释器把第一参数self使用实参tom注入
print(id(tom), hex(id(tom)))
tom.showage() #相当于调用 showage(tom) 解释器自己的行为
# <bound method Person.showage of <__main__.Person object at 0x000001FE76C27CB0>>
# 2192425778352 0x1fe76c27cb0
self
class Person: #__name__ 定义执行之后,会得到一个类对象
    def __init__(self, name, age=18): #self永远指向当前实例自身,java\c++\javascript用的是this
        print(1, 'init ~~~~', id(self))
        self.name = name #self ,当前实例,实例属性
        self.age = age

    def showme(self):
        print(2, 'who am i = {}'.format(id(self)))

tom = Person('Tom')
print(3, tom.name, id(tom))
tom.showme() #绑定tom访问

#返回
# 1 init ~~~~ 2004157758976
# 3 Tom 2004157758976
# 2 who am i = 2004157758976

上例说明,self就是调用者,就是tom对应的实例对象。

self这个名字只是一个惯例,它可以修改,但是请不要修改,否则影响代码的可读性

实例变量和类变量

class Person: #__name__ 定义执行之后,会得到一个类对象
    age = 3
    def __init__(self, name): #self永远指向当前实例自身,java\c++\javascript用的是this
        print('init ~~~~')
        self.name = name #self ,当前实例,实例属性

tom = Person('tom')
print(tom.name, tom.age) #和tom没有age,所以找到了类属性 3

jerry = Person('jerry')
print(jerry.name, jerry.age)

Person.age = 30  #重新定义类属性
print(Person.age, tom.age, jerry.age)

#返回结果
# init ~~~~
# tom 3
# init ~~~~
# jerry 3
# 30 30 30

#类的属性,实例可以共享。是类的,就是大家的
#
  • 实例变量是每一个实例自己的变量,是自己独有的
  • 类变量是类的变量,是类的所有实例共享的属性和方法

特殊属性

__name__  对象名
__class__  对象类型
__dict__  对象的属性的字典
__qualname__ 类的限定名

注意: Python中每一种对象都拥有不同的属性。函数、类都是对象,类的实例也是对象。

属性的本质

举例

class Person: #__name__ 定义执行之后,会得到一个类对象
    age = 3
    def __init__(self, name): #self永远指向当前实例自身,java\c++\javascript用的是this
        print('init ~~~~')
        self.name = name #self ,当前实例,实例属性

print('class info~~~~~')
print(Person) #<class '__main__.Person'>
print(Person.__name__) #Person
print(Person.__class__, type(Person), Person.__class__ is type(Person)) #同一类型 <class 'type'> <class 'type'> True
print(Person.__dict__) #类的字典,看到了age、__doc__、__init__
# {'__module__': '__main__', '__firstlineno__': 1, 'age': 3, '__init__': <function Person.__init__ at 0x0000019AAD044680>, '__static_attributes__': ('name',), '__dict__': <attribute '__dict__' of 'Person' objects>, '__weakref__': <attribute '__weakref__' of 'Person' objects>, '__doc__': None}
# 上面所有的key都是字符串
print(Person.__init__) # 标识符,成员运算符.来访问,这种叫属性访问或者成员访问 <function Person.__init__ at 0x0000019AAD044680>
print(Person.__init__.__name__) #__init__的名字是字符串  __init__

tom = Person('tom')
#类的属性,实例可以共享。是类的,就是大家的。类属性在类字典中存储
#实例属性,存在实例的字典中,我的就是我的。我没有,问类要,类没有,抛异常AttributeError。
print(tom) # <__main__.Person object at 0x000002299F947CB0>
print(tom.__class__, type(tom), tom.__class__ is type(tom)) #<class '__main__.Person'> <class '__main__.Person'> True
print(type(tom).__name__) #Person
print(tom.__class__.__dict__) #访问实例的类型的字典
# {'__module__': '__main__', '__firstlineno__': 1, 'age': 3, '__init__': <function Person.__init__ at 0x0000019AAD044680>, '__static_attributes__': ('name',), '__dict__': <attribute '__dict__' of 'Person' objects>, '__weakref__': <attribute '__weakref__' of 'Person' objects>, '__doc__': None}
print('=' * 30)
print(tom.__dict__) #实例字典存储实例自己的属性 {'name': 'tom'}

jerry = Person('jerry')
print(jerry.__dict__) #{'name': 'jerry'}

上例中,可以看到类属性保存在类的 __dict__ 中,实例属性保存在实例的 __dict__ 中,如果从实例访问类的属性, 也可以借助 __class__ 找到所属的类,再通过类来访问类属性,例如 tom.__class__.age

class Person:
    age = 3
    height = 170

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

tom = Person('Tom') # 实例化、初始化
jack = Person('jack', 20)

Person.age = 30
print(1, Person.age, tom.age, jack.age)  #  30, 18, 20
print(2, Person.height, tom.height, jack.height) # 170, 170, 170
jack.height = 175 #赋值即定义,实例属性,会放在实例字典中
print(3, Person.height, tom.height, jack.height) # 170, 170, 175

tom.height += 10 #等式右边类的属性值加10,赋值给实例属性
print(4, Person.height, tom.height, jack.height) # 170, 180, 175

Person.height += 15
print(5, Person.height, tom.height, jack.height) # 185, 180, 175

Person.weight = 70 #类属性,放在类字典中
print(6, Person.weight, tom.weight, jack.weight) # 70, 70, 70

print(7, tom.__dict__['height']) # 180
# print(8, tom.__dict__['weight']) # KeyError
总结
  1. 是类的,也是这个类所有实例的,其实例都可以访问
  2. 是实例的,就是这个实例自己的,通过类访问不到
  3. 类变量是属于类的变量,这个类的所有实例可以 共享 这个变量
  4. 对象(实例或类)可以动态的给自己增加一个属性(赋值即定义一个新属性)
  5. 实例.__dict__[变量名]实例.变量名 都可以访问到实例自己的属性(注意这两种访问是有本质区别的)
  6. 实例的同名变量会 隐藏 掉类变量,或者说是覆盖了这个类变量。但是注意类变量还在那里,并没有真正被覆盖

实例属性的查找顺序

  • 指的是实例使用 .点号 来访问属性,会先找自己的 __dict__, 如果没有,然后通过属性 __class__ 找到自己的类,再去类的 __dict__ 中找

注意:如果实例使用 __dict__[变量名] 访问变量,将不会按照上面的查找顺序找变量了,这是指明使用 字典的key 查找,不是属性查找

一般来说, 类变量可使用全大写来命名

类方法和静态方法

前面的例子中定义的 __init__ 等方法,这些方法本身都是类的属性,第一个参数必须是self,而self必须指向一个对象,也就是类实例化之后,由实例来调用这个方法。

装饰一个类

需求,为一个类通过装饰,增加一些类属性。例如能否给一个类增加一个NAME类属性并提供属性值

# 增加类变量
def add_name(name, cls):
    cls.NAME = name # 动态增加类属性

# 改进装饰器
def add_name(name):
    def wrapper(cls):
        cls.NAME = name
        return cls
    return wrapper

@add_name('Tom')
class Person:
    AGE = 3

print(Person.NAME)
  • 之所以能够装饰,本质上是为类对象动态的添加了一个属性,而Person这个标识符指向这个类对象
普通方法
class Person:
    # def method(self):
    #     print('普通方法, 实例方法', self)
    def normal_function():
        print('普通方法')

Person.normal_function() #返回 普通方法
#1 Person.normal_function 找类属性,找到了
#2 fn() 对的,没问题,因为无参,一个使用类限定名的普通函数调用罢了

Person().normal_function() #报错TypeError: Person.normal_function() takes 0 positional arguments but 1 was given
#1 Person().normal_function 实例.属性  实例属性的搜索规则,找不到去类上找该属性,fn
#由于normal_function是类属性,然后通过实例调用该类属性,所以返回不仅仅是一个普通函数
#是boud method  self -> Person()
#2 Person().normal_function() 调用是实际解释器为你注入第一个参数Person()
#Person().normal_function他没有形参,Person()无法可注入

注意:虽然语法是对的,但是,没有人么用,也就是说 禁止 这么写

类方法
class Person:
    # def method(self):
    #     print('普通方法, 实例方法', self)
    # def normal_function():
    #     print('普通方法')
    @classmethod #内建函数,装饰器内部获取了类型信息,并绑定这个类型
    def cls_mtd(cls): #类属性
        print('类方法',cls) #注入类型

Person.cls_mtd() #用类调用方法,可以 #类方法 <class '__main__.Person'>
Person().cls_mtd() #用的实例.cls_mtd(), 可以 #类方法 <class '__main__.Person'>
#有注入
print(Person.cls_mtd) #绑定类 <bound method Person.cls_mtd of <class '__main__.Person'>>
print(Person().cls_mtd) #本来绑定实例的类型 <bound method Person.cls_mtd of <class '__main__.Person'>>
  1. 在类定义中,使用 @classmethod 装饰器修饰的方法
  2. 必须至少有一个参数,且第一个参数留给了cls,cls指代调用者即类对象自身
  3. cls这个标识符可以是任意合法名称,但是为了易读,请不要修改
  4. 通过cls可以直接操作类的属性
  5. 注意:无法通过cls操作类的实例

    类方法,类似于C++、Java中的静态方法

静态方法
class Person:
    def method(self):
        print('普通方法, 实例方法', self)
    # def normal_function():
    #     print('普通方法')
    # @classmethod #内建函数,装饰器内部获取了类型信息,并绑定这个类型
    # def cls_mtd(cls): #类属性
    #     print('类方法',cls) #注入类型

    @staticmethod #装饰器,内建函数。调用函数,就没有绑定效果,不会注入第一参数
    def static_mtd():
        print('静态方法~~~~')

Person.static_mtd() #
Person().static_mtd() #
print(Person.static_mtd) #<function Person.static_mtd at 0x00000284C59E39C0>
print(Person().static_mtd) #<function Person.static_mtd at 0x00000284C59E39C0>
print(Person.__dict__)
#{'__module__': '__main__', '__firstlineno__': 1, 'static_mtd': <staticmethod(<function Person.static_mtd at 0x00000284C59E39C0>)>, '__static_attributes__': (), '__dict__': <attribute '__dict__' of 'Person' objects>, '__weakref__': <attribute '__weakref__' of 'Person' objects>, '__doc__': None}


tom = Person()
tom.method() #返回 普通方法, 实例方法 <__main__.Person object at 0x000001E98D997390>
print(Person.method) #<function Person.method at 0x000001E98D9C39C0>
print(Person().method) #返回 <bound method Person.method of <__main__.Person object at 0x000001E98D829E00>>
Person.method(Person()) #返回 普通方法, 实例方法 <__main__.Person object at 0x000001E98D829E00>
Person.method(str) #返回 普通方法, 实例方法 <class 'str'>
Person.method([1,2,3]) #返回 普通方法, 实例方法 [1, 2, 3]
Person.method(tom) #普通访问,没有绑。但是效果和 tom.method() 等价
  1. 在类定义中,使用 @staticmethod 装饰器修饰的方法
  2. 调用时,不会隐式的传入参数

    静态方法,只是表明这个方法属于这个名词空间。函数归在一起,方便组织管理

方法的调用
  • 类几乎可以调用所有内部定义的方法,但是调用普通的方法时会报错,原因是第一参数必须是类的实例
  • 实例也几乎可以调用所有的方法,普通的函数的调用一般不可能出现,因为不允许这么定义

总结:

  • 类除了普通方法都可以调用,普通方法需要对象的实例作为第一参数
  • 实例可以调用所有类中定义的方法(包括类方法、静态方法),普通方法传入实例自身,静态方法和类方法需要找到实例的类
class Person:
    def method(self):
        print("{}".format(self))
        print("{}".format(__class__))
        print("{}".format(__class__.__name__))
        print("{}".format(__name__))

tom = Person()
tom.method()
Person.method(1)
Person.method(tom)
tom.__class__.method(tom)
  • tom.method()调用的时候,会绑定实例,调用method方法时,实例tom会注到method中,这样第一参数就满足了。
  • Person.method(),使用类调用,不会有实例绑定,调用method方法时,就缺少了第一参数,可以手动的填入

访问控制

class Person:
    def __init__(self, name, age=18):
        self.name = name
        self.age = age

    def growup(self, i=1):
        if i > 0 and i < 150: # 控制逻辑
            self.age += 1

p1 = Person('tom')
p1.growup(20) # 正常的范围
print(p1.age) # 19
p1.age = 160 # 超过了范围,并绕过了控制逻辑
print(p1.age) # 160

上例,本来是想通过方法控制属性,但是由于属性在外部可以访问,或者说可见,就可以 直接绕过方法,直接修改这个属性。

Python提供了私有属性可以解决这个问题

私有(Private)属性

使用 __ 双下划线开头的属性名,就是私有属性

class Person:
    def __init__(self, name, age=18):
        self.name = name
        self.__age = age

    def growup(self, i=1):
        if i > 0 and i < 150: # 控制逻辑
            self.__age += 1

p1 = Person('tom')
p1.growup(20) # 正常的范围
# print(p1.__age) # 访问不到
  • 通过实验可以看出,外部已经访问不到 __age 了,age根本就没有定义,更是访问不到。
  • 那么,如何访问这个私有变量 __age 呢?

使用方法访问

class Person:
    def __init__(self, name, age=18):
        self.name = name
        self.__age = age

    def growup(self, i=1):
        if i > 0 and i < 150: # 控制逻辑
            self.__age += 1

    def getage(self):
        return self.__age

print(Person('tom').getage()) # 18
私有成员

在Python中,在类变量或实例变量前使用两个下划线的变量,称为私有成员,包括私有属性、私有方法。

class Person:
    def __init__(self, name, age=18):
        self.name = name
        self.__age = age #属性使用了双下划线加属性名的方式,self._Person__age = age

    def __growup(self, i=1): # _Person__growup
        if i > 1 and i < 150: # 控制逻辑
            self.__age += 1 # self._Person__age += 1
        return self.__age

    def getage(self): #public
        return self.__age #private 等价 self._Person__age

tom = Person('tom')
print(tom.getage()) #18
#print(tom.age) #报错 AttributeError: 'Person' object has no attribute 'age'
#print(tom.__age) #报错 AttributeError: 'Person' object has no attribute '__age'
#tom.__growup(20) #报错 AttributeError: 'Person' object has no attribute '__growup'

tom.__age = 101 #赋值即定义,没有改名,因为在类外。
print(tom.__age) #101
print(tom.__dict__) #{'name': 'tom', '_Person__age': 18, '__age': 101}
print(tom.getage()) #18
私有变量的本质
#Python对象模型使用了字典
#私有成员 __成员名,如果在类中定义,都会被解释器改名,_当前类+私有成员
#使用上面的改名方案之后,造成一种在类外无法使用 限定名.__成员名 访问到属性的现象,隐藏。
#体会一下,什么叫做私有,不公开的。离开了类定义环境,不能访问
#Python的类字典、实例字典是公开的,所以可以访问。但是其他高级语言,私有的无法访问。
#不到万不得以,不要用这种方式访问
class Person:
    def __init__(self, name, age=18):
        self.name = name
        self.__age = age #属性使用了双下划线加属性名的方式,self._Person__age = age

    def __growup(self, i=1): # _Person__growup
        if i > 1 and i < 150: # 控制逻辑
            self.__age += 1 # self._Person__age += 1
        return self.__age

    def getage(self): #public
        return self.__age #private 等价 self._Person__age

tom = Person('tom')
print(tom.getage()) #18
#print(tom.age) #报错 AttributeError: 'Person' object has no attribute 'age'
#print(tom.__age) #报错 AttributeError: 'Person' object has no attribute '__age'
#tom.__growup(20) #报错 AttributeError: 'Person' object has no attribute '__growup'

tom.__age = 101 #赋值即定义,没有改名,因为在类外。
print(tom.__age) #101
print(tom.__dict__) #{'name': 'tom', '_Person__age': 18, '__age': 101}
print(tom.getage()) #18

#私有的在外部就不要这种方式访问,用公开的
print(tom.__class__.__dict__)
#{'__module__': '__main__', '__firstlineno__': 4, '__init__': <function Person.__init__ at 0x0000028065194680>, '_Person__growup': <function Person.__growup at 0x0000028065194D60>, 'getage': <function Person.getage at 0x00000280652F71A0>, '__static_attributes__': ('__age', 'name'), '__dict__': <attribute '__dict__' of 'Person' objects>, '__weakref__': <attribute '__weakref__' of 'Person' objects>, '__doc__': None}
print(tom._Person__growup(50)) #19

秘密都在 __dict__ 中,原来私有成员都被改名了。

私有变量的本质:

  • 类定义的时候,如果声明一个实例变量的时候,使用双下划线,Python解释器会将其 改名 ,转换名称为 _类名__变量名 的名称,使用原来的名字访问不到了

使用私有变量变更名,修改其属性

class Person:
    def __init__(self, name, age=18):
        self.name = name
        self.__age = age

    def __growup(self, i=1):
        if i > 0 and i < 150: # 控制逻辑
            self.__age += 1

    def getage(self):
        return self.__age

p1 = Person('tom')

# 直接修改私有变量
p1._Person__age = 15
print(p1.getage()) # 15
print(p1.__dict__) # {'name': 'tom', '_Person__age': 15, '__age': 28}
保护成员

在变量名前使用一个 _ 下划线,称为保护成员。

class Person:
    def __init__(self, name, age=18):
        self._name = name
        self.__age = age

    def _getname(self): # 没改名
        return self._name

    def getage(self): # 
        return self.__age

tom = Person('tom')
print(tom._name, tom._getname())  #tom tom
print(tom.__dict__) #  {'_name': 'tom', '_Person__age': 18}
print(Person.__dict__)
# {'__module__': '__main__', '__firstlineno__': 1, '__init__': <function Person.__init__ at 0x00000146586D1440>, '_getname': <function Person._getname at 0x00000146588AC540>, 'getage': <function Person.getage at 0x0000014658914F40>, '__static_attributes__': ('__age', '_name'), '__dict__': <attribute '__dict__' of 'Person' objects>, '__weakref__': <attribute '__weakref__' of 'Person' objects>, '__doc__': None}
  • 可以看出,保护成员没有改名,解释器不做任何特殊处理。
  • 这只是开发者共同的约定,看见这种变量,就如同私有变量,不要直接使用。
访问控制的总结

在Python中使用 _=单下划线 或者 =__ 双下划线来标识一个成员被保护或者被私有化隐藏起来。

但是,不管使用什么样的访问控制,都不能真正的阻止用户修改类的成员。Python中没有绝对的安全的保护成员或者私有成员。

因此,前导的下划线只是一种警告或者提醒,遵守这个约定。除非真有必要,不要修改或者使用保护成员或者私有成员,更不要修改它们。

补丁

可以通过修改或者替换类的成员。使用者调用的方式没有改变,但是,类提供的功能可能已经改变了。

猴子补丁(Monkey Patch): (慎用)

  • 在运行时,对属性、方法、函数等进行动态替换。
  • 其目的往往是为了通过替换、修改来增强、扩展原有代码的能力。
# t1.py
class Person:
    def get_score(self):
        # from mysql数据库
        return {'English':25, 'Chinese':20}

# t2.py
def get_score(self):
    return {'English':80, 'Chinese':76}

# t3.py
from t1 import Person
from t2 import get_score

def mokeypatch4Person(cls):
    cls.get_score = get_score

mokeypatch4Person(Person)

if __name__ == "__main__":
    print(Person().get_score()) 

# 打印结果
{'English': 80, 'Chinese': 76}
  • 上例中,假设Person类get_score方法是从数据库拿数据,但是测试的时候,不方便
  • 为了测试时方便,使用猴子补丁,替换了get_score方法,返回模拟的数据

属性装饰器

一般好的设计是:把实例的某些属性保护起来,不让外部直接访问,外部使用 getter 读取属性和 setter 方法设置属性。

  • Python提供了属性property装饰器
class Person:
    def __init__(self, name, age=18):
        self.name = name
        self.__age = age

    @property # 只读
    def age(self):
        return self.__age

    @age.setter # 可写
    def age(self, age):
        self.__age = age

    @age.deleter
    def age(self):
        # del self.__age
        print('del')

tom = Person('Tom')
print(tom.age) #用到了getter 18
tom.age = 20  #用到了stter
print(tom.__dict__) # {'name': 'Tom', '_Person__age': 20}
print(tom.age) # 20
del tom.age # 执行@age.deleter下函数,打印del

特别注意: 使用property装饰器的时候这三个方法同名

property装饰器

  • 后面跟的函数名就是以后的属性名。它就是getter。这个必须有,有了它至少是只读属性
  • setter装饰器
    • 与属性名同名,且接收2个参数,第一个是self,第二个是将要赋值的值。有了它,属性可写
  • deleter装饰器
    • 可以控制是否删除属性。很少用
  • property装饰器必须在前,setter、deleter装饰器在后
  • property装饰器能通过简单的方式,把对方法的操作变成对属性的访问,并起到了一定隐藏效果

其它的写法

class Person:
    def __init__(self, name, age=18):
        self.name = name
        self.__age = age

    def getage(self):
        return self.__age

    def setage(self, age):
        self.__age = age

    def delage(self):
        # def self.__age
        print('del')

    age = property(getage, setage, delage, 'age property')

tom = Person('Tom')
print(tom.age) # 18
tom.age = 20
print(tom.__dict__) # {'name': 'Tom', '_Person__age': 20}
print(tom.age) # 20
del tom.age # del

还可以如下

 class Person:
    def __init__(self, name, age=18):
        self.name = name
        self.__age = age

    age = property(lambda self:self.__age)

tom = Person('Tom')
print(tom.age) # 18

对象的销毁

  • 类中可以定义 __del__ 方法,称为析构函数(方法)
  • 作用:销毁类的实例的时候调用,以释放占用的资源。其中就放些清理资源的代码,比如释放连接
  • 注意这个方法不能引起对象的真正销毁,只是对象销毁的时候会自动调用它
  • 使用del语句删除实例,引用计数减1。当引用计数为0时,会自动调用 __del__ 方法。 由于Python实现了垃圾回收机制,不能确定对象何时执行垃圾回收
class Person:
    def __init__(self, name, age=18):
        self.name = name
        self.__age = age

    def __del__(self):
        print('delete {}'.format(self.name))

tom = Person('tom')
tom.__del__() # delete tom
print('======statr======')
tom2 = tom
tom3 = tom
del tom
print(2, 'del') # 2 del
del tom2 # delete tom
del tom3

由于垃圾回收对象销毁时,才会真正清理对象,还会在回收对象之前自动调用 __del__ 方法,除非你明确知道自 己的目的,建议不要手动调用这个方法。

方法重载(overload)

  • 其他面向对象的高级语言中,会有重载的概念
  • 所谓重载,就是同一个方法名,但是参数个数、类型不一样,就是同一个方法的重载
  • Python没有重载
  • Python不需要重载
  • Python中,方法(函数)定义中,形参非常灵活,不需要指定类型(就算指定了也只是一个说明而非约束),参数个数也不固定(可变参数)。
  • 一个函数的定义可以实现很多种不同形式实参的调用。所以Python不需要方法的重载。
  • 或者说Python语法本身就实现了其它语言的重载。

封装

面向对象的三要素之一,封装Encapsulation

  • 将数据和操作组织到类中,即属性和方法
  • 将数据隐藏起来,给使用者提供操作(方法)。使用者通过操作就可以获取或者修改数据。getter和setter。
  • 通过访问控制,暴露适当的数据和操作给用户,该隐藏的隐藏起来,例如保护成员或私有成员。

练习

随机整数生成类

可以指定一批生成的个数,可以指定数值的范围,可以调整每批生成数字的个数。

设计类

#指定初始化范围,开始、线束、个数
class RandomGen:
    def __init__(self, start=1, end=100, count=10):
        pass

    def generate(self):
        return

gen1 = RandomGen()
print(gen1.generate())
print(gen1.generate())

gen2 = RandomGen(1,10)
print(gen2.generate())
print(gen2.generate())
import random

class RandomGen:
    def __init__(self, start=1, end=100, count=10):
        self.start = start
        self.end = end
        self.count = count

    def generate(self):
        return [random.randint(self.start, self.end) for i in range(self.count)]

gen1 = RandomGen()
print(gen1.generate())
print(gen1.generate())
#返回结果
# [61, 42, 1, 77, 97, 86, 25, 8, 2, 29]
# [50, 73, 78, 60, 67, 9, 54, 82, 12, 10]

gen2 = RandomGen(1,10)
print(gen2.generate())
print(gen2.generate())
#返回结果
# [1, 6, 1, 10, 2, 1, 3, 5, 6, 3]
# [2, 6, 8, 2, 3, 5, 2, 1, 10, 6]

也可以是通过类可能直接访问的工具函数

import random

#通过类可以直接访问的工具函数
class RandomGen:
    @classmethod
    def generate(cls, start=1, end=100, count=10):
        return [random.randint(start, end) for i in range(count)]

print(RandomGen.generate())
print(RandomGen().generate(1,5,3))
#返回结果
# [69, 67, 64, 44, 53, 69, 13, 22, 83, 83]
# [4, 5, 2]

生成器方式

import random

class RandomGen:
    def __init__(self, start=1, end=100, count=10):
        self.start = start
        self.end = end
        self.count = count

    def generate(self):
        return (random.randint(self.start, self.end) for i in range(self.count))

r = RandomGen()
print(*r.generate())
print(list(r.generate()))
#返回结果
# 91 8 77 25 29 8 24 45 42 66
# [43, 79, 80, 43, 63, 21, 15, 51, 40, 87]

生成器函数方式

import random

class RandomGen:
    def __init__(self, start=1, end=100, count=10):
        self.start = start
        self.end = end
        self.count = count
        self._gen = self._generate()

    def _generate(self): #生成器函数
        while True:  #一次生成一个数据
            yield random.randint(self.start, self.end)

    def generate(self): #立即返回count个数据的列表
        return [next(self._gen) for i in range(self.count)]

r = RandomGen()
print(r.generate())
print(r.generate())
#返回结果
# 91 8 77 25 29 8 24 45 42 66
# [43, 79, 80, 43, 63, 21, 15, 51, 40, 87]
import random

class RandomGen:
    def __init__(self, start=1, end=100, count=10):
        self.start = start
        self.end = end
        self.count = count
        self._gen = self._generate()

    def _generate(self): #生成器函数
        while True: #一次生成一批指定个数的数据
            yield [random.randint(self.start, self.end) for i in range(self.count)]

    def generate(self): #立即返回count个数据的列表
        return next(self._gen)

r = RandomGen()
print(r.generate())
r.count = 4
print(r.generate())
#返回结果
# [52, 25, 43, 96, 56, 46, 8, 61, 37, 30]
# [54, 77, 55, 8]

count改成保护属性

import random

class RandomGen:
    def __init__(self, start=1, end=100, count=10):
        self.start = start
        self.end = end
        self._count = count #隐藏count
        self._gen = self._generate()

    def _generate(self): #生成器函数
        while True: #一次生成一批指定个数的数据
            yield [random.randint(self.start, self.end) for i in range(self._count)]

    def generate(self, count=0): #立即返回count个数据的列表
        # c = self._count if count <= 0 else count
        if count >0:
            self._count = count
        return next(self._gen)

r = RandomGen()
print(r.generate())
print(r.generate(2))
#返回结果
# [42, 18, 13, 50, 58, 7, 100, 32, 17, 13]
# [39, 70]

count使用属性装饰器

import random

class RandomGen:
    def __init__(self, start=1, end=100, count=10):
        self.start = start
        self.end = end
        self._count = count #隐藏count
        self._gen = self._generate()

    #使用属性装饰器
    def _generate(self): #生成器函数
        while True: #一次生成一批指定个数的数据
            yield [random.randint(self.start, self.end) for i in range(self.count)] #self._count 改成self.count

    def generate(self): #立即返回count个数据的列表
        return next(self._gen)

    @property
    def count(self):
        return self._count
    @count.setter
    def count(self, value):
        if value > 0:
            self._count = value

r = RandomGen()
print(r.generate())
r.count = 2
print(r.generate())
#返回结果
# [42, 18, 13, 50, 58, 7, 100, 32, 17, 13]
# [39, 70]
打印坐标

使用上题中的类,随机生成20个数字,两两配对形成二维坐标系的坐标,把这些坐标组织起来,并打印输出。

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

方法1

import random

class RandomGen:
    def __init__(self, start=1, end=100, count=10):
        self.start = start
        self.end = end
        self._count = count #隐藏count
        self._gen = self._generate()

    #使用属性装饰器
    def _generate(self): #生成器函数
        while True: #一次生成一批指定个数的数据
            yield [random.randint(self.start, self.end) for i in range(self.count)] #self._count 改成self.count

    def generate(self): #立即返回count个数据的列表
        return next(self._gen)

    @property
    def count(self):
        return self._count
    @count.setter
    def count(self, value):
        if value > 0:
            self._count = value


class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    #str repr 魔术方法 重写了这个方法
    def __repr__(self):
        return "<Point {} {}>".format(self.x, self.y)

r = RandomGen()
points = [Point(x, y) for x,y in zip(r.generate(), r.generate())]
# for p in points:
#     print(p.x, p.y, p)
# #返回结果
# # 1 7 <Point 1 7>
# # 13 19 <Point 13 19>
# # 59 28 <Point 59 28>
# # ......
print(*points, sep='\n')
#返回结果
# <Point 54 94>
# <Point 69 38>
# <Point 27 13>
# <Point 84 20>
# <Point 36 97>
# <Point 24 58>
# <Point 4 90>
# <Point 95 16>
# <Point 24 83>
# <Point 90 63>

方法2:命名元组方式

什么是命名元组

from collections import OrderedDict, defaultdict, namedtuple

x  = namedtuple('Point', 'x y') #返回一个类
print(type(x)) #<class 'type'> 说明什么?
print(x) #<class '__main__.Point'> x是标识符,指向一个类对象,
# Point是类的名称字符串,x和y是属性名称

Point = namedtuple('Point', 'x,y') #可变吗? 属性不可变,返回类型,这个类型是tuple类型的子类
p1 = Point(4, 5)
print(p1) #Point(x=4, y=5)
p2 = Point(15, 16)
print(p2) #Point(x=15, y=16)
print(Point.mro()) #[<class '__main__.Point'>, <class 'tuple'>, <class 'object'>]
print(Point.__bases__) #(<class 'tuple'>,)

# class x: #x 标识符,类的名称是字符串'x'
#     def __init__(self, x, y):
#         self.x = x
#         self.y = y
#
# def a(): #a 标识符,函数名称字符串'a'
#     pass

class Point: #Point 标识符,类的名称是字符串'Point'
    def __init__(self, x, y):
        self.x = x
        self.y = y
p3 = Point(15, 16)
print(p3) #<__main__.Point object at 0x0000026F68F97CB0>
print(p3.x, p3.y) #15 16
import random

class RandomGen:
    def __init__(self, start=1, end=100, count=10):
        self.start = start
        self.end = end
        self._count = count #隐藏count
        self._gen = self._generate()

    #使用属性装饰器
    def _generate(self): #生成器函数
        while True: #一次生成一批指定个数的数据
            yield [random.randint(self.start, self.end) for i in range(self.count)] #self._count 改成self.count

    def generate(self): #立即返回count个数据的列表
        return next(self._gen)

    @property
    def count(self):
        return self._count
    @count.setter
    def count(self, value):
        if value > 0:
            self._count = value


#nametuple 命名元组
from collections import namedtuple

Point =namedtuple('P', 'x y')

r = RandomGen()
points = [Point(x, y) for x,y in zip(r.generate(), r.generate())]
print(*points, sep='\n')
#返回结果
# P(x=81, y=48)
# P(x=23, y=45)
# P(x=11, y=21)
# P(x=89, y=29)
# P(x=42, y=38)
# P(x=69, y=19)
# P(x=3, y=94)
# P(x=98, y=80)
# P(x=19, y=8)
# P(x=71, y=73)

zip换成map函数

import random

class RandomGen:
    def __init__(self, start=1, end=100, count=10):
        self.start = start
        self.end = end
        self._count = count #隐藏count
        self._gen = self._generate()

    #使用属性装饰器
    def _generate(self): #生成器函数
        while True: #一次生成一批指定个数的数据
            yield [random.randint(self.start, self.end) for i in range(self.count)] #self._count 改成self.count

    def generate(self): #立即返回count个数据的列表
        return next(self._gen)

    @property
    def count(self):
        return self._count
    @count.setter
    def count(self, value):
        if value > 0:
            self._count = value


# class Point:
#     def __init__(self, x, y):
#         self.x = x
#         self.y = y
#
#     #str repr 魔术方法 重写了这个方法
#     def __repr__(self):
#         return "<Point {} {}>".format(self.x, self.y)

#nametuple 命名元组
from collections import namedtuple

Point =namedtuple('P', 'x y')

r = RandomGen()
points = [Point(x, y) for x,y in zip(r.generate(), r.generate())]
points = map(lambda x,y: Point(x,y), r.generate(), r.generate())
# for p in points:
#     print(p.x, p.y, p)
# #返回结果
# # 1 7 <Point 1 7>
# # 13 19 <Point 13 19>
# # 59 28 <Point 59 28>
# # ......
print(*points, sep='\n')
#返回结果
# P(x=81, y=48)
# P(x=23, y=45)
# P(x=11, y=21)
# P(x=89, y=29)
# P(x=42, y=38)
# P(x=69, y=19)
# P(x=3, y=94)
# P(x=98, y=80)
# P(x=19, y=8)
# P(x=71, y=73)
车辆信息

记录车的品牌Mark、颜色color、价格price、速度speed等特征,并实现增加车辆信息,显示全部车辆信息的功能。

class Car:
    def __init__(self, mark, speed, price, color):
        self.mark = mark
        self.speed = speed
        self.price = price
        self.color = color
    def __repr__(self):
        return str(sorted(self.__dict__.items())) #或者return str(self.__dict__)

class CarInfoMgr:
    def __init__(self):
        self.__info = [] #定义一个容器

    def add_car(self, car:Car):
        self.__info.append(car)

    def get_all(self):
        if len(self):
            pass
        return self.__info

    def __len__(self):
        return len(self.__info)

mgr = CarInfoMgr()
car = Car('Audi', 200, 20, 'red')
mgr.add_car(car)
mgr.add_car(Car('RedFlag', 300, 40, 'Black'))

print(*mgr.get_all(), sep='\n')
#返回结果
# [('color', 'red'), ('mark', 'Audi'), ('price', 20), ('speed', 200)]
# [('color', 'Black'), ('mark', 'RedFlag'), ('price', 40), ('speed', 300)]
实现温度的处理

实现华氏温度和摄氏温度的转换

\(^\circ C = 5 \times ( ^\circ F - 32 ) / 9\)

\(^\circ F = 9 \times \ ^\circ C / 5 + 32\)

完成以上转换后,增加与开氏温度的转换, \(K = ^\circ C + 273.15\)

思路:工具类不需要实例化

class Temperature:
    @classmethod
    def c2f(cls, c):
        pass

    @classmethod
    def f2c(cls, f):
        pass
class Temperature:
    @classmethod
    def c2f(cls, c):
        return 9 * c /5 + 32

    @classmethod
    def f2c(cls, f):
        return (f - 32) * 5 / 9

    @classmethod
    def c2k(cls, c):
        return c + 273.15

    @classmethod
    def k2c(cls, k):
        return k - 273.15

    #华氏温度和开氏温度如何转换?

print(Temperature.c2f(40))
print(Temperature.f2c(104))
print(Temperature.c2k(40))
print(Temperature.k2c(313.15))
class Temperature:
    @classmethod
    def c2f(cls, c):
        return 9 * c /5 + 32

    @classmethod
    def f2c(cls, f):
        return (f - 32) * 5 / 9

    @classmethod
    def c2k(cls, c):
        return c + 273.15

    @classmethod
    def k2c(cls, k):
        return k - 273.15

    #华氏温度和开氏温度如何转换?
    @classmethod
    def f2k(cls, f):
        return cls.c2k(cls.f2c(f))  #核心,最常用的温度体系是摄氏度

    @classmethod
    def k2f(cls,k):
        return cls.c2f(cls.k2c(k))

print(Temperature.c2f(40))
print(Temperature.f2c(104))
print(Temperature.c2k(40))
print(Temperature.k2c(313.15))
print(Temperature.f2k(104))
print(Temperature.k2f(313.15))

利用属性装饰器, 完善

class Temperature:
    def __init__(self, t, unit='c'):
        self._c = None
        self._k = None
        self._f = None
        #中转的温度是摄氏度
        if unit == 'k':
            self._k = t
            self._c = self.k2c(t)
        elif unit == 'f':
            self._f = t
            self._c = self.f2c(t)
        else:
            self._c = t

    @property
    def c(self):
        return self._c

    @property
    def f(self):
        if self._f is None:
            self._f = self.c2f(self.c)
        return self._f

    @property
    def k(self):
        if self._k is None:
            self._k = self.c2k(self.c)
        return self._k

    @classmethod
    def c2f(cls, c):
        return 9 * c /5 + 32

    @classmethod
    def f2c(cls, f):
        return (f - 32) * 5 / 9

    @classmethod
    def c2k(cls, c):
        return c + 273.15

    @classmethod
    def k2c(cls, k):
        return k - 273.15

    #华氏温度和开氏温度如何转换?
    @classmethod
    def f2k(cls, f):
        return cls.c2k(cls.f2c(f))  #核心,最常用的温度体系是摄氏度

    @classmethod
    def k2f(cls,k):
        return cls.c2f(cls.k2c(k))

# print(Temperature.c2f(40))
# print(Temperature.f2c(104))
# print(Temperature.c2k(40))
# print(Temperature.k2c(313.15))
# print(Temperature.f2k(104))
# print(Temperature.k2f(313.15))

#需求 先给你一个某单位的温度,先存着,但并不着急知道未来需要什么单位的温度
t = Temperature(40, 'c')
print(t.__dict__) #{'_c': 40, '_k': None, '_f': None}
print(t.k) #313.15
print(t.__dict__) #{'_c': 40, '_k': 313.15, '_f': None}
print(t.f) #104.0
print(t.__dict__) #{'_c': 40, '_k': 313.15, '_f': 104.0}
模拟购物车购物

思路

购物车是购物,分解得到两个对象,购物车,物品。一个操作:买

购买不是购物车的行为,其实是人的行为,但是对于购物车来说就是增加add.

商品有很多种类,商品的属性多种多样,怎么解决?

购物车可以加入很多不同的商品,如何实现?

#购物车购物

class Item:
    pass

class Cart:
    def __init__(self):
        self.items = []

    def add(self, item:Item):
        self.items.append(item)

    def get_all(self):
        return self.items
#购物车购物
#不能设计所有的商品类,统一设计
class Color:
    RED = 0
    BLUE = 1
    BLACK = 2

class Item:
    def __init__(self, name, price, **kwargs):
        self.name = name
        self.price = price
        self.__spec = kwargs
        self.color = Color.BLUE
        if 'color' in kwargs.keys():
            self.color = kwargs['color']
            kwargs.pop('color')

    def __repr__(self):
        return str(dict(color=self.color, **self.__spec))

class Cart:
    def __init__(self):
        self.items = []

    def add(self, item:Item):
        self.items.append(item)

    def get_all(self):
        return self.items

mycart = Cart()

phone = Item('P30', 3000, color=Color.RED, memory='4G')

mycart.add(phone)
mycart.add(Item('RedFlag', 40000, color=Color.BLACK, year=2015))
print(mycart.get_all())
#返回结果
# [{'color': 0, 'memory': '4G'}, {'color': 2, 'year': 2015}]

注意,上面的代码只是一个非常简单的实现,生成环境实现购物车的增删改查,要考虑很多。

类的继承

基本概念

面向对象三要素之一,继承Inheritance

人类和猫类都继承自动物类
个体继承自父母,继承了父母的一部分特征,但也可以有自己的个性

在面向对象的世界中,从父类继承,就可以直接拥有父类的属性和方法,这样可以减少代码、多复用。子类可以定义自己的属性和方法

class Animal:
    def shout(self):
        print('{} shouts'.format(self.__class__.__name__))

a = Animal()
a.shout()

class Cat(Animal): #继承
    # pass

c = Cat()
c.shout()
  • 继承 class Cat(Animal) 这种形式就是父类继承,括号中写上继承的类的列表

    继承可以让子类从父类获取特征(属性和方法)

  • 父类 Animal就是Cat的父类,也称为基类、超类
  • 子类 Cat就是Animal的子类,也称为派生类

定义

格式如下

class 子类名(基类1[,基类2,...]):
    语句块

如果类定义时,没有基类列表,等同于继承自object。在Python3中,object类是所有对象的 根基类

class A:
    pass
# 等价于
class A(object):
    pass

注意,上例在Python2中,两种写法是不同的

Python支持多继承,继承也可以多级

查看继承的特殊属性和方法有

__bases__        #类的基类元组
__base__         #类的基类元组第一项
__mro__          #显示方法查找顺序,返回基类的元组
mro()方法        #同上,返回列表
__subclass__()   #类的子类列表
class Animal:
    def __init__(self, name):
        self.name = name
    def shout(self):
        print('{} shouts'.format(self.__class__.__name__))

a = Animal('A')
a.shout()

class Cat(Animal): pass

c = Cat('cat')
c.shout()

class Dog(Animal): pass

d = Dog('dog')
d.shout()

print(Dog.__bases__) #(<class '__main__.Animal'>,) 直接的父类,定义时直接写出的父类的列表
print(Dog.__base__) #Dog.__bases__[0] 元组第一项 <class '__main__.Animal'>
print(Animal.__bases__) #(<class 'object'>,)
print(Animal.__subclasses__()) #列出子类 [<class '__main__.Cat'>, <class '__main__.Dog'>]

print(bool.__bases__) #(<class 'int'>,)
print(int.__subclasses__()) #[<class 'bool'>]

print('-' * 30)
print(Cat.__mro__)#显示方法的查找顺序,返回基类的元组
print(Cat.mro()) #mro用在类上

python不同版本的类

Python2.2之前类是没有共同的祖先的,之后,引入object类,它是所有类的共同祖先类object。

python2中做了兼容,分为古典类(旧式类)和新式类

python3中全部是新式类。新式类都是继承自object的,新式类可以使用super

python3

class A: pass
class B(object): pass

print(A.__bases__)  #(<class 'object'>,)
print(B.__bases__) #(<class 'object'>,)
print(A.mro()) #[<class '__main__.A'>, <class 'object'>]
print(B.mro()) #[<class '__main__.B'>, <class 'object'>]

a = A()
print(a.__class__) #<class '__main__.A'>
print(type(a)) #<class '__main__.A'>

b = B()
print(b.__class__) #<class '__main__.B'>
print(type(b)) #<class '__main__.B'>

python2

#python2

class A: pass
class B(object): pass

print(A.__bases__)  #()
print(B.__bases__) #(<type 'object'>,)
print(A.mro()) #报错  AttributeError: class A has no attribute 'mro'
print(B.mro()) #[<class '__main__.B'>, <type 'object'>]

a = A()
print(a.__class__) #<class '__main__.A' at 0x000xxxx>
print(type(a)) #<type 'instance'>

b = B()
print(b.__class__) #<class '__main__.B'>
print(type(b)) #<class '__main__.B'>

继承中的访问控制

class Animal:
    __COUNT = 100 # _Animal__COUNT
    HEIGHT = 0

    def __init__(self,age, weight, height):
        self.__COUNT += 1 #赋值即定义,放在了实例字典中
        # 改名 self._Animal__COUNT =  self._Animal__COUNT + 1
        self.age = age
        self.__weight = weight
        self.HEIGHT = height

    def eat(self):
        print('{} eat'.format(self.__class__.__name__))

    def __getweight(self):
        print(self.__weight)

    @classmethod
    def showcount1(cls):
        print(cls)
        print(cls.__dict__)
        print(cls.__COUNT)

    @classmethod
    def __showcount2(cls):
        print(cls.__COUNT)

    def showcount3(self):
        print(self.__COUNT) #c._Animal__COUNT


class Cat(Animal):
    NAME = 'CAT'
    __COUNT = 200 #改名, _Cat__COUNT = 200

# c = Cat() # __init__ 函数参数错误
c = Cat(3, 5, 15)
c.eat() # Cat eat
print(c.HEIGHT) #  15

#print(c.__COUNT) #报错 AttributeError, 私有成员不能直接访问,可通过c._Animal__COUNT访问

#c.__getweight() #私有属性不能直接访问。可通过c._Animal__getweight()访问

c.showcount1()
# 打印结果
#实例调用方法,showcount1在父类中有,且是类方法,绑定类
#从c实例上拿,即type(c) c.__class__ => Cat , showcount1(cls)注入的第一参数就是Cat
#cls.__COUNT 相当于访问的Cat._Animal__COUNT
# Cat类<class '__main__.Cat'>
# Cat类字典{'__module__': '__main__', '__firstlineno__': 30, 'NAME': 'CAT', '_Cat__COUNT': 200, '__static_attributes__': (), '__doc__': None}
# 100

#c.__showcount2() #报错,私有属性。通过 c._Animal__showcount2()
c._Animal__showcount2() #100

c.showcount3() # 101
#实例化2个阶段,先实例化调用__new__方法,只有object类有这个方法, Cat -> Animal -> object
#Cat没有初始化方法,Animal有初始化方法
#self.__COUNT 实例字典中有不管类要了
print(c.__dict__) #{'_Animal__COUNT': 101, 'age': 3, '_Animal__weight': 5, 'HEIGHT': 15}

print(c._Cat__COUNT) #200
print(c._Animal__COUNT) #101
print(c.NAME) #CAT

#打印出字典都明白了
print('{}'.format(Animal.__dict__))
print('{}'.format(Cat.__dict__))
print(c.__dict__)
print(c.__class__.mro()) #方法查找顺序
#返回
# {'__module__': '__main__', '__firstlineno__': 1, '_Animal__COUNT': 100, 'HEIGHT': 0, '__init__': <function Animal.__init__ at 0x00000192AC147A60>, 'eat': <function Animal.eat at 0x00000192AC154680>, '_Animal__getweight': <function Animal.__getweight at 0x00000192AC154D60>, 'showcount1': <classmethod(<function Animal.showcount1 at 0x00000192AC2B71A0>)>, '_Animal__showcount2': <classmethod(<function Animal.__showcount2 at 0x00000192AC2B7240>)>, 'showcount3': <function Animal.showcount3 at 0x00000192AC2B72E0>, '__static_attributes__': ('HEIGHT', '__weight', 'age'), '__dict__': <attribute '__dict__' of 'Animal' objects>, '__weakref__': <attribute '__weakref__' of 'Animal' objects>, '__doc__': None}
# {'__module__': '__main__', '__firstlineno__': 32, 'NAME': 'CAT', '_Cat__COUNT': 200, '__static_attributes__': (), '__doc__': None}
# {'_Animal__COUNT': 101, 'age': 3, '_Animal__weight': 5, 'HEIGHT': 15}
# [<class '__main__.Cat'>, <class '__main__.Animal'>, <class 'object'>]
  • 从父类继承,自己没有的,就可以到父类中找。
  • 私有的都是不可以访问的,但是本质上依然是改了名称放在这个属性所在的类或实例的__dict__中。 知道这个新名称就可以直接找到这个隐藏的变量,这是黑魔法,慎用。

总结:

  • 继承时,公有成员、子类和实例都可以随意访问;私有成员被隐藏,子类和实例不可直接访问,但 私有变量所在的类内的方法中可以访问这个私有变量
  • Python通过自己一套实现,实现和其他语言一样的面向对象的继承机制。

实例属性查找顺序

实例的__dict__ --> 类__dict__ --> 如果有继承 --> 父类__dict__

如果搜索这些地方后没有找到就会抛异常,先找到就立即返回

方法的重写、覆盖override

class Animal:
    def shout(self):
        print('Animal shouts')

class Cat(Animal):
    # 覆盖了父类方法
    def shout(self):
        print('miao')

a = Animal()
a.shout() #: Animal shouts
c = Cat() # Animal shouts
c.shout() # miao

print(a.__dict__)
print(c.__dict__)
print(Animal.__dict__)
print(Cat.__dict__)
  • Cat中能够有覆盖自己的方法?
class Animal:
    def shout(self):
        print('Animal shouts')

class Cat(Animal):
    # 覆盖了父类的方法
    def shout(self):
        print('miao')

    # 覆盖了自身的方法,显示调用了父类的方法
    def shout(self):
        print(super()) # <super: <class 'Cat'>, <Cat object>>
        print(super(Cat, self)) # <super: <class 'Cat'>, <Cat object>>
        print(super(self.__class__, self)) # <super: <class 'Cat'>, <Cat object>>

        super().shout() # Animal shouts
        super(Cat, self).shout() # 等价于super() # Animal shouts
        self.__class__.__base__.shout(self) # 不推荐 # Animal shouts


a = Animal()
a.shout() # Animal shouts
c = Cat() 
c.shout() # miao

print(a.__dict__)
print(c.__dict__)
print(Animal.__dict__)
print(Cat.__dict__)
super()
  • 可以访问到父类的类属性
class Animal:
    def shout(self):
        print('Animal shouts')


class Cat(Animal):
    # def shout(self): #完全 覆盖
    #     print('miao')

    def shout(self):  # 继承并发扬
        # super() -> same as super(__class__, <first argument>)
        # super(type) -> unbound super object
        # super(type, obj) -> bound super object; requires isinstance(obj, type)
        # super(type, type2) -> bound super object; requires issubclass(type2, type)
        print('~~~')
        print(super())
        print(super(Cat, self))  #当前类 super(__class__, self)

        super().shout() #安全实现,先调用父类的方法,执行自己的代码
        super(Cat, self).shout() #与super().shout()等价
        # Cat.__base__.shout(self) #self.__class__.__base__.shout(self) 麻烦不推荐
        print('miao')

c = Cat()
c.shout()
#返回
# <super: <class 'Cat'>, <Cat object>>
# <super: <class 'Cat'>, <Cat object>>
# Animal shouts
# Animal shouts
# miao

print(Animal.__dict__)
print(Cat.__dict__)

如果是类方法和静态方法?

class Animal:
    @classmethod
    def class_method(cls):
        print('class_method_animal')

    @staticmethod
    def static_method():
        print('static_method_animal')

class Cat(Animal):
    @classmethod
    def class_method(cls):
        print('class_method_cat')

    @staticmethod
    def static_method():
        print('static_method_cat')

c = Cat()
c.class_method() # class_method_cat
c.static_method() # static_method_cat

print(Cat.__dict__)
print(Animal.__dict__)

Cat.static_method() # static_method_cat
Animal.static_method() # static_method_animal

静态方法和类方法,是特殊方法,也是类属性,这些方法都可以覆盖,原理都一样,属性字典的搜索顺序

继承时使用初始化

  • 初始化只跟实例字典相关
class A:
    def __init__(self,a, d=10):
        self.a = a
        self.d = d

class B(A):
    def __init__(self, b, c):
        #A.__init__(self, b + c, b - c)
        super().__init__(self, b + c, b - c) #super(B, self) 传递给父类
        self.b = b
        self.c = c

    def printv(self):
        print(self.b) # 200
        print(self.a) # 500

f = B(200, 300)
print(f.__dict__) # {'a': 500, 'd': -100, 'b': 200, 'c': 300}
print(f.__class__.__bases__) # (<class '__main__.A'>,)
f.printv()
  • 如果父类定义了=__init__=方法,应该在子类的__init__中调用它
  • 那么,子类什么时候自动调用父类的__init__方法呢?

示例1

class A:
    def __init__(self):
        self.a1 = 'a1'
        self.a2 = 'a2'
        print('init in A')

class B(A):
   pass

b = B() # init in A
print(b.__dict__) # {'a1': 'a1', 'a2': 'a2'}

B实例的初始化会自动调用基类A的=__init__=方法,所以=print('init in A')=会打印出来

示例2

class A:
    def __init__(self):
        self.a1 = 'a1'
        self.a2 = 'a2'
        print('init in A')

class B(A):
   def __init__(self):
       self.b1 = 'b1'
       print('init in B')

b = B() # init in B
print(b.__dict__) # {'b1': 'b1'}

B实例一旦定义了初始化=__init__=方法,就不会自动调用父类的初始化=__init__=方法,需要手动调用

class A:
    def __init__(self):
        self.a1 = 'a1'
        self.a2 = 'a2'
        print('init in A')

class B(A):
   def __init__(self):
       self.b1 = 'b1'
       print('init in B')
       A.__init__(self)

b = B() # init in B

print(b.__dict__) # 注意看__a2
# init in A
# {'b1': 'b1', 'a1': 'a1', '_A__a2': 'a2'}
正确初始化
class Animal:
    def __init__(self, age):
        print('init in Animal')
        self.age = age

    def show(self):
        print(self.age)

class Cat(Animal):
    def __init__(self, age, weight):
        # 调用父类的__init__方法的顺序有时决定着show方法的结果
        super().__init__(age)
        print('init in Cat')
        self.age = age + 1
        self.weight = weight
        super().__init__(age)

c = Cat(10, 5)
# 打印结果
# init in Animal
# init in Cat
# init in Animal
c.show() #10
  • 注意,调用父类的 __init__ 方法,出现在不同的位置,可能导致出现不同的结果
  • 将上例中所有的实例属性改成私有变量
class Animal:
    def __init__(self, age):
        print('init in Animal')
        self.__age = age

    def show(self):
        print(self.__age)

class Cat(Animal):
    def __init__(self, age, weight):
        # 调用父类的__init__方法的顺序有时决定着show方法的结果
        super().__init__(age)
        print('init in Cat')
        self.__age = age + 1
        self.__weight = weight
        super().__init__(age)

c = Cat(10, 5)
# 打印结果
# init in Animal
# init in Cat
# init in Animal
c.show() # 10

print(c.__dict__) # {'_Animal__age': 10, '_Cat__age': 11, '_Cat__weight': 5}
  • 上例中打印10,原因看 __dict__ 就知道了。因为父类Animal的show方法中 _age 会被解释为 _Animal__age ,因此显示的是10,而不是11
  • 这样的设计不好,Cat的实例c应该显示自己的属性值更好

解决的办法:

  • 一个原则,自己的私有属性,就该自己的方法读取和修改,不要借助其他类的方法,即使是父类或者派生类的方法

单继承

class Animal:
    def shout(self):
        print('Animal shouts')

class Cat(Animal):
    pass

a = Animal()
a.shout() # Animal shouts
c = Cat()
c.shout() # Animal shouts

上面例子中,类的继承列表只有一个类,这种继承为单一继承。

继承的用途:在子类上实例对基类的增强,实现多态。

多态

在面向对象中,父类通过继承联系在一起,如果可能通过一套方法,就可以实现不同表现,就是多态。

多继承

一个类继承自多个类就是多继承,它将具有多个类的特征。

多继承弊端

  • 多继承很好的模拟了世界,因为事物很少是单一继承,但是舍弃简单,必然引入复杂性,带来了冲突。
  • 多继承的实现会导致编译器设计的复杂度增加,所以有些高级编程语言舍弃了类的多继承。
  • C++支持多继承;Java舍弃了多继承。
  • Java中,一个类可以实现多个接口,一个接口也可以继承多个接口。Java的接口很纯粹,只是方法的声明,继承者必须实现这些方法,就具有了这些能力,就能干什么。
  • 多继承可能会带来二义性,实现多继承的语言,要解决二义性,深度优先或者广度优先。

Python多继承实现

img_20250113_111827.png

左图是多继承(菱形继承),右图是单一继承

多继承带来路径选择问题, 究竟是继承哪个父类的特征呢?

Python使用MRO(method resolution order方法解析顺序)解决基类搜索顺序问题。

  • 历史原因,MRO有三个搜索算法:
    1. 经典算法,按照定义从左到右,深度优先策略。2.2版本之前
      • 左图的MRO是MyClass,D,B,A,C,A
    2. 新式类算法,是经典算法的升级,深度优先,重复的只保留最后一个。2.2版本
      • 左图的MRO是MyClass,D,B,C,A,object
    3. C3算法,在类被创建出来的时候,就计算出一个MRO有序列表。2.3之后,Python3唯一支持的算法
      • 左图中的MRO是MyClass,D,B,C,A,object的列表
      • C3算法解决多继承的二义性

经典算法有很大的问题,如果C中有就去覆盖了A的方法,也不会访问到C的方法,因为先访问A的(深度优先)。

新式类算法,依然采用深度优先,解决了重复问题,但是同经典算法一样,没有解决继承的单调性。

  • C3算法,解决了继承的单调性,它阻止创建之前版本产生二义性的代码。求得的MRO本质是为了线性化,且确定了顺序。
  • 单调性:假设有A、B、C三个类,C的mro是[C, A, B],那么C的子类的mro中,A、B的顺序一致就是单调的。

多继承的缺点

当类很多,继承复杂的情况下, 继承路径太多,很难说清什么样的继承路径。 Python语法是允许多继承,但Python代码是解释执行,只有执行到的时候,才发现错误。

团队协助开发,如果引入多继承,那代码就不可控。

不管编程语言是否支持多继承,都应当避免多继承。

Python的面向对象,我们看到的太灵活了,太开放了,所以要团队守规矩。

Mixin

文档document类是其他所有文档类的抽象基类。Word,pdf是document的子类。

需求:为document子类提供打印能力。

思路1:在document中提供print方法。

class Document:
    def __init__(self,content):
        self.content = content

    def print(self):
        print('打印')

class Word(Document):pass
class Pdf(Document):pass

w = Word('test word')
w.print()
p = Pdf("test pdf")
p.print()
#基类提供的方法不应该具体实现,因为它未必适合子类的打印,子类中需要覆盖重写。
#print算是一种能力——打印能力,不是所有的document的子类都需要的,所以,从这个角度出发,有点问题。

思路2: 需要打印的子类上增加

如果在现有子类上直接增加,违反了OCP原则,所以应该继承后增加

class Document:
    def __init__(self,content):
        self.content = content

    # def print(self):
    #     print('打印')
    def print(self): #未实现的方法,抽象方法,规范子类行为
        #pass #基类不实现.
        raise NotImplementedError #抛异常,未实现异常

class Word(Document):pass
class Pdf(Document):pass

#上面都是第三方库,你觉得不合适,自己继承覆盖就是了
## OCP原则,都应该继承后覆盖
class PrintableWord(Word):
    def print(self):
        print(self.content, '~~~')

class PrintablePdf(Pdf):
    def print(self):
        print(self.content, '+++')

w = Word('test word')
w.print()
p = Pdf("test pdf")
p.print()

思路3:装饰器

用装饰器增强一个类,把功能给类附加上去,那个类需要,就装饰它。

class Document:
    def __init__(self,content):
        self.content = content

    # def print(self):
    #     print('打印')
    # def print(self): #未实现的方法,抽象方法,规范子类行为
    #     #pass #基类不实现.
    #     raise NotImplementedError #抛异常,未实现异常

class Word(Document):pass
class Pdf(Document):pass

#上面都是第三方库,你觉得不合适,自己继承覆盖就是了
## OCP原则,都应该继承后覆盖

#打印功能不是核心业务功能,也不是所有的子类都需要的,谁需要给谁,缺什么补什么,用装饰器
def printable(cls):
    def _print(self): #避免递归
        print(self.content, '~~~')
    #cls.print = lambda self: print(self.content, '~~~')
    cls.print = _print
    return cls

@printable
class PrintableWord(Word):
    pass
    # def print(self):
    #     print(self.content, '~~~')

@printable
class PrintablePdf(Pdf): pass
#     def print(self):
#         print(self.content, '+++')

pw = PrintableWord('test pw~~~')
pw.print()  #: test pw~~~ ~~~

ppdf = PrintablePdf('test pfd +++')
ppdf.print() #: test pfd +++ ~~~


思路4:Mixin

class Document:
    def __init__(self,content):
        self.content = content

    # def print(self):
    #     print('打印')
    # def print(self): #未实现的方法,抽象方法,规范子类行为
    #     #pass #基类不实现.
    #     raise NotImplementedError #抛异常,未实现异常

class Word(Document):pass
class Pdf(Document):pass

#上面都是第三方库,你觉得不合适,自己继承覆盖就是了
## OCP原则,都应该继承后覆盖

#打印功能不是核心业务功能,也不是所有的子类都需要的,谁需要给谁,缺什么补什么,用装饰器
#Mixin 缺什么补什么 类就可以继承
def printable(cls):
    def _print(self): #避免递归
        print(self.content, '~~~')
    #cls.print = lambda self: print(self.content, '~~~')
    cls.print = _print
    return cls

@printable
class PrintableWord(Word):
    pass
    # def print(self):
    #     print(self.content, '~~~')

pw = PrintableWord('test pw~~~')
pw.print()  #: test pw~~~ ~~~

class PrintableMixin:
    def print(self):
        print(self.content, '+++')

class PrintableWordMixin(PrintableMixin, Pdf): pass
#这是多继承
print(PrintableWordMixin.mro()) #: [<class '__main__.PrintableWordMixin'>, <class '__main__.PrintableMixin'>, <class '__main__.Pdf'>, <class '__main__.Document'>, <class 'object'>]
print(PrintableWordMixin.__dict__) #: {'__module__': '__main__', '__firstlineno__': 38, '__static_attributes__': (), '__doc__': None}
print(PrintableMixin.__dict__)
#: {'__module__': '__main__', '__firstlineno__': 34, 'print': <function PrintableMixin.print at 0x00000193BED91B20>, '__static_attributes__': (), '__dict__': <attribute '__dict__' of 'PrintableMixin' objects>, '__weakref__': <attribute '__weakref__' of 'PrintableMixin' objects>, '__doc__': None}

pwm = PrintableWordMixin('test pdf mix***')
pwm.print()  #: test pdf mix*** +++


class SuperPrintableMix(PrintableMixin):
    def print(self):
        print('超级打印开始~~~')
        super().print()
        print('超级打印结束~~~')


class SuperPrintableWordMixin(SuperPrintableMix, Pdf): pass

spwm = SuperPrintableWordMixin('super print ^^^')
spwm.print()
#返回结果
#: 超级打印开始~~~
#: super print ^^^ +++
#: 超级打印结束~~~
print(SuperPrintableWordMixin.mro())
#: [<class '__main__.SuperPrintableWordMixin'>, <class '__main__.SuperPrintableMix'>, <class '__main__.PrintableMixin'>, <class '__main__.Pdf'>, <class '__main__.Document'>, <class 'object'>]
Mixin类
  • Mixin本质上就是多继承实现的
  • Mixin体现的是一种组合的设计模式
  • 在面向对象的设计中,一个复杂的类,往往需要很多功能,而这些功能有来自不同的类提供,这就需要很多的类组合在一起
  • 从设计模式的角度来说,多组合,少继承

Mixin类的使用原则

  • Mixin类中不应该显式的出现__init__初始化方法
  • Mixin类通常不能独立工作,因为它是准备混入别的类中的部分功能实现
  • Mixin类的祖先类也应是Mixin类

使用时,Mixin类通常在继承列表的第一个位置, 例如 class PrintableWordMixin(PrintableMixin, Pdf): pass

Mixin类和装饰器

  • 这两种方式都可以使用,看个人喜好
  • 如果还需要继承就得使用Mixin类的方式

练习

Shape基类

要求所有子类都必须提供面积的计算,子类有三角形、矩形、圆。

  • 上题圆类的数据可序列化

三角形面积–海伦公式

假设在平面内,有一个三角形,边长分别为a、b、c,三角形的面积S可由以下公式求得:

\begin{equation*} S \quad = \quad \sqrt{p(p-a)(p-b)(p-c)} \end{equation*}

而公式里的p为半周长(周长的一半):

\(p = \frac{ ( a + b + c ) }{2}\)

class Shape:
    pass

class Triangle(Shape):
    pass

class Rectangle(Shape):
    pass

class Circle(Shape):
    pass
import math

class Shape:
    def area(self):
        raise NotImplementedError('基类未实现')

class Triangle(Shape):
    def __init__(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c

    def area(self):
        p = (self.a + self.b + self.c) / 2
        return math.sqrt(p * (p - self.a) * (p - self.b) * (p - self.c))

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius):
        self.r = radius

    def area(self):
        return 3.14 * self.r * self.r

shapes = [Triangle(3,4,5), Rectangle(4, 5), Circle(4)]
for s in shapes:
    print(s.area())

利用属性装饰器

import math

class Shape:
    def area(self):
        raise NotImplementedError('基类未实现')

class Triangle(Shape):
    def __init__(self, a, b, c):
        self.__a = a
        self.__b = b
        self.__c = c
        self.__area = None

    @property
    def a(self):
        return self.__a
    @a.setter
    def a(self, value):
        self.__a = value
        self.__area = None
    @property
    def b(self):
        return self.__b
    @property
    def c(self):
        return self.__c

    def area(self):
        if self.__area is None:
            p = (self.a + self.b + self.c) / 2
            self.__area = math.sqrt(p * (p - self.a) * (p - self.b) * (p - self.c))
        return self.__area

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius):
        self.r = radius

    def area(self):
        return 3.14 * self.r * self.r

shapes = [Triangle(3,4,5), Rectangle(4, 5), Circle(4)]
for s in shapes:
    print(s.area())

t = shapes[0]
print(t.area())
import math

class Shape:
    @property
    def area(self):
        raise NotImplementedError('基类未实现')

class Triangle(Shape):
    def __init__(self, a, b, c):
        self.__a = a
        self.__b = b
        self.__c = c
        self.__area = None

    @property
    def a(self):
        return self.__a
    @a.setter
    def a(self, value):
        self.__a = value
        self.__area = None
    @property
    def b(self):
        return self.__b
    @property
    def c(self):
        return self.__c

    @property
    def area(self):
        if self.__area is None:
            p = (self.a + self.b + self.c) / 2
            self.__area = math.sqrt(p * (p - self.a) * (p - self.b) * (p - self.c))
        return self.__area

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    @property
    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius):
        self.r = radius

    @property
    def area(self):
        return 3.14 * self.r * self.r

shapes = [Triangle(3,4,5), Rectangle(4, 5), Circle(4)]
for s in shapes:
    print(s.area)

t = shapes[0]
t.a = 5
print(t.area)

圆类的数据可序列化。

import math
import json
import msgpack

class Shape:
    @property
    def area(self):
        raise NotImplementedError('基类未实现')

class Triangle(Shape):
    def __init__(self, a, b, c):
        self.__a = a
        self.__b = b
        self.__c = c
        self.__area = None

    @property
    def a(self):
        return self.__a
    @a.setter
    def a(self, value):
        self.__a = value
        self.__area = None
    @property
    def b(self):
        return self.__b
    @property
    def c(self):
        return self.__c

    @property
    def area(self):
        if self.__area is None:
            p = (self.a + self.b + self.c) / 2
            self.__area = math.sqrt(p * (p - self.a) * (p - self.b) * (p - self.c))
        return self.__area

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    @property
    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius):
        self.r = radius

    @property
    def area(self):
        return 3.14 * self.r * self.r

class SerializableMixin:
    def dumps(self, type='json'):
        if type == 'json':
            return json.dumps(self.__dict__)
        elif type == 'msgpack':
            return msgpack.dumps(self.__dict__)
        else:
            raise TypeError

class SerializableCircleMixin(SerializableMixin, Circle): pass

shapes = [Triangle(3,4,5), Rectangle(4, 5), SerializableCircleMixin(4)]
for s in shapes:
    print(s.area)

c = shapes[2]
print(c.dumps()) #{"r": 4}
用面向对象实现LinkedList链表
  • 单向链表实现append,iternodes方法。
  • 双向链表实现append,pop,insert,remove,iternodes方法。
img_20250114_164505.png
img_20250114_164506.png

对于链表来说

  • 每一个结点是一个独立的对象,结点自己知道内容是什么,下一跳是什么。
  • 而链表则是一个容器,它内部装着一个个结点对象。
  • 不管是单向还是双向,建议加一个尾tail,方便追加元素。

所以,建议设计2个类,一个是结点Node类,一个是链表LinkedList类。

如同一个箱子是容器,里面放的小球就是一个个节点。

如同一串珠子,节点对象是一个个小珠子,每一颗珠子关联其前后的珠子,所以珠子形成了串珠的概念,相当于容器。 一串珠子可以从固定的一头提起,这是单向链表;如果这串珠子可以从两头中的任意一头提起,这就是双向链表。

简单的单向链表

增加append和iternodes

1.设置两个类 item元素本身,next为下一个元素

2.设置头head和尾tail,初始为None

#单向链表

class Node:
    def __init__(self, item, next=None):
        self.item = item
        self.next = next

    def __repr__(self):
        return str(self.item)

class LinkedList:
    def __init__(self):
        self.head = None
        self.tail = None

3.尾部追加实现

  • 假设1个都没有,头尾都不动,上一个是None,下一个也是None
  • 假设有一个或多个,只需动尾部
class Node:
    def __init__(self, item, next=None):
        self.item = item
        self.next = next

    def __repr__(self):
        return str(self.item)

class LinkedList:
    def __init__(self):
        self.head = None
        self.tail = None

    def append(self, value):
        node = Node(value) # 下一个是None的结点

        if self.tail is None: # empty
            self.head = node
            self.tail = node
        else:
            self.tail.next = node
        self.tail = node
        return self

4.遍历链表

  • 从头开始遍历,如果头是None,则不进入循环
  • 判断下一个是否为None
class Node:
    def __init__(self, item, next=None):
        self.item = item
        self.next = next

    def __repr__(self):
        return str(self.item)

class LinkedList:
    def __init__(self):
        self.head = None
        self.tail = None

    def append(self, value):
        node = Node(value) # 下一个是None的结点

        if self.tail is None: # empty
            self.head = node
            self.tail = node
        else:
            self.tail.next = node
        self.tail = node
        return self

    def iternodes(self): #提醒你返回一个惰性对象
        current = self.head
        while current:
            yield current
            current = current.next

ll = LinkedList()
ll.append(4).append(5).append(6) # 链式编程

for x in ll.iternodes():
    print(x)

5.链式编程,增加add函数

class Node:
    def __init__(self, item, next=None):
        self.item = item
        self.next = next

    def __repr__(self):
        return str(self.item)

class LinkedList:
    def __init__(self):
        self.head = None
        self.tail = None

    def append(self, value):
        node = Node(value) # 下一个是None的结点

        if self.tail is None: # empty
            self.head = node
            self.tail = node
        else:
            self.tail.next = node
        self.tail = node
        return self

    def iternodes(self): #提醒你返回一个惰性对象
        current = self.head
        while current:
            yield current
            current = current.next

    def __add__(self, item):
        return self.append(item)

ll = LinkedList()
ll.append(4).append(5).append(6) # 链式编程
ll.append(7) + 8 # 调用add函数
ll + 9 + 10 # 调用add函数

for x in ll.iternodes():
    print(x)
简单的双向链表

1.设置两个类

  • item元素本身,next为下一个元素,上一个元素prev

2.设置展示格式

class Node:
    def __init__(self, item, next=None, prev=None):
        self.item = item
        self.next = next # Node instance
        self.prev = prev # Node instance

    def __repr__(self):
        return "{} <== {} ==> {}".format(
            self.prev.item if self.prev else None,
            self.item, 
            self.next.item if self.next else None
        )

3.设置头和尾

4.尾部追加和add实现

class Node:
    def __init__(self, item, next=None, prev=None):
        self.item = item
        self.next = next  # Node instance
        self.prev = prev  # Node instance

    def __repr__(self):
        return "{} <== {} ==> {}".format(
            self.prev.item if self.prev else None,
            self.item,
            self.next.item if self.next else None
        )


class LinkedList:  # Double
    def __init__(self):
        self.head = None
        self.tail = None

    def append(self, value):
        node = Node(value)  # 下一个是None的结点

        if self.tail is None:  # empty
            self.head = node
        else:
            self.tail.next = node  # 当前结点的下一个
            node.prev = self.tail
        self.tail = node
        return self

    def __add__(self, item):
        return self.append(item)

5.insert 插入

6.pop 尾部弹出

7.remove 移除

7.iternodes 遍历

class Node:
    def __init__(self, item, next=None, prev=None):
        self.item = item
        self.next = next # Node instance
        self.prev = prev # Node instance

    def __repr__(self):
        return "{} <== {} ==> {}".format(
            self.prev.item if self.prev else None,
            self.item,
            self.next.item if self.next else None
        )

class LinkedList:
    def __init__(self):
        self.head = None
        self.tail = None

    def append(self, value):
        node = Node(value) # 下一个是None的结点

        if self.tail is None: # empty
            self.head = node
        else:
            self.tail.next = node
            node.prev = self.tail
        self.tail = node
        return self

    def __add__(self, item):
        return self.append(item)

    def insert(self, index, value):
        if index < 0:
            raise IndexError('Wrong index. Not negative index {}'.format(index))

        current = None
        for i,node in enumerate(self.iternodes()):
            if i == index:
                current = node #找到了位置
                break
        else: #如果能执行self子句,说明? 没有找到,超界了
            self.append(value) # 没找到,当做尾部追加
            return

        # 找到了,容器非空,就准备在中间某个位置插入数据
        newnode = Node(value)
        prev = current.prev
        # next = current
        #1 开头插入
        if prev is None: # i==0  index==0  self.head==current
            self.head = newnode
        else: #2 非开头
            newnode.prev = prev
            prev.next = newnode
        newnode.next = current
        current.prev = newnode

    def pop(self):
        if self.tail is None:
            raise Exception("empty")

        tail = self.tail # 当前尾巴元素
        prev = tail.prev # 前尾巴一个元素
        # 1 only one 改头尾; 2 不止一个,改尾
        if prev is None: # self.head.next is None
            self.head = None
            self.tail = None
        else:
            prev.next = None
            self.tail = prev

        value = tail.item
        return value

    def remove(self, index):
        if self.tail is None:
            raise Exception("empty")

        if index < 0:
            raise IndexError('Wrong index. Not negative index {}'.format(index))

        current = None
        for i,node in enumerate(self.iternodes()):
            if i == index:
                current = node
                break
        else:
            raise IndexError('Out of range. index={}'.format(index)) # 没找到

        prev = current.prev #当前被移除节点的上一个
        next = current.next #当前被移除节点的下一个

        if prev is None and next is None: # 只有一个 # self.head == self.tail
            self.head = None
            self.tail = None
        elif prev is None: # 头部移除
            self.head = next
            next.prev = None
        elif next is None: # 尾部移除
            self.tail = prev
            prev.next = None
        else: # 中间移除
            prev.next = next
            next.prev = prev

        del current # 删除临时变量,减少引用计数,释放内存

    def iternodes(self, reverse=False):
        current = self.tail if reverse else self.head
        while current:
            yield current
            current = current.prev if reverse else current.next

ll = LinkedList() # 实例化
ll.append(0).append(1).append(2)
ll.insert(2, 4)
ll.pop()
ll.remove(0)
for x in ll.iternodes(): # 倒序遍历
    print(x)
print('-'*30)
for x in ll.iternodes(True): # 正序遍历
    print(x)

# 执行结果
# None <== 1 ==> 4
# 1 <== 4 ==> None
# ------------------------------
# 1 <== 4 ==> None
# None <== 1 ==> 4

双向链表容器化

class Node:
    __slots__ = 'item next prev'.split()

    def __init__(self, item, next=None, prev=None):
        self.item = item
        self.next = next  # Node instance
        self.prev = prev  # Node instance

    def __repr__(self):
        return "{} <== {} ==> {}".format(
            self.prev.item if self.prev else None,
            self.item,
            self.next.item if self.next else None
        )


class LinkedList:
    def __init__(self):
        self.head = None
        self.tail = None
        self._size = 0

    def append(self, value):
        node = Node(value)  # 下一个是None的结点

        if self.tail is None:  # empty
            self.head = node
        else:
            self.tail.next = node
            node.prev = self.tail
        self.tail = node
        self._size += 1
        return self

    def __add__(self, item):
        return self.append(item)

    def insert(self, index, value):
        if index >= len(self):
            self.append(value)
            return

        if index < -len(self):
            index = 0
        current = self[index]

        # 找到了,容器非空
        newnode = Node(value)
        prev = current.prev
        next = current
        # 开头插入
        if prev is None:  # i==0  index==0  self.head==current
            self.head = newnode
        else:  # 非开头
            newnode.prev = prev
            prev.next = newnode
        newnode.next = next
        next.prev = newnode
        self._size += 1

    def pop(self):
        if self.tail is None:
            raise Exception("empty")

        node = self.tail  # 当前元素
        prev = node.prev  # 前一个元素
        # only one
        if prev is None:  # self.head.next is None
            self.head = None
            self.tail = None
        else:
            prev.next = None
            self.tail = prev

        value = node.item
        self._size -= 1
        return value

    def remove(self, index):
        if self.tail is None:
            raise Exception("empty")

        current = self[index]

        prev = current.prev
        next = current.next

        if prev is None and next is None:  # 只有一个 # self.head == self.tail
            self.head = None
            self.tail = None
        elif prev is None:  # 头部移除
            self.head = next
            next.prev = None
        elif next is None:  # 尾部移除
            self.tail = prev
            prev.next = None
        else:  # 中间移除
            prev.next = next
            next.prev = prev
        self._size -= 1
        del current  # 删除临时变量,减少引用计数,释放内存

    def iternodes(self, reverse=False):
        current = self.tail if reverse else self.head
        while current:
            yield current
            current = current.prev if reverse else current.next

    # 容器
    def __len__(self):
        return self._size

    size = property(lambda self: self._size)

    def __getitem__(self, index):
        if index >= len(self) or index < -len(self):
            raise IndexError('out of index : {}'.format(index))

        reverse = True if index < 0 else False
        start = 1 if index < 0 else 0

        for i, node in enumerate(self.iternodes(reverse), start):  # index >=0
            if i == abs(index):
                return node

    def __setitem__(self, index, value):  #         self[index].item = value

    # 此魔术方法可以不实现,但是要使用reversed内建函数,必须有__len__和__getitem__
    def __reversed__(self):
        # yield from self.iternodes(True)
        return self.iternodes(True)

    # def __iter__(self):
    #     return self.iternodes()

    __iter__ = iternodes

    def __contains__(self, item):
        current = self.head
        while current:
            if item == current.item:
                return True
            current = current.next
        return False


ll = LinkedList()

ll.append(0)
ll + 1 + 2 + 3
ll.insert(2, 4)
ll.pop()
ll.remove(0)
ll[1] = 6

for x in ll.iternodes():  # 倒序遍历
    print(x)
print('-' * 30)
for x in ll.iternodes(True):  # 正序遍历
    print(x)

print(2 in ll)
print(len(ll), ll[1])

# 执行结果
# None <= = 1 == > 6
# 1 <= = 6 == > 2
# 6 <= = 2 == > None
# ------------------------------
# 6 <= = 2 == > None
# 1 <= = 6 == > 2
# None <= = 1 == > 6
# True
# 3
# 1 <= = 6 == > 2

Python系统编程

  • 正则
  • 文件处理

Python 异常处理与异常机制

主要内容:

  • 异常的工作机制
  • 异常的语法
  • Python异常类结构
  • 捕获异常
  • 抛出与转移异常
  • Try/except/else 理解与使用
  • Try/finally 理解与使用
  • With 语句及其异常域内存管理机制

    参考:https://docs.python.org/zh-cn/3/tutorial/errors.html

错误和异常

错误 Error

  • 逻辑错误:算法写错了,例如加法写成了减法
  • 笔误:例如变量名写错了,语法错误
  • 函数或类使用错误,其实这也属于逻辑错误
  • 总之,错误是可以避免的

异常 Exception

  • 本意就是意外情况
  • 这有个前提,没有出现上面说的错误,也就是说程序写的没有问题,但是在某些情况下,会出现一些意外,导致程序无法正常的执行下去
  • 例如open函数操作一个文件,文件不存在,或者创建一个文件时已经存在了,或者访问一个网络文件,突然断网了,这就是异常,是个意外的情况
  • 异常不可能避免

错误和异常

  • 在高级编程语言中,一般都有错误和异常的概念,异常是可以捕获,并被处理的,但是错误是不能被捕获的

范例:对比异常和错误

print( 1/ 0) # 除0异常

def 1a(): pass # 语法错误

with open('test') as f: #抛出异常  文件不存在
    print(f)
    f.write('abc')

一个健壮的程序

  • 应尽可能避免错误
  • 尽可能捕获、处理各种异常

产生异常

产生:

  • raise 语句显式的抛出异常
  • Python解释器自己检测到异常并引发它

范例: 检测到异常

def foo():
    print('~~~~')
    1/0  #除零异常
    print('----')

foo() #ZeroDivisionError: division by zero

范例: raise 主动抛出异常

def foo():
    print('~~~~')
    raise TypeError
    print('----')

foo() #TypeError

程序会在异常抛出的地方中断执行,如果不捕获,就会提前结束程序(其实是终止当前线程的执行)

异常的捕获

try:
        待捕获异常的代码块
except [异常类型]:
        异常的处理代码块

范例:

def foo():
    print('~~~~')
    try:
        raise TypeError
        print('++++')
    except:
        print('产生异常了')
    print('----')

foo()
print('==end==')
#返回结果
# ~~~~
# 产生异常了
# ----
# ==end==

上例执行到raise时产生异常并抛出,由于使用了try…except语句块则捕捉到了这个异常,异常生成位置之后语句将不再执行,转而执行对应的except部分的语句,最后执行try…except语句块之外的语句

捕获指定类型的异常

范例:

#LEGB B内建函数、异常类。解释器启动时,就需要加载所有built-ins内容
def foo(a):
    print('~~~~')
    try:
        b = 1/a #除以0会抛出除零异常类的实例
        # raise TypeError
        print('++++')
    # except ZeroDivisionError: #我只关心和处理除零异常 除零异常是算法异常的子类 isinstance(异常实例, 异常类型和异常类型的父类)
    except ArithmeticError:
        #异常是有传承继承,子类只需要覆盖父类的部分方法就可以了
        print('产生了除零异常了')
    print('----')

foo(0)
print('==end==')
# ~~~~
# 产生了除零异常了
# ----
# ==end==

异常类及继承层次

# Python异常的继承
#https://docs.python.org/zh-cn/3/tutorial/errors.html

BaseException             #所有异常通用的基类
 ├── BaseExceptionGroup
 ├── GeneratorExit
 ├── KeyboardInterrupt    #键盘中断
 ├── SystemExit           #系统退出
 └── Exception            #自己写的所有异常类,从这里开始继承
      ├── ArithmeticError                   #算术异常
      │    ├── FloatingPointError           #浮点数异常
      │    ├── OverflowError                #溢出异常
      │    └── ZeroDivisionError            #除零异常
      ├── AssertionError
      ├── AttributeError
      ├── BufferError
      ├── EOFError
      ├── ExceptionGroup [BaseExceptionGroup]
      ├── ImportError
      │    └── ModuleNotFoundError
      ├── LookupError                       #找位置找不着
      │    ├── IndexError                   #超界
      │    └── KeyError                     #key找不到
      ├── MemoryError                       #内存异常
      ├── NameError                         #名称不存在
      │    └── UnboundLocalError
      ├── OSError                           #操作系统里的错误
      │    ├── BlockingIOError
      │    ├── ChildProcessError            #子进程错误
      │    ├── ConnectionError
      │    │    ├── BrokenPipeError
      │    │    ├── ConnectionAbortedError
      │    │    ├── ConnectionRefusedError
      │    │    └── ConnectionResetError
      │    ├── FileExistsError              #文件已存在
      │    ├── FileNotFoundError            #文件不存在
      │    ├── InterruptedError
      │    ├── IsADirectoryError
      │    ├── NotADirectoryError
      │    ├── PermissionError
      │    ├── ProcessLookupError
      │    └── TimeoutError
      ├── ReferenceError
      ├── RuntimeError                      #运行时异常
      │    ├── NotImplementedError
      │    ├── PythonFinalizationError
      │    └── RecursionError               #递归异常
      ├── StopAsyncIteration
      ├── StopIteration                     #迭代次数超出
      ├── SyntaxError                       #语法错误
      │    └── IndentationError
      │         └── TabError
      ├── SystemError
      ├── TypeError
      ├── ValueError
      │    └── UnicodeError
      │         ├── UnicodeDecodeError
      │         ├── UnicodeEncodeError
      │         └── UnicodeTranslateError
      └── Warning
           ├── BytesWarning
           ├── DeprecationWarning
           ├── EncodingWarning
           ├── FutureWarning
           ├── ImportWarning
           ├── PendingDeprecationWarning
           ├── ResourceWarning
           ├── RuntimeWarning
           ├── SyntaxWarning
           ├── UnicodeWarning
           └── UserWarning

BaseException及子类

BasaException

所有内建异常类的基类是BasaException

SystemExit

sys.exit()函数引发的异常,异常不捕获处理,就直接交给Python解释器,解释器推出

范例:

import sys

print('-' * 30)
sys.exit(100)
print('~' * 30) #不会执行
#返回结果
# ------------------------------
#捕获异常
import sys

print('-' * 30)
try:
    sys.exit(100) #正常退出
    print('~' * 30)
except SystemExit:
    print('system exits')

print('=' * 30)
#返回结果
# ------------------------------
# system exits
# ==============================

如果except语句捕获了异常,则继续向后面执行,如果没有捕获住该异常SystemExit,解释器直接退出程序

注意捕获前后程序退出状态码的变化。

KeyboardInterrupt

对应的捕获用户终端行为Ctrl + C

import time

def foo():
    try:
        while True:
            time.sleep(1)
            print('___+++---')
    except KeyboardInterrupt:
        print('Ctrl + c') # 脚本在终端命令行执行, python t.py

foo()
#终端执行,按Ctrl加c后,返回结果
# (.venv) PS D:\project\pyprojs> python .\py-new\test.py
# ___+++---
# ___+++---
# Ctrl + c

Exception及子类

Exception是所有内建的、非系统退出的异常的基类,自定义异常应该继承自它

SyntaxError 语法错误

  • Python将这种错误也归到异常类下面的Exception下的子类,但是这种错误是不可捕获的

ArithmeticError

  • 所有算术计算引发的异常,其子类有除零异常等

LookupError

  • 使用映射的键或序列的索引无效时引发的异常的基类:IndexError,KeyError

自定义异常类

  • 从Exception继承的类

范例:自定义异常类

class MyException(Exception):
    pass

try:
    raise MyException()
except MyException: # 捕获自定义异常
    print('catch the exception')

多种捕获

except可以指定捕获的类型,捕获多种异常

import sys

class MyException(Exception):
    pass

try:
    a = 1/0
    raise MyException
    open('t1')
    sys.exit(1)
except ZeroDivisionError: #if elif 分支只进一个,except类似
    print('zero')
except ArithmeticError:
    print('ari')
except MyException:
    print('catch my exception')
except Exception:
    print('excption')
except:    #大小通吃 if elif ... else 缺省捕获,可以没有
    print('sysexit')

捕获规则

  • 捕获是从上到下依次比较,如果匹配,则执行匹配的except语句块
  • 如果被一个except语句捕获,其他except语句就不会再次捕获了
  • 如果没有任何一个except语句捕获到这个异常,则该异常向外抛出
  • except: 称为缺省捕获,缺省捕获必须是最后一个捕获语句

捕获的原则

  • 从小到大,从具体异常到宽泛的异常

as子句

先来看一个例子

class A:pass

try:
    # 1/0
    raise 1
    # raise 'abc'
    # raise {}
    # raise A
    # raise A()
except:
    print('catch the exception')

被抛出的异常,应该是异常类的实例,可以使用as子句获得这个对象

class MyException(Exception):
    def __init__(self, code, message):
        self.code = code
        self.message = message

try:
    raise MyException
    #raise后可以跟实例或者异常类
    #异常类实例化初始化, 正常应该 raise MyException(400, '')
except MyException as e:
    print('my exception +++') #MyException.__init__() missing 2 required positional arguments: 'code' and 'message'

except Exception as e:
    print(e) #MyException.__init__() missing 2 required positional arguments: 'code' and 'message'
    print('非系统退出的异常类都是我的子类的实例')
except:
    print('===end===')

#返回结果
# MyException.__init__() missing 2 required positional arguments: 'code' and 'message'
# 非系统退出的异常类都是我的子类的实例
class MyException(Exception):
    def __init__(self, code=404, message='NotFound'):
        self.code = code
        self.message = message

try:
    raise MyException
    #raise MyException() #直接写类型相当于无参构造
    #raise后可以跟实例或者异常类
    #异常类实例化初始化, 正常应该 raise MyException(400, '')
except MyException as e:
    print('my exception +++') #MyException.__init__() missing 2 required positional arguments: 'code' and 'message'

except Exception as e:
    print(e) #MyException.__init__() missing 2 required positional arguments: 'code' and 'message'
    print('非系统退出的异常类都是我的子类的实例')
except:
    print('===end===')

#返回结果
# my exception +++
class MyException(Exception):
    def __init__(self, code, message='NotFound'):
        self.code = code
        self.message = message

try:
    raise MyException(404, 'Not')
    #raise MyException() #直接写类型相当于无参构造
    #raise后可以跟实例或者异常类
    #异常类实例化初始化, 正常应该 raise MyException(400, '')
except MyException as e:
    print('my exception +++')
    print(e.code, e.message) #可以取到异常类型的属性 404 Not

except Exception as e:
    print(e)
    print('非系统退出的异常类都是我的子类的实例')
except:
    print('===end===')

#返回结果
# my exception +++
# 404 Not

#raise子句
#- raise 后可以写也可以不写
#- raise 必须写Exception或其子类的实例,如果写的是前面这些类的类,实际上会调用该类的无参构造
#- raise后什么都不写,一般不用,抛出当前激活的异常

raise语句

  • raise后要求应该是BaseException类的子类或实例,如果是类,将被无参实例化。自定义应该是Exception子类
  • raise后什么都没有,表示抛出最近一个被激活的异常,如果没有被激活的异常,则抛类型异常。这种方式很少用

finally子句

finally最终,即最后一定要执行的,try…finally语句块中,不管是否发生了异常,都要执行finally的部分

范例

try:
    f = open('test.txt')
except FileNotFoundError as e:
    print('{} {} {}'.format(e.__class__, e.errno, e.strerror))
finally:
    print('清理工作')
    # f.close()

# 执行结果
#<class 'FileNotFoundError'> 2 No such file or directory
#清理工作

注意上例中的f的作用域,解决的办法是在外部定义f

finally中一般放置资源的清理、释放工作的语句

f = None
try:
    f = open('test.txt')
except FileNotFoundError as e:
    print('{} {} {}'.format(e.__class__, e.errno, e.strerror))
finally:
    print('清理工作')
    if f:
        f.close()

也可以在finally中再次捕获异常

try:
    f = open('test.txt')
except FileNotFoundError as e:
    print('{}'.format(e))
finally:
    print('清理工作')
    try:  #异常捕获 嵌套
        f.close()
    except Exception as e:
        print(e)

# 执行结果
[Errno 2] No such file or directory: 'test.txt'
清理工作
name 'f' is not defined

语句嵌套和捕获

  • 异常语句内部可以嵌入到try块、except块、finally块
  • 异常在内部产生后,如果没有捕获到,就会继续向外抛出
  • 如果外部也没能捕获到,将继续再向外抛出,直至异常代码所在线程,导致线程崩溃
  • finally中有return、break语句,则异常就不会继续向外抛出

范例: finally中有return、break语句,则异常就不会继续向外抛出

def foo():
    try:
        ret = 1 / 0
    except KeyError as e:
        print(e)
    finally:
        print('inner fin')
        return # 异常被丢弃
try:
    foo()
except:
    print('outer catch')
finally:
    print('outer fin')

# 执行结果
inner fin
outer fin

范例:finally和try里都有return

def fn():
    try:
        print('aaa')
        return 11 #一般情况下,return语句会立即返回,所以第一个return有效
    except:
        print('bbb')
    finally:
        print('ccc')
        return 22 #由于上面的return在try里面,还有个return在finally里面,
        #而且finally不管try里面是否有异常都会执行,以最后的return有效
    return 33

print(fn())
#返回结果
# aaa
# ccc
# 22

else子句

在except之后,finally之前

没有异常的时候会执行else语句

try:
    ret = 1 * 0
except ArithmeticError as e:
    print(e)
else:
    print('ok')
finally:
    print('fin')

esle子句没有任何异常发生,则执行

总结

try:
    <语句>   #运行别的代码
except <异常类>:
    <语句>   #捕获某种类型的异常
except <异常类> as <变量名>:
    <语句>   #捕获某种类型的异常并获得对象
else:
    <语句>   #如果没有异常发生
finally:
    <语句>   #退出try时总会执行

argparse模块和PEP

argparse 模块

参数解析,3.2版本后出现。

官方文档:https://docs.python.org/3/library/argparse.html

参数分类

参数分为:

  • 位置参数:参数放在那里,就要对应一个参数位置。例如/etc就是对应一个参数位置
  • 选项参数:必须通过前面的 - 的短选项或者 – 长选项然后后面的才算它的参数,当然短选项后面也可以没有参数。
ls -l /etc
# -l 是选项参数
# /etc是位置参数

基本解析

范例:

import argparse

parser = argparse.ArgumentParser() #获得一个参数解析器
parser.print_help() #打印帮助
#返回结果
# usage: test.py [-h]
# 
# options:
#   -h, --help  show this help message and exit

解析器的参数

  • prog - 程序的名称 (默认值: os.path.basename(sys.argv[0]))
  • description - 要在参数帮助信息之前显示的文本(默认:无文本)
  • add_help - 为解析器添加一个 -h/–help 选项(默认值: True)

范例

import argparse

parser = argparse.ArgumentParser(
    prog='ls',
    description='List information about the FILEs (the current directory by default)',
    add_help=False
)
parser.print_help()
#返回结果
# usage: ls
# 
# List information about the FILEs (the current directory by default)

位置参数解析

import argparse

parser = argparse.ArgumentParser(
    prog='ls',
    description='List information about the FILEs (the current directory by default)',
    add_help=False
)
parser.add_argument('path') #位置参数,必须写参数
parser.print_help() #打印帮助
print('-' * 30)
args = parser.parse_args() #使用解析器,解析参数,真正的解析
print(args) #返回Namespace类型实例,实例有属性,属性是参数
print(args.path)

#返回结果
# (.venv) PS D:\project\pyproj> python .\test.py d:/
# usage: ls path
#
# List information about the FILEs (the current directory by default)
#
# positional arguments:
#   path
# ------------------------------
# Namespace(path='d:/')
# d:/
usage: ls [-h] path

[xx] #可有可无
path #必须提供
传参
parse_args(self, args=None, namespace=None)
  • args参数列表,一个可迭代对象。内部会把可迭代对象转换成list。
  • 如果为None则使用命令行传入参数,非None则使用args参数的可迭代对象。
import argparse

parser = argparse.ArgumentParser(
    prog='ls',
    description='List information about the FILEs (the current directory by default)',
    add_help=False
)
parser.add_argument('path') #位置参数,必须写参数
parser.print_help() #打印帮助


args = parser.parse_args(('o:/abc',)) #使用解析器,解析参数,真正的解析
print(args, args.__dict__) #返回Namespace类型实例,实例有属性,属性是参数
print(args.path)

#返回结果
# usage: ls path
# 
# List information about the FILEs (the current directory by default)
# 
# positional arguments:
#   path
# Namespace(path='o:/abc') {'path': 'o:/abc'}
# o:/abc

Namespace(path='d:/')里面的path参数存储在一个Namespace对象的属性上,可以 通过Namespace对象属性来访问,如args.path

非必须位置参数

上面代码必须输入位置参数,否则会报错。

import argparse

parser = argparse.ArgumentParser(
    prog='ls',
    description='List information about the FILEs (the current directory by default)',
    add_help=True
)
parser.add_argument('path', nargs='?', default='.', help='Path help') #位置参数,可有可无,缺省值,帮助
parser.print_help() #打印帮助


# args = parser.parse_args(('o:/abc',)) #使用解析器,解析参数,真正的解析 python t.py o:/abc
args = parser.parse_args() #使用解析器,解析参数,真正的解析 python t.py
print(args, args.__dict__) #返回Namespace类型实例,实例有属性,属性是参数
print(args.path)

#返回结果
# usage: ls [-h] [path]
#
# List information about the FILEs (the current directory by default)
#
# positional arguments:
#   path        Path help
#
# options:
#   -h, --help  show this help message and exit
# Namespace(path='.') {'path': '.'}

可以看出path也变成可选的位置参数,没有提供就使用默认值 .点号 表示当前路径。

  • help 表示帮助文档中这个参数的描述
  • nargs 表示这个参数接收结果参数
    • ? 表示可有可无
    • + 表示至少一个
    • * 可以任意个
    • 数字表示必须是指定数目个
  • default 表示如果不提供该参数,就使用这个值。一般和 ?、* 配合,因为它们都可以 不提供位置参数,不提供就是缺省值

选项参数

-l 的实现
parser.add_argument('-l', action='store_true', help='Long list format') # l属性名
parser.add_argument('-l', action='store_const', const=22) #提供 -l 选项,对应属性值为22; 否则,对应值为None

范例:

import argparse

parser = argparse.ArgumentParser(
    prog='ls',
    description='List information about the FILEs (the current directory by default)',
    add_help=True
)
parser.add_argument('path', nargs='?', default='.', help='Path help') #位置参数,可有可无,缺省值,帮助
parser.add_argument('-l', action='store_true', help='Long list format') # l属性名
parser.print_help() #打印帮助


# args = parser.parse_args(('o:/abc', '-l')) #使用解析器,解析参数,真正的解析 python t.py o:/abc -l
# args = parser.parse_args('-l o:/tmp'.split()) # python t.py -l o:/tmp
args = parser.parse_args() # python t.py .
args = parser.parse_args(['-l']) # python t.py -l
print(args) #返回Namespace类型实例,实例有属性,属性是参数
print(args.path)

#返回结果
# usage: ls [-h] [-l] [path]
# 
# List information about the FILEs (the current directory by default)
# 
# positional arguments:
#   path        Path help
# 
# options:
#   -h, --help  show this help message and exit
#   -l          Long list format
# Namespace(path='.', l=True)
# .
import argparse

parser = argparse.ArgumentParser(
    prog='ls',
    description='List information about the FILEs (the current directory by default)',
    add_help=True
)
parser.add_argument('path', nargs='?', default='.', help='Path help') #位置参数,可有可无,缺省值,帮助
parser.add_argument('-l', action='store_const', const=22, default=33, help='Long list format') # l属性名 store_const 存一个常量
parser.print_help() #打印帮助


# args = parser.parse_args(('o:/abc', '-l')) #使用解析器,解析参数,真正的解析 python t.py o:/abc -l
# args = parser.parse_args('-l o:/tmp'.split()) # python t.py -l o:/tmp
args = parser.parse_args() # python t.py .
args = parser.parse_args([]) # python t.py -l 33 .
print(args) #返回Namespace类型实例,实例有属性,属性是参数


#返回结果
# usage: ls [-h] [-l] [path]
# 
# List information about the FILEs (the current directory by default)
# 
# positional arguments:
#   path        Path help
# 
# options:
#   -h, --help  show this help message and exit
#   -l          Long list format
# Namespace(path='.', l=33)
-a 的实现
parser.add_argument('-a', '--all', action='store_true')  #长短选项同时给

范例:

import argparse

parser = argparse.ArgumentParser(
    prog='ls',
    description='List information about the FILEs (the current directory by default)',
    add_help=True
)
parser.add_argument('path', nargs='?', default='.', help='Path help') #位置参数,可有可无,缺省值,帮助
parser.add_argument('-l', action='store_true', help='Long list format') # l属性名
parser.add_argument('-a', '--all', action='store_true', help='Do not ignore entries starting with .')
parser.print_help() #打印帮助


# args = parser.parse_args(('o:/abc', '-l')) #使用解析器,解析参数,真正的解析 python t.py o:/abc -l
# args = parser.parse_args('-l o:/tmp'.split()) # python t.py -l o:/tmp
# args = parser.parse_args() # python t.py .
# args = parser.parse_args(['-la']) # Namespace(path='.', l=True, all=True)
args = parser.parse_args(['-l', '-a']) #Namespace(path='.', l=True, all=True)
print(args) #返回Namespace类型实例,实例有属性,属性是参数


#返回结果
# usage: ls [-h] [-l] [-a] [path]
# 
# List information about the FILEs (the current directory by default)
# 
# positional arguments:
#   path        Path help
# 
# options:
#   -h, --help  show this help message and exit
#   -l          Long list format
#   -a, --all   Do not ignore entries starting with .
# Namespace(path='.', l=True, all=True)

属性名称

参数都Namespace对象的属性,如果想指定这些属性名,可以使用dest

parser.add_argument('-l', action='store_true', dest='longfmt') 

范例

import argparse

parser = argparse.ArgumentParser(
    prog='ls',
    description='List information about the FILEs (the current directory by default)',
    add_help=True
)
parser.add_argument('path', nargs='?', default='.', help='Path help') #位置参数,可有可无,缺省值,帮助
parser.add_argument('-l', action='store_true', dest='longfmt', help='Long list format') # l属性名
parser.add_argument('-a', '--all', action='store_true', help='Do not ignore entries starting with .')
parser.print_help() #打印帮助


# args = parser.parse_args(('o:/abc', '-l')) #使用解析器,解析参数,真正的解析 python t.py o:/abc -l
# args = parser.parse_args('-l o:/tmp'.split()) # python t.py -l o:/tmp
# args = parser.parse_args() # python t.py .
# args = parser.parse_args(['-la']) # Namespace(path='.', l=True, all=True)
args = parser.parse_args(['-l', '-a']) #Namespace(path='.', l=True, all=True)
print(args) #返回Namespace类型实例,实例有属性,属性是参数
print(args.longfmt)


#返回结果
# usage: ls [-h] [-l] [-a] [path]
# 
# List information about the FILEs (the current directory by default)
# 
# positional arguments:
#   path        Path help
# 
# options:
#   -h, --help  show this help message and exit
#   -l          Long list format
#   -a, --all   Do not ignore entries starting with .
# Namespace(path='.', longfmt=True, all=True)
# True

练习,实现ls命令功能

实例-l -a 和 –all -h选项

  • 实现显示路径下的文件列表
  • -a和–all 显示包含.开头的文件
  • -l 详细列表显示
  • -h和-l配合,人性化显示文件大小
  • 显示文件类型
    • c 字符
    • d 目录
    • 普通文件
    • l 链接文件
    • b 块设备
    • s socket 文件
    • p 管道文件,即FIFO

列出目录

import argparse
from pathlib import Path

parser = argparse.ArgumentParser(
    prog='ls',
    description='List information about the FILEs (the current directory by default)',
    add_help=True
)
parser.add_argument('path', nargs='?', default='.', help='Path help') #位置参数,可有可无,缺省值,帮助
parser.add_argument('-l', action='store_true', dest='longfmt', help='Long list format') # l属性名
parser.add_argument('-a', '--all', action='store_true', help='Do not ignore entries starting with .')
parser.print_help() #打印帮助


# args = parser.parse_args(('o:/abc', '-l')) #使用解析器,解析参数,真正的解析 python t.py o:/abc -l
# args = parser.parse_args('-l o:/tmp'.split()) # python t.py -l o:/tmp
# args = parser.parse_args() # python t.py .
# args = parser.parse_args(['-la']) # Namespace(path='.', l=True, all=True)
args = parser.parse_args(['-l', '-a']) #Namespace(path='.', l=True, all=True)
print(args) #返回Namespace类型实例,实例有属性,属性是参数
print(args.longfmt)


def list_dir(path:str):
    p = Path(path)
    if p.is_dir():
        for f in p.iterdir():
            print(f)
    else:
        pass

list_dir(args.path)

解决-a参数

import argparse
from pathlib import Path

parser = argparse.ArgumentParser(
    prog='ls',
    description='List information about the FILEs (the current directory by default)',
    add_help=True
)
parser.add_argument('path', nargs='?', default='.', help='Path help') #位置参数,可有可无,缺省值,帮助
parser.add_argument('-l', action='store_true', dest='longfmt', help='Long list format') # l属性名
parser.add_argument('-a', '--all', action='store_true', help='Do not ignore entries starting with .')
parser.print_help() #打印帮助


# args = parser.parse_args(('o:/abc', '-l')) #使用解析器,解析参数,真正的解析 python t.py o:/abc -l
# args = parser.parse_args('-l o:/tmp'.split()) # python t.py -l o:/tmp
# args = parser.parse_args() # python t.py .
# args = parser.parse_args(['-la']) # Namespace(path='.', l=True, all=True)
args = parser.parse_args(['-l', '-a']) #Namespace(path='.', l=True, all=True)
print(args) #返回Namespace类型实例,实例有属性,属性是参数
print(args.longfmt)


def list_dir(path:str , all=False): # -a
    p = Path(path)
    if p.is_dir(): #只允许列出目录 #怎么知道它是隐藏文件?
        # for f in p.iterdir():
        #     # print(f, type(f), f.name, type(f.name))
        #     if not all and f.name.startswith('.'):
        #         continue
        #     # print(f.name)
        #     yield f.name
        # yeild from filter(lambda f: not (not all and f.name.startswith('.')), p.iterdir())
        yield from filter(lambda f: all or not f.name.startswith('.'), p.iterdir())
    else:
        pass

print(*list_dir(args.path, args.all), sep='\n')

解决长格式

import argparse
from pathlib import Path

parser = argparse.ArgumentParser(
    prog='ls',
    description='List information about the FILEs (the current directory by default)',
    add_help=True
)
parser.add_argument('path', nargs='?', default='.', help='Path help') #位置参数,可有可无,缺省值,帮助
parser.add_argument('-l', action='store_true', dest='longfmt', help='Long list format') # l属性名
parser.add_argument('-a', '--all', action='store_true', help='Do not ignore entries starting with .')
parser.print_help() #打印帮助


# args = parser.parse_args(('o:/abc', '-l')) #使用解析器,解析参数,真正的解析 python t.py o:/abc -l
# args = parser.parse_args('-l o:/tmp'.split()) # python t.py -l o:/tmp
# args = parser.parse_args() # python t.py .
# args = parser.parse_args(['-la']) # Namespace(path='.', l=True, all=True)
args = parser.parse_args(['-l', '-a']) #Namespace(path='.', l=True, all=True)
print(args) #返回Namespace类型实例,实例有属性,属性是参数
print(args.longfmt)


def list_dir(path:str , all=False): # -a
    p = Path(path)
    if p.is_dir(): #只允许列出目录 #怎么知道它是隐藏文件?
        # for f in p.iterdir():
        #     # print(f, type(f), f.name, type(f.name))
        #     if not all and f.name.startswith('.'):
        #         continue
        #     # print(f.name)
        #     yield f.name
        # yeild from filter(lambda f: not (not all and f.name.startswith('.')), p.iterdir())
        yield from filter(lambda f: all or not f.name.startswith('.'), p.iterdir())
    else:
        pass

print(*list_dir(args.path, args.all), sep='\n')
for x in list_dir(args.path):
    print(type(x), x.name, str(x)) #<class 'pathlib._local.WindowsPath'> t.py t.py
    break

def _get_type(f:Path):
    if f.is_dir():
        return 'd'
    elif f.is_symlink():
        return 'l'
    elif f.is_socket():
        return 's'
    elif f.is_fifo():
        return 'p'
    elif f.is_block_device():
        return 'b'
    elif f.is_char_device():
        return 'c'
    elif f.is_fifo():
        return 'p'
    else:
        return '-'

# modelist = ['---', '--x', '-w-', '-wx', 'r--', 'r-x', 'rw-', 'rwx']
# modelist = dict(zip('01234567', modelist)) 
modelist ={'0': '---', '1': '--x', '2': '-w-', '3': '-wx', '4': 'r--', '5': 'r-x', '6': 'rw-', '7': 'rwx'}

def _get_mode_str(mode:int):
    print(mode, type(mode), oct(mode)[-3:])
    mstr = ''
    for c in oct(mode)[-3:]:
        mstr += modelist[c]
    return mstr

def list_detail(path:str, all=False, detail=False):
    p = Path(path)
    if p.is_dir(): #只允许列出目录 #怎么知道它是隐藏文件?
        for f in p.absolute().iterdir():
            # print(f, type(f), f.name, type(f.name))
            if not all and f.name.startswith('.'):
                continue
            #长格式 -rw-r--r-- 1 Administrator 197121 62 Feb 26 17:35 __main__.log
            #       type  mode  硬链接  owner  group  size  mtime  name
            st = f.stat()
            print(st)
            #文件类型? #mode 算法
            typ = _get_type(f)
            mode = _get_mode_str(st.st_mode) #33206 -> oct()
            yield typ, mode

        # yield from map(lambda x: x.name, filter(lambda f: all or not f.name.startswith('.'), p.absolute().iterdir()))
    else:
        pass

for x in list_detail(args.path, args.all, args.longfmt):
    print(type(x), x)
    break

#返回结果
# usage: ls [-h] [-l] [-a] [path]

# List information about the FILEs (the current directory by default)

# positional arguments:
#   path        Path help

# options:
#   -h, --help  show this help message and exit
#   -l          Long list format
#   -a, --all   Do not ignore entries starting with .
# Namespace(path='.', longfmt=True, all=True)
# True
# t.py
# <class 'pathlib._local.WindowsPath'> t.py t.py
# os.stat_result(st_mode=33206, st_ino=281474976809275, st_dev=515109334059982733, st_nlink=1, st_uid=0, st_gid=0, st_size=3536, st_atime=1740649238, st_mtime=1740649236, st_ctime=1740549471)
# 33206 <class 'int'> 666
# <class 'tuple'> ('-', 'rw-rw-rw-')
import argparse
from pathlib import Path

parser = argparse.ArgumentParser(
    prog='ls',
    description='List information about the FILEs (the current directory by default)',
    add_help=True
)
parser.add_argument('path', nargs='?', default='.', help='Path help')  # 位置参数,可有可无,缺省值,帮助
parser.add_argument('-l', action='store_true', dest='longfmt', help='Long list format')  # l属性名
parser.add_argument('-a', '--all', action='store_true', help='Do not ignore entries starting with .')
parser.print_help()  # 打印帮助

# args = parser.parse_args(('o:/abc', '-l')) #使用解析器,解析参数,真正的解析 python t.py o:/abc -l
# args = parser.parse_args('-l o:/tmp'.split()) # python t.py -l o:/tmp
# args = parser.parse_args() # python t.py .
# args = parser.parse_args(['-la']) # Namespace(path='.', l=True, all=True)
args = parser.parse_args(['-l', '-a'])  # Namespace(path='.', l=True, all=True)
print(args)  # 返回Namespace类型实例,实例有属性,属性是参数
print(args.longfmt)


def list_dir(path: str, all=False):  # -a
    p = Path(path)
    if p.is_dir():  # 只允许列出目录 #怎么知道它是隐藏文件?
        # for f in p.iterdir():
        #     # print(f, type(f), f.name, type(f.name))
        #     if not all and f.name.startswith('.'):
        #         continue
        #     # print(f.name)
        #     yield f.name
        # yeild from filter(lambda f: not (not all and f.name.startswith('.')), p.iterdir())
        yield from filter(lambda f: all or not f.name.startswith('.'), p.iterdir())
    else:
        pass


print(*list_dir(args.path, args.all), sep='\n')
for x in list_dir(args.path):
    print(type(x), x.name, str(x))  # <class 'pathlib._local.WindowsPath'> t.py t.py
    break


def _get_type(f: Path):
    if f.is_dir():
        return 'd'
    elif f.is_symlink():
        return 'l'
    elif f.is_socket():
        return 's'
    elif f.is_fifo():
        return 'p'
    elif f.is_block_device():
        return 'b'
    elif f.is_char_device():
        return 'c'
    elif f.is_fifo():
        return 'p'
    else:
        return '-'


#取权限方法1
# # modelist = ['---', '--x', '-w-', '-wx', 'r--', 'r-x', 'rw-', 'rwx']
# # modelist = dict(zip('01234567', modelist)) 
# modelist ={'0': '---', '1': '--x', '2': '-w-', '3': '-wx', '4': 'r--', '5': 'r-x', '6': 'rw-', '7': 'rwx'}

# def _get_mode_str(mode:int):
#     print(mode, type(mode), oct(mode)[-3:]) #33206 <class 'int'> 666
#     mstr = ''
#     for c in oct(mode)[-3:]:
#         mstr += modelist[c]
#     return mstr

#取权限方法2
# modelist = 'rwxrwxrwx' #rwx 1为对应位置值,0为空
# def _get_mode_str(mode:int):
#     print(mode, type(mode), bin(mode)[-9:], '{:09b}'.format(mode)) #110 110 110 是0b1000000110110110的后9位
#     mstr = ''
#     for i,c in enumerate('{:09b}'.format(mode)[-9:]): #110110110
#         mstr += modelist[i] if c == '1' else '-'
#     return mstr

#取权限方法3-移位
modelist = 'rwxrwxrwx'
def _get_mode_str(mode:int):
    # print(mode, type(mode), bin(mode)[-9:], '{:09b}'.format(mode), mode & 0o777) #位与,只要后9位
    #移位计算
    mstr = ''
    for i in range(8, -1, -1):
        mstr += modelist[8-i] if mode >> i & 1 else '-'
    return mstr

def list_detail(path: str, all=False, detail=False):
    p = Path(path)
    if p.is_dir():  # 只允许列出目录 #怎么知道它是隐藏文件?
        for f in p.absolute().iterdir():
            # print(f, type(f), f.name, type(f.name))
            if not all and f.name.startswith('.'):
                continue
            # 长格式 -rw-r--r-- 1 Administrator 197121 62 Feb 26 17:35 __main__.log
            #       type  mode  硬链接  owner  group  size  mtime  name
            st = f.stat()
            print(st)
            # 文件类型? #mode 算法
            typ = _get_type(f)
            mode = _get_mode_str(st.st_mode)  # 33206 -> oct()
            yield typ+mode

        # yield from map(lambda x: x.name, filter(lambda f: all or not f.name.startswith('.'), p.absolute().iterdir()))
    else:
        pass


for x in list_detail(args.path, args.all, args.longfmt):
    print(type(x), x)
    break

#返回结果
# <class 'str'> drwxrwxrwx

直接使用stat模块中的filemode方法

import argparse
from pathlib import Path
from stat import filemode

parser = argparse.ArgumentParser(
    prog='ls',
    description='List information about the FILEs (the current directory by default)',
    add_help=True
)
parser.add_argument('path', nargs='?', default='.', help='Path help')  # 位置参数,可有可无,缺省值,帮助
parser.add_argument('-l', action='store_true', dest='longfmt', help='Long list format')  # l属性名
parser.add_argument('-a', '--all', action='store_true', help='Do not ignore entries starting with .')
parser.print_help()  # 打印帮助

# args = parser.parse_args(('o:/abc', '-l')) #使用解析器,解析参数,真正的解析 python t.py o:/abc -l
# args = parser.parse_args('-l o:/tmp'.split()) # python t.py -l o:/tmp
# args = parser.parse_args() # python t.py .
# args = parser.parse_args(['-la']) # Namespace(path='.', l=True, all=True)
args = parser.parse_args(['-l', '-a'])  # Namespace(path='.', l=True, all=True)
print(args)  # 返回Namespace类型实例,实例有属性,属性是参数
print(args.longfmt)


def list_dir(path: str, all=False):  # -a
    p = Path(path)
    if p.is_dir():  # 只允许列出目录 #怎么知道它是隐藏文件?
        # for f in p.iterdir():
        #     # print(f, type(f), f.name, type(f.name))
        #     if not all and f.name.startswith('.'):
        #         continue
        #     # print(f.name)
        #     yield f.name
        # yeild from filter(lambda f: not (not all and f.name.startswith('.')), p.iterdir())
        yield from filter(lambda f: all or not f.name.startswith('.'), p.iterdir())
    else:
        pass


print(*list_dir(args.path, args.all), sep='\n')
for x in list_dir(args.path):
    print(type(x), x.name, str(x))  # <class 'pathlib._local.WindowsPath'> t.py t.py
    break


def _get_type(f: Path):
    if f.is_dir():
        return 'd'
    elif f.is_symlink():
        return 'l'
    elif f.is_socket():
        return 's'
    elif f.is_fifo():
        return 'p'
    elif f.is_block_device():
        return 'b'
    elif f.is_char_device():
        return 'c'
    elif f.is_fifo():
        return 'p'
    else:
        return '-'


# # modelist = ['---', '--x', '-w-', '-wx', 'r--', 'r-x', 'rw-', 'rwx']
# # modelist = dict(zip('01234567', modelist))
# modelist = {'0': '---', '1': '--x', '2': '-w-', '3': '-wx', '4': 'r--', '5': 'r-x', '6': 'rw-', '7': 'rwx'}
#
#
# def _get_mode_str(mode: int):
#     print(mode, type(mode), oct(mode)[-3:])
#     mstr = ''
#     for c in oct(mode)[-3:]:
#         mstr += modelist[c]
#     return mstr

#方法3-移位
# modelist = 'rwxrwxrwx'
# def _get_mode_str(mode:int):
#     # print(mode, type(mode), bin(mode)[-9:], '{:09b}'.format(mode), mode & 0o777) #位与,只要后9位
#     #移位计算
#     mstr = ''
#     for i in range(8, -1, -1):
#         mstr += modelist[8-i] if mode >> i & 1 else '-'
#     return mstr

def list_detail(path: str, all=False, detail=False):
    p = Path(path)
    if p.is_dir():  # 只允许列出目录 #怎么知道它是隐藏文件?
        for f in p.absolute().iterdir():
            # print(f, type(f), f.name, type(f.name))
            if not all and f.name.startswith('.'):
                continue
            # 长格式 -rw-r--r-- 1 Administrator 197121 62 Feb 26 17:35 __main__.log
            #       type  mode  硬链接  owner  group  size  mtime  name
            st = f.stat()
            print(st)
            # 文件类型? #mode 算法
            # typ = _get_type(f)
            # mode = _get_mode_str(st.st_mode)  # 33206 -> oct()
            yield filemode(st.st_mode)

        # yield from map(lambda x: x.name, filter(lambda f: all or not f.name.startswith('.'), p.absolute().iterdir()))
    else:
        pass


for x in list_detail(args.path, args.all, args.longfmt):
    print(type(x), x)
    break
import argparse
from pathlib import Path
from stat import filemode
import datetime

parser = argparse.ArgumentParser(
    prog='ls',
    description='List information about the FILEs (the current directory by default)',
    add_help=True
)
parser.add_argument('path', nargs='?', default='.', help='Path help')  # 位置参数,可有可无,缺省值,帮助
parser.add_argument('-l', action='store_true', dest='longfmt', help='Long list format')  # l属性名
parser.add_argument('-a', '--all', action='store_true', help='Do not ignore entries starting with .')
parser.print_help()  # 打印帮助

# args = parser.parse_args(('o:/abc', '-l')) #使用解析器,解析参数,真正的解析 python t.py o:/abc -l
# args = parser.parse_args('-l o:/tmp'.split()) # python t.py -l o:/tmp
# args = parser.parse_args() # python t.py .
# args = parser.parse_args(['-l', '-a'])  # Namespace(path='.', l=True, all=True)


def list_detail(path: str, all=False, detail=False):
    p = Path(path)
    if p.is_dir():  # 只允许列出目录 #怎么知道它是隐藏文件?
        for f in p.absolute().iterdir():
            # print(f, type(f), f.name, type(f.name))
            if not all and f.name.startswith('.'):
                continue
            # 长格式 -rw-r--r-- 1 Administrator 197121 62 Feb 26 17:35 __main__.log
            #       type  mode  硬链接  owner  group  size  mtime  name
            if not detail:
                yield (f.name,)
            else:
                st = f.stat()
                # print(st)
                size = st.st_size
                mtime = "{:%Y-%m-%d %H:%M:%S}".format(datetime.datetime.fromtimestamp(st.st_mtime))
                yield filemode(st.st_mode), st.st_nlink, st.st_uid, st.st_gid, size, mtime, f.name
    else:
        pass

if __name__ == '__main__':
    args = parser.parse_args(['-la'])  # Namespace(path='.', longfmt=True, all=True)
    print(args)  # 返回Namespace类型实例,实例有属性,属性是参数

    for x in list_detail(args.path, args.all, args.longfmt):
        print(x)
        # break

#返回结果
# usage: ls [-h] [-l] [-a] [path]
#
# List information about the FILEs (the current directory by default)
#
# positional arguments:
#   path        Path help
#
# options:
#   -h, --help  show this help message and exit
#   -l          Long list format
#   -a, --all   Do not ignore entries starting with .
# Namespace(path='.', longfmt=True, all=True)
# ('drwxrwxrwx', 1, 0, 0, 0, '2025-02-27 00:16:42', 'm')
# ('-rw-rw-rw-', 1, 0, 0, 191, '2025-01-06 23:37:23', 'mysql.ini')
# ('-rw-rw-rw-', 1, 0, 0, 17772, '2025-01-08 20:39:39', 'sample.txt')
# ('-rw-rw-rw-', 1, 0, 0, 250, '2025-01-07 00:44:02', 't.ini')
# ('-rw-rw-rw-', 1, 0, 0, 1082, '2025-01-14 21:51:12', 't2.py')
# ('-rw-rw-rw-', 1, 0, 0, 2133, '2025-02-27 21:25:42', 'test.py')

-r 排序

import argparse
from pathlib import Path
from stat import filemode
import datetime

parser = argparse.ArgumentParser(
    prog='ls',
    description='List information about the FILEs (the current directory by default)',
    add_help=True
)
parser.add_argument('path', nargs='?', default='.', help='Path help')  # 位置参数,可有可无,缺省值,帮助
parser.add_argument('-l', action='store_true', dest='longfmt', help='Long list format')  # l属性名
parser.add_argument('-a', '--all', action='store_true', help='Do not ignore entries starting with .')

#-r reverse 按文件名称排序
parser.add_argument('-r', '--reverse', action='store_true', help='reverse order while sorting')
parser.print_help()  # 打印帮助

# args = parser.parse_args(('o:/abc', '-l')) #使用解析器,解析参数,真正的解析 python t.py o:/abc -l
# args = parser.parse_args('-l o:/tmp'.split()) # python t.py -l o:/tmp
# args = parser.parse_args() # python t.py .
# args = parser.parse_args(['-l', '-a'])  # Namespace(path='.', l=True, all=True)


def list_detail(path: str, all=False, detail=False, reverse=False):
    p = Path(path)
    if p.is_dir():  # 只允许列出目录 #怎么知道它是隐藏文件?
        # for f in p.absolute().iterdir():
        for f in sorted(p.iterdir(), key=lambda f: f.name, reverse=reverse):
            # print(f, type(f), f.name, type(f.name))
            if not all and f.name.startswith('.'):
                continue
            # 长格式 -rw-r--r-- 1 Administrator 197121 62 Feb 26 17:35 __main__.log
            #       type  mode  硬链接  owner  group  size  mtime  name
            if not detail:
                yield (f.name,)
            else:
                st = f.stat()
                # print(st)
                size = st.st_size
                mtime = "{:%Y-%m-%d %H:%M:%S}".format(datetime.datetime.fromtimestamp(st.st_mtime))
                yield filemode(st.st_mode), st.st_nlink, st.st_uid, st.st_gid, size, mtime, f.name
    else:
        pass

if __name__ == '__main__':
    args = parser.parse_args(['-lar'])  # Namespace(path='.', longfmt=True, all=True)
    print(args)  # 返回Namespace类型实例,实例有属性,属性是参数

    for x in list_detail(args.path, args.all, args.longfmt, args.reverse):
        print(x)
        # break

#返回结果
# usage: ls [-h] [-l] [-a] [-r] [path]
# 
# List information about the FILEs (the current directory by default)
# 
# positional arguments:
#   path           Path help
# 
# options:
#   -h, --help     show this help message and exit
#   -l             Long list format
#   -a, --all      Do not ignore entries starting with .
#   -r, --reverse  reverse order while sorting
# Namespace(path='.', longfmt=True, all=True, reverse=True)
# ('-rw-rw-rw-', 1, 0, 0, 3149, '2025-02-28 00:28:52', 'test.py')
# ('-rw-rw-rw-', 1, 0, 0, 1082, '2025-01-14 21:51:12', 't2.py')
# ('-rw-rw-rw-', 1, 0, 0, 250, '2025-01-07 00:44:02', 't.ini')
# ('-rw-rw-rw-', 1, 0, 0, 17772, '2025-01-08 20:39:39', 'sample.txt')
# ('-rw-rw-rw-', 1, 0, 0, 191, '2025-01-06 23:37:23', 'mysql.ini')
# ('drwxrwxrwx', 1, 0, 0, 0, '2025-02-27 00:16:42', 'm')

排序方法2

import argparse
from pathlib import Path
from stat import filemode
import datetime

parser = argparse.ArgumentParser(
    prog='ls',
    description='List information about the FILEs (the current directory by default)',
    add_help=True
)
parser.add_argument('path', nargs='?', default='.', help='Path help')  # 位置参数,可有可无,缺省值,帮助
parser.add_argument('-l', action='store_true', dest='longfmt', help='Long list format')  # l属性名
parser.add_argument('-a', '--all', action='store_true', help='Do not ignore entries starting with .')

#-r reverse 按文件名称排序
parser.add_argument('-r', '--reverse', action='store_true', help='reverse order while sorting')
parser.print_help()  # 打印帮助

# args = parser.parse_args(('o:/abc', '-l')) #使用解析器,解析参数,真正的解析 python t.py o:/abc -l
# args = parser.parse_args('-l o:/tmp'.split()) # python t.py -l o:/tmp
# args = parser.parse_args() # python t.py .
# args = parser.parse_args(['-l', '-a'])  # Namespace(path='.', l=True, all=True)


def list_detail(path: str, all=False, detail=False, reverse=False):
    def _listdir(path, all, detail, reverse):
        p = Path(path)
        if p.is_dir():  # 只允许列出目录 #怎么知道它是隐藏文件?
            for f in p.absolute().iterdir():
            # for f in sorted(p.iterdir(), key=lambda f: f.name, reverse=reverse):
                # print(f, type(f), f.name, type(f.name))
                if not all and f.name.startswith('.'):
                    continue
                # 长格式 -rw-r--r-- 1 Administrator 197121 62 Feb 26 17:35 __main__.log
                #       type  mode  硬链接  owner  group  size  mtime  name
                if not detail:
                    yield (f.name,)
                else:
                    st = f.stat()
                    # print(st)
                    size = st.st_size
                    mtime = "{:%Y-%m-%d %H:%M:%S}".format(datetime.datetime.fromtimestamp(st.st_mtime))
                    yield filemode(st.st_mode), st.st_nlink, st.st_uid, st.st_gid, size, mtime, f.name
        else:
            pass

    return sorted(_listdir(path, all, detail, reverse), key=lambda x: x[-1], reverse=reverse)

if __name__ == '__main__':
    args = parser.parse_args(['-lar'])  # Namespace(path='.', longfmt=True, all=True)
    print(args)  # 返回Namespace类型实例,实例有属性,属性是参数

    for x in list_detail(args.path, args.all, args.longfmt, args.reverse):
        print(x)
        # break

#返回结果
# usage: ls [-h] [-l] [-a] [-r] [path]
# 
# List information about the FILEs (the current directory by default)
# 
# positional arguments:
#   path           Path help
# 
# options:
#   -h, --help     show this help message and exit
#   -l             Long list format
#   -a, --all      Do not ignore entries starting with .
#   -r, --reverse  reverse order while sorting
# Namespace(path='.', longfmt=True, all=True, reverse=True)
# ('-rw-rw-rw-', 1, 0, 0, 3448, '2025-02-28 00:33:43', 'test.py')
# ('-rw-rw-rw-', 1, 0, 0, 1082, '2025-01-14 21:51:12', 't2.py')
# ('-rw-rw-rw-', 1, 0, 0, 250, '2025-01-07 00:44:02', 't.ini')
# ('-rw-rw-rw-', 1, 0, 0, 17772, '2025-01-08 20:39:39', 'sample.txt')
# ('-rw-rw-rw-', 1, 0, 0, 191, '2025-01-06 23:37:23', 'mysql.ini')
# ('drwxrwxrwx', 1, 0, 0, 0, '2025-02-27 00:16:42', 'm')

-h 人类可读

import argparse
from pathlib import Path
from stat import filemode
import datetime

parser = argparse.ArgumentParser(
    prog='ls',
    description='List information about the FILEs (the current directory by default)',
    add_help=False
)
parser.add_argument('path', nargs='?', default='.', help='Path help')  # 位置参数,可有可无,缺省值,帮助
parser.add_argument('-l', action='store_true', dest='longfmt', help='Long list format')  # l属性名
parser.add_argument('-a', '--all', action='store_true', help='Do not ignore entries starting with .')

#-r reverse 按文件名称排序
parser.add_argument('-r', '--reverse', action='store_true', help='reverse order while sorting')
parser.add_argument('-h', '--human-readable', dest='human', action='store_true')
parser.add_argument('--help')
parser.print_help()  # 打印帮助

# args = parser.parse_args(('o:/abc', '-l')) #使用解析器,解析参数,真正的解析 python t.py o:/abc -l
# args = parser.parse_args('-l o:/tmp'.split()) # python t.py -l o:/tmp
# args = parser.parse_args() # python t.py .
# args = parser.parse_args(['-l', '-a'])  # Namespace(path='.', l=True, all=True)

def _get_size(size:int): #多少字节
    units = ' KM'
    depth = 0
    while size >= 1000 and depth < len(units):
        size //= 1000
        depth += 1
    return "{}{}".format(size, units[depth] if depth else '')

def _listdir(path, all, detail, reverse, human):
    p = Path(path)
    if p.is_dir():  # 只允许列出目录 #怎么知道它是隐藏文件?
        for f in p.absolute().iterdir():
            # for f in sorted(p.iterdir(), key=lambda f: f.name, reverse=reverse):
            # print(f, type(f), f.name, type(f.name))
            if not all and f.name.startswith('.'):
                continue
            # 长格式 -rw-r--r-- 1 Administrator 197121 62 Feb 26 17:35 __main__.log
            #       type  mode  硬链接  owner  group  size  mtime  name
            if not detail:
                yield (f.name,)
            else:
                st = f.stat()
                # print(st)
                size = _get_size(st.st_size) if human else st.st_size
                mtime = "{:%Y-%m-%d %H:%M:%S}".format(datetime.datetime.fromtimestamp(st.st_mtime))
                yield filemode(st.st_mode), st.st_nlink, st.st_uid, st.st_gid, size, mtime, f.name
    else:
        pass

def list_detail(path: str, all=False, detail=False, reverse=False, human=False):
    return sorted(_listdir(path, all, detail, reverse, human), key=lambda x: x[-1], reverse=reverse)

if __name__ == '__main__':
    args = parser.parse_args(['-lar', '-h'])  # Namespace(path='.', longfmt=True, all=True)
    print(args)  # 返回Namespace类型实例,实例有属性,属性是参数

    for x in list_detail(args.path, args.all, args.longfmt, args.reverse, args.human):
        print(x)
        # break

#返回结果
# usage: ls [-l] [-a] [-r] [-h] [--help HELP] [path]
#
# List information about the FILEs (the current directory by default)
#
# positional arguments:
#   path                  Path help
#
# options:
#   -l                    Long list format
#   -a, --all             Do not ignore entries starting with .
#   -r, --reverse         reverse order while sorting
#   -h, --human-readable
#   --help HELP
# Namespace(path='.', longfmt=True, all=True, reverse=True, human=True, help=None)
# ('-rw-rw-rw-', 1, 0, 0, '3K', '2025-02-28 00:59:15', 'test.py')
# ('-rw-rw-rw-', 1, 0, 0, '1K', '2025-01-14 21:51:12', 't2.py')
# ('-rw-rw-rw-', 1, 0, 0, '250', '2025-01-07 00:44:02', 't.ini')
# ('-rw-rw-rw-', 1, 0, 0, '17K', '2025-01-08 20:39:39', 'sample.txt')
# ('-rw-rw-rw-', 1, 0, 0, '191', '2025-01-06 23:37:23', 'mysql.ini')
# ('drwxrwxrwx', 1, 0, 0, '0', '2025-02-27 00:16:42', 'm')

处理uid, gid

import argparse
from pathlib import Path
from stat import filemode
import datetime

parser = argparse.ArgumentParser(
    prog='ls',
    description='List information about the FILEs (the current directory by default)',
    add_help=False
)
parser.add_argument('path', nargs='?', default='.', help='Path help')  # 位置参数,可有可无,缺省值,帮助
#? 没有则用.  * 可以任意个,如果收集了就是列表
parser.add_argument('-l', action='store_true', dest='longfmt', help='Long list format')  # l属性名
parser.add_argument('-a', '--all', action='store_true', help='Do not ignore entries starting with .')

#-r reverse 按文件名称排序
parser.add_argument('-r', '--reverse', action='store_true', help='reverse order while sorting')
parser.add_argument('-h', '--human-readable', dest='human', action='store_true')
parser.add_argument('--help')
parser.print_help()  # 打印帮助

# args = parser.parse_args(('o:/abc', '-l')) #使用解析器,解析参数,真正的解析 python t.py o:/abc -l
# args = parser.parse_args('-l o:/tmp'.split()) # python t.py -l o:/tmp
# args = parser.parse_args() # python t.py .
# args = parser.parse_args(['-l', '-a'])  # Namespace(path='.', l=True, all=True)

def _get_size(size:int): #多少字节
    units = ' KM'
    depth = 0
    while size >= 1000 and depth < len(units):
        size //= 1000
        depth += 1
    return "{}{}".format(size, units[depth] if depth else '')

def _listdir(path, all, detail, reverse, human):
    p = Path(path)
    if p.is_dir():  # 只允许列出目录 #怎么知道它是隐藏文件?
        for f in p.absolute().iterdir():
            # for f in sorted(p.iterdir(), key=lambda f: f.name, reverse=reverse):
            # print(f, type(f), f.name, type(f.name))
            if not all and f.name.startswith('.'):
                continue
            # 长格式 -rw-r--r-- 1 Administrator 197121 62 Feb 26 17:35 __main__.log
            #       type  mode  硬链接  owner  group  size  mtime  name
            if not detail:
                yield (f.name,)
            else:
                st = f.stat()
                # print(st)
                size = _get_size(st.st_size) if human else st.st_size
                mtime = "{:%Y-%m-%d %H:%M:%S}".format(datetime.datetime.fromtimestamp(st.st_mtime))
                yield filemode(st.st_mode), st.st_nlink, st.st_uid, st.st_gid, size, mtime, f.name
                yield filemode(st.st_mode), st.st_nlink, f.owner(), f.group(), size, mtime, f.name #linux
    else:
        pass

def list_detail(path: str, all=False, detail=False, reverse=False, human=False):
    return sorted(_listdir(path, all, detail, reverse, human), key=lambda x: x[-1], reverse=reverse)

if __name__ == '__main__':
    args = parser.parse_args(['-lar', '-h'])  # Namespace(path='.', longfmt=True, all=True)
    print(args)  # 返回Namespace类型实例,实例有属性,属性是参数

    for x in list_detail(args.path, args.all, args.longfmt, args.reverse, args.human):
        print(x)
        # break

#返回结果
# usage: ls [-l] [-a] [-r] [-h] [--help HELP] [path]
#
# List information about the FILEs (the current directory by default)
#
# positional arguments:
#   path                  Path help
#
# options:
#   -l                    Long list format
#   -a, --all             Do not ignore entries starting with .
#   -r, --reverse         reverse order while sorting
#   -h, --human-readable
#   --help HELP
# Namespace(path='.', longfmt=True, all=True, reverse=True, human=True, help=None)
# ('-rw-rw-rw-', 1, 0, 0, '3K', '2025-02-28 00:59:15', 'test.py')
# ('-rw-rw-rw-', 1, 0, 0, '1K', '2025-01-14 21:51:12', 't2.py')
# ('-rw-rw-rw-', 1, 0, 0, '250', '2025-01-07 00:44:02', 't.ini')
# ('-rw-rw-rw-', 1, 0, 0, '17K', '2025-01-08 20:39:39', 'sample.txt')
# ('-rw-rw-rw-', 1, 0, 0, '191', '2025-01-06 23:37:23', 'mysql.ini')
# ('drwxrwxrwx', 1, 0, 0, '0', '2025-02-27 00:16:42', 'm')

PEP

PEP 是每一个 Python 程序员必须知道的。

https://peps.python.org/

PEP 是 Python Enhancement Proposals 的缩写(Python 增强提案)。一个PEP是一份为 Python 社区提供各种 增强功能的技术规格,也是提交新特性,以便让社区指出问题,精确化技术文档的提案。

每个 Python 版本的新特性或变化都是将 PEP 提案通过社区决策层讨论、投票决议,通过后才有我们看到的 功能变化。

Guido van Possum 被戏称为“仁慈的独裁者“(benevolent dictator for life, BDFL)。他会亲自签署并确认每一份 PEP。 但在 2018 年 7 月,由于 PEP 572(Assignment Expressions)这个提案,他已经卸任 BDFL。没有继任者,没有管理 原则,PEP 路在何方?

  • PEP 0:文档索引
  • PEP 1:协议指南
  • PEP 8:编码风格
  • PEP 20:Python 之禅
import this

Beautiful is better than ugly.
# 优美胜于丑陋(Python以编写优美的代码为目标)

Explicit is better than implicit.
# 明确胜于隐晦(优美的代码应当是明了的,命名规范,风格相似)

Simple is better than complex.
# 简单胜于复杂(优美的代码应当是简洁的,不要有复杂的内部实现)

Complex is better than complicated.
# 复杂胜于凌乱(如果复杂不可避免,那代码间也不能有难懂的关系,要保持接口简洁)

Flat is better than nested.
# 扁平胜于嵌套(优美的代码应当是扁平的,不能有太多的嵌套)

Sparse is better than dense.
# 留白胜于紧凑(优美的代码有适当的空白,不要奢望一行代码解决问题)

Readability counts.
# 可读性很重要(优美的代码是可读的)

Special cases aren't special enough to break the rules.
Although practicality beats purity.
# 即便假借特例的实用性之名,也不可违背这些规则(这些规则至高无上)

Errors should never pass silently.
Unless explicitly silenced.
# 不要包容所有错误,除非你确定需要这样做(精准地捕获异常,不写Exception:pass风格代码)

In the face of ambiguity, refuse the temptation to guess.
# 面对多种可能,不要靠猜
There should be one-- and preferably only one --obvious way to do it.
# 而是尽量找一种,最好是唯一一种明显的解决方案(如果不确定,就用穷举法)

Although that way may not be obvious at first unless you're Dutch.
# 虽然这并不容易,因为你不是 Python 之父(这里的Dutch是指Python之父Guido Van Rossum,他是荷兰人。)

Now is better than never.
Although never is often better than *right* now.
# 现在行动胜过永不开始,但是不假思索就动手还不如不做(动手之前要细思量)

If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
# 如果你的实现很难向别人解释,这往往不是一个好的方案;如果你的实现很容易向别人解释,这可能是个好的方案。(方案测评标准)

Namespaces are one honking great idea -- let's do more of those!
# 命名空间是一种绝妙的理念,我们应当多加利用(倡导与号召)

PEP8016

Python社区治理方案,这是Python之父卸任BDFL之后,社区从几种方案中投票胜出的方案。

PEP 8016 治理方案采用指导委员会模式,其特点是引导治理的迭代,该方案由 Nathaniel J. Smith 和 Donald Stufft 提出。

PEP 8016 中提出了不信任投票,也就是弹劾机制,可将任期内的当权者赶下台;它严格限定了在委员会里,只允 许少于 50% 的成员是企业(5 人委员会里最多有 2 个);并且关注到核心开发者的选举/淘汰、如何更新治理提案等问题。

而且,PEP 8016 中也提出了新的 PEP 流程,目前的 PEP 流程是提案人确定 PEP 的选题方向,提案人负责收集与 整合来自整个社区的反馈。然后,相关领域的专家们汇总全部讨论,并开启为期 14 天的审查,之后进行社区投 票。如果一个 PEP 很有争议,任何专家成员都可发起动议来拒绝通过它,这需要超过 2/3 的票数。

PEP 8016 的 PEP 流程:指导委员会在必要时可直接地批准/否决 PEP,但最好是设置流程来避免这样做决策,例如,将决策权委派给团队或者 BDFL 代表。

2019 年 2 月 4 日,为期2周的投票后,Python 社区选出来了指导委员会的5名成员。

5人分别是:

  • Barry Warsaw:自 1995 年起成为核心开发者之一,荣获 2014 年的弗兰克·威利森纪念奖。业余爱好是音乐和太极。
  • Brett Cannon:自2003年起成为核心开发者之一,荣获 2016 年的弗兰克·威利森纪念奖。曾担任 Python 软 件基金会的执行副主席。目前供职于微软,负责 VSCode 的 Python 插件项目。
  • Carol Willing:Python 核心开发者,Jupyter 核心开发者及 Jupyter 的指导委员会成员。自由职业,兴趣在于 科研及教育项目。
  • Guido van Rossum:Python 的创始人,被称为“Python 之父”,长期领导 Python 社区的发展,直到此次的 退位风波。
  • Nick Coghlan:自2005年起成为核心开发者之一。

Python 面向对象高级编程

主要内容:

  • 命名空间与模块与包管理
  • init.py 文件
  • Python 包格式、管理工具和程序打包分发
  • Python 动态模块加载技术
  • 运算符重载
  • 实例化方法本质
  • 上下文管理
  • 可调用对象
  • 可视化方法

魔术方法

查看属性__dir__()

dir(obj)对于不同类型的对象obj具有不同的行为:

  • 如果对象是模块对象,返回的列表包含模块的属性名和变量名
  • 如果对象是类型或者说是类对象,返回的列表包含类的属性名,及它的祖先类的属性名
  • 如果是类的实例
    • 有 __dir__方法,返回可迭代对象的返回值
    • 没有__dir__方法,则尽可能收集实例的属性名、类的属性和祖先类的属性名
  • 如果obj不写,返回列表包含内容不同
    • 在模块中,返回模块的属性和变量名
    • 在函数中,返回本地作用域的变量名
    • 在方法中,返回本地作用域的变量名
方法 意义
__dir__ 返回类或者对象的所有成员名称列表
  dir()函数操作实例就是调用 __dir__()

如果dir[obj]参数obj包含方法__dir__(),该方法将被调用。如果参数obj不包含__dir__(),该方法将最 大限度地收集属性信息。

范例: 如果对象是模块对象,返回的列表包含模块的属性名和变量名

#t1.py
print('~~~~~~~~~~~~~~~')

#直接在t1.py上运行,当前模块名会被替换为__main__
#dir() 跟所处位置有关,返回属性名称的列表,表示这些名称都是模块的变量,都模块的属性
print("moudule'name = {}, {}".format(__name__, dir()))

class Animal:
    def __init__(self, name):
        self._name = name
        self.__age = 20
        self.weight = 80
print("moudule'name = {}, {}".format(__name__, dir()))

#返回结果
# ~~~~~~~~~~~~~~~
# moudule'name = __main__, ['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__']
# moudule'name = __main__, ['Animal', '__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__']
print('-' * 30)
import t1 #t1模块名,加载模块,执行模块,t1是全局变量
from t1 import Animal #导入模块,同一模块只能导入一次不够了
print('=' * 30)

class Cat(t1.Animal): #通过from t1 import Animal, 可写成class Cat(Animal)
    x = 'abc'
    y = 'cat class'
print("moudule'name = {}, {}".format(t1.__name__, dir(t1))) #对象是模块对象,返回的列表包含模块的属性名和变量名
print("moudule'name = {}, {}".format(__name__, dir()))
print('*' * 30)

print(Cat, Cat.mro())
print(dir(Cat)) #对象是类型或者说是类对象,返回的列表包含类的属性名,及它的祖先类的属性名
tom = Cat('tom')
print(tom)
print(dir(tom))
print(sorted(tom.__dict__.keys() | Cat.__dict__.keys()
             | Animal.__dict__.keys()
             | object.__dict__.keys()
             ))

#返回结果
# ------------------------------
# ~~~~~~~~~~~~~~~
# moudule'name = t1, ['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__']
# moudule'name = t1, ['Animal', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__']
# ==============================
# moudule'name = t1, ['Animal', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__']
# moudule'name = __main__, ['Animal', 'Cat', '__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 't1']
# ******************************
# <class '__main__.Cat'> [<class '__main__.Cat'>, <class 't1.Animal'>, <class 'object'>]
# ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__firstlineno__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__static_attributes__', '__str__', '__subclasshook__', '__weakref__', 'x', 'y']
# <__main__.Cat object at 0x00000260F31F7CB0>
# ['_Animal__age', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__firstlineno__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__static_attributes__', '__str__', '__subclasshook__', '__weakref__', '_name', 'weight', 'x', 'y']
# ['_Animal__age', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__firstlineno__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__static_attributes__', '__str__', '__subclasshook__', '__weakref__', '_name', 'weight', 'x', 'y']

print('&&&&&&&&&&&&&&&&&')
class Dog(Animal):
    def __dir__(self): #这个魔术方法self,和当前实例有关
        return [123] #返回一个可迭代对象,里面应该是字符串

dog = Dog('snoopy')
print(dog)
print(dir(dog))
print(dir(Dog))
#返回结果
# &&&&&&&&&&&&&&&&&
# <__main__.Dog object at 0x000002685DC27E00>
# [123]
# ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__firstlineno__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__static_attributes__', '__str__', '__subclasshook__', '__weakref__']
class A:
    def show(self, a, b=1):
        print(self, a, b)
        c = 200
        print(dir()) #表现什么,函数中,收集 局部变量
        print(locals()) #locals() 内建函数,本地的,局部变量的字典
        print(globals()) #内建函数,全局的,当前模块的所有属性字典

A().show(100)

def b(x=123, y='abc'):
    z = [1,1,1]
    print(dir())  # 表现什么,函数中,收集 局部变量
    print(locals())  # locals() 内建函数,本地的,局部变量的字典
    print(globals())  # 内建函数,全局的,当前模块的所有属性字典
print('*' * 30)
b()

print('=' * 30)
print(dir())
print(sorted(locals().keys()))
print(sorted(locals().keys()))

#返回结果
# <__main__.A object at 0x0000028A31727E00> 100 1
# ['a', 'b', 'c', 'self']
# {'self': <__main__.A object at 0x0000028A31727E00>, 'a': 100, 'b': 1, 'c': 200}
# {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x0000028A318D44B0>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'D:\\project\\pyproj\\t3.py', '__cached__': None, 'A': <class '__main__.A'>}
# ******************************
# ['x', 'y', 'z']
# {'x': 123, 'y': 'abc', 'z': [1, 1, 1]}
# {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x0000028A318D44B0>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'D:\\project\\pyproj\\t3.py', '__cached__': None, 'A': <class '__main__.A'>, 'b': <function b at 0x0000028A318BF240>}
# ==============================
# ['A', '__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'b']
# ['A', '__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'b']
# ['A', '__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'b']

内建函数:

  • locals() 返回当前作用域中的变量字典
  • globals() 返回当前模块全局变量的字典

魔术方法分类

分类

  • 创建、初始化与销毁
    • __new__
    • __init__与__del__
  • 可视化
  • hash
  • bool
  • 运算符重载
  • 容器和大小
  • 可调用对象
  • 上下文管理
  • 反射
  • 描述器
  • 其他杂项

实例化 __new__,__init__,__del__

  • 创建、初始化与销毁
    • __new__
    • __init__与__del__
这3个方法分别是用来创建、初始化和销毁的。销毁是析构方法。
对于一个实例的创建过程来讲,实例上是分2步,第1步通过 __new__ 方法得一个实例,第2步__init__方法,它是当我们
创建一个实例后还不能直接给用户使用还需要初始化属性,这时候我们会写self.xx等于xx。相当于做出厂配置,因为
python是动态语言,动态地添加属性。__init__方法不能有返回值。最后我们就拿到了一个实例,这个实例是在内存
中创建出来的。

实例消亡跟引用计数相关,一个对象引用计数为0时,python会在适当时候垃圾回收。垃圾回收在调用它之前会
调用__del__方法

__new__

  • 实例化一个对象
  • 该方法需要返回一个值,如果该值不是cls的实例,则不会调用__init__
  • 该方法永远都是静态方法

__new__方法很少使用,即使创建了该方法,也会使用return super().__new__(cls) 基类object的__new__方法来创建实例并返回

范例:

class A:
    def __new__(cls, *args, **kwargs):
        print('A new ~~~~~~~~~~~~')
        print(cls) #cls 为当前类
        print(args)
        args =('jerry',)
        print(kwargs)
        kwargs['age'] = 30
        # return object.__new__(cls) #使用cls A类对象,模板,构建一个实例
        return super().__new__(cls)
        # return None

    def __init__(self, name, age):
        print('A init _________')
        self.name = name
        self.age = age

t = A('tom', age=20)
print(t)
print(t.name, t.age)

#返回结果
# A new ~~~~~~~~~~~~
# <class '__main__.A'>
# ('tom',)
# {'age': 20}
# A init _________
# <__main__.A object at 0x0000022D37C97E00>
# tom 20

可视化 __str__,__repr__,__bytes__

__str__

  • str()函数、format()函数、print()函数调用,需要返回对象的字符串表达。如果没有定义,就去调用__repr__方法返回字符串表达,如果__repr__没有定义,直接返回对象的内存地址信息

__repr__

  • 内建函数repr()对一个对象获取字符串表达
  • 调用__repr__方法返回字符串表达,如果__repr__也没有定义,就直接返回object的定义,就是显示内存地址信息

__bytes__

  • bytes()函数调用,返回一个对象的bytes表达,即返回bytes对象

范例:

class A:
    def __init__(self, name, age=19):
        self.name = name
        self.age = age

    def __repr__(self):
        return 'repr: {},{}'.format(self.name, self.age)

    def __str__(self):
        return 'str: {},{}'.format(self.name, self.age)

    def __bytes__(self):
        # return "{} is {}".format(self.name, self.age).encode()
        # return str(self).encode()
        import json
        return json.dumps(self.__dict__).encode()

print(A('tom')) #print作用于这个实例 # str print format 这个三个函数优先使用 __str__,如果没有则使用__repr__,如没有则调用object
print('{}'.format(A('tom')))
print(str(A('tom'))) #str使用于这个实例上
print('=' * 30)
print([A('tom')]) #[]使用__str__,但其内部使用__repr__
print([str(A('tom'))]) #[]使用__str__,其中元素使用str()函数也调用__str__
print(bytes(A('tom')))

# 执行结果
# str: tom,19
# str: tom,19
# str: tom,19
# ==============================
# [repr: tom,19]
# ['str: tom,19']
# b'{"name": "tom", "age": 19}'

注意不能通过判断是否带引号来判断输出值的类型,类型判断要使用type或者isinstance

hash __hash__

__hash__
内建函数hash()调用的返回值,返回一个整数,如果定义个方法该类的实例就可hash
print(hash(1))
print(hash('tom'))
print(hash(('tom',)))

上例中,A的实例放在set中,他们hash值是相同的,并没有去重

class A:
    def __init__(self, name, age=18):
        self.name = name
    def __repr__(self):
        return self.name

    def __hash__(self):
        return 1

    def __eq__(self, other):
        print("id(self)", id(self))
        print("id(other)", id(other))
        return self.name == other.name

a1 = A('tom')
a2 = A('tom')
s = {a1, a2}  #hash地址相同,hash值相同,hash冲突;还要看内容是否相同
                  #等价于 a1.__eq__(a2) True
print(s) #??? {tom at 2237306535424}

print('=' * 30)
print(a1 == a2) #等价于 a1.__eq__(a2) True

#set去重原理,第一步必须求hash,hash冲突才需要去重。第二步去重 比较内容

#返回结果
# id(self) 2237306535424
# id(other) 2237308499152
# {tom at 2237306535424}
# ==============================
# id(self) 2237306535424
# id(other) 2237308499152
# True
class A:
    def __init__(self, name, age=18):
        self.name = name
    def __repr__(self):
        return "{} at {}".format(self.name, id(self))

    def __hash__(self):
        return 1

    def __eq__(self, other):
        print("id(self)", id(self))
        print("id(other)", id(other))
        return self.name == other.name

a1 = A('tom')
a2 = A('tom')
s = {a1, a2}  #hash地址相同,hash值相同,hash冲突;还要看内容是否相同
              #等价于 a1.__eq__(a2) True
print(s) #??? {tom at 2306495249920}

print('=' * 30)
print(a1 == a2) #等价于 a1.__eq__(a2) True

#set去重原理,第一步必须求hash,hash冲突才需要去重。第二步去重 比较内容

print('~' * 30)
jerry = A('jerry')
s = {a1, a2, jerry}
print(s)

#返回结果
# id(self) 2306495249920
# id(other) 2306496492432
# {tom at 2306495249920}
# ==============================
# id(self) 2306495249920
# id(other) 2306496492432
# True
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# id(self) 2306495249920
# id(other) 2306496492432
# id(self) 2306495249920
# id(other) 2306496492752
# {tom at 2306495249920, jerry at 2306496492752}
__eq__
对应==操作符,判断2个对象是否相等,返回bool值
定义了这个方法,如果不提供__hash__方法,那么实例将不可hash

__hash__ 方法只是返回一个 hash 值作为 set 的 key,但是去重,还需要 __eq__ 来判断 2 个对象是否相等

hash 值相等,只是 hash 冲突,并不能说明两个对象是相等的

因此,一般来说提供 __hash__ 方法是为了作为set或者dict的key,如果去重要同时提供__eq__方法

去重需要提供__eq__方法

思考:

  • list类实例为什么不可hash?
    • 源码中有一句__hash__ = None,也就是如果调用__hash__()相当于None(),一定报错
    • 所有类都继承object,而这个类是具有__hash__()方法的,如果一个类不能被hash,就把__hash__设置为None
  • functolls.lru_chche使用到的functools.HashedSwq类继承自list,为什么可hash?
    • _HashedSeq类提供了__hash__方法,这个方法实际上计算的是元组的hahs值

练习

  • 设计二维坐标类Point,使其成为可hash类型,并比较2个坐标的实例是否相等
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __hash__(self):
        return hash((self.x,self.y))

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

    def __repr__(self):
        return "<Point ({}:{})> id:{}".format(self.x, self.y, hex(id(self)))

p1 = Point(4, 5)
p2 = Point(4, 5)
print(hash(p1)) # -1009709641759730766
print(hash(p2)) # -1009709641759730766

print(p1 is p2) # False
print(p1 == p2) # True
print(hex(id(p1)),hex(id(p2))) # 0x1ad611b7cb0 0x1ad61427610
print(set((p1, p2))) #{<Point (4:5)> id:0x1ad611b7cb0}

bool __bool__

__bool__
内建函数bool(),或者对象放在逻辑表达式的位置,调用这个函数返回布尔值
没有定义__bool__(),就找__len__()返回长度,非0为真
如果__len__()没有定义,那么所有实例都返回真
class A:pass

print(bool(A())) # True

if A():
    print('Real A') # Real A

class B:
    def __bool__(self):
        return False

print(bool(B)) # True
print(bool(B())) # False
if B():
    print('Real B')

class C:
    def __len__(self):
        return 0

print(bool(C())) # False
if C():
    print('Real C')

运算符重载

operator模块提供以下的特殊方法,可以将类的实例使用下面的操作符来操作

1.比较运算符
<,<=,==,>,>=,!=

__lt__,__le__,__eq__, __gt__,__ge__,__ne__


2.算数运算符,移位、位运算也有对应的方法

+, -, *, /, %, //, **, divmod  

__add__,__sub__,__mul__,__truediv__,__mod__,__floordiv__,__pow__,__divmod__


3.in-place 就地修改
+=, -=,*=,/=,%=,//=,**=

__iadd__,__isub__,__imul__, __itruediv__,__impd__,__ifloordiv__,__ipow__

范例:实现Person类的2个实例相减

class Person:
    def __init__(self, name, age=18):
        self.name = name
        self.age = age

    def __sub__(self, other): #双目
        return self.age - other.age

    def __isub__(self, other): #如果没有定义__isub__,则会调用__sub__
        return Person(self.name, self.age - other.age) #返回一个全新的实例

    def __repr__(self):
        return "<Person {} {}>".format(self.name, self.age)

tom = Person('tom', 20)
jerry = Person('jerry')

print(tom - jerry) # 会转化成 tom.__sub__(jerry) -> 2
print(tom, jerry) #<Person tom 20> <Person jerry 18>
print('-' * 30)

tom -= jerry #有__isub__方法,tom = tom.__isub__(jerry)
#如果没有__isub__方法,调用__sub__方法 tom = tom - jerry => tom = tom.__sub__(jerry)
# print(tom, type(tom)) #2 <class 'int'>
print(tom, type(tom)) #<Person tom 2> <class '__main__.Person'>
  • __isub__方法定义,一般会in-place就地来修改自身
  • 如果没有定义__isub__方法,则会调用__sub__

练习

完成Point类设计,实现判断点相等的方法,并完成向量的加法

在直角坐标系里,定义原点为向量的起点。两个向量和与差的坐标分别等于这两个向量相应坐标的和与差。

若向量的表示为(x,y)形式,
A(X1,Y1) B(X2,Y2) , 则A+B=(X1+X2, Y1+Y2), A-B= (X1-X2, Y1-Y2)
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

    def __add__(self, other):
        return Point(self.x + other.x, self.y + other.y)

    def __iadd__(self, other):
        self.x += other.x
        self.y += other.y
        return self

    def add(self,other):
        return (self.x + other.x, self.y + other.y)
        # return self + other

    def __repr__(self):
        return '<Point: {},{}> {}'.format(self.x, self.y, id(self))

p1 = Point(4,5)
p2 = Point(4,5)
print(p1, p2) #<Point: 4,5> 2663588724224 <Point: 4,5> 2663590688272

print(p1.add(p2)) #(8, 10)

# 使用运算符重载,更像是在写数学公式
#+ 返回一个全新的坐标对象
print(p1 + p2) #<Point: 8,10> 2663590688592

# += 就地修改
p1 += p2
print(p1, p2) #<Point: 8,10> 2663588724224 <Point: 4,5> 2663590688272
运算符重载应用场景

往往是用面向对象实现的类,需要做大量的运算,而运算符是这种运算在数学上 最常见的表达方式。例如,上例中的对+进行了运算符重载,实现了Point类的二 元操作,重新定义为Point + Point

提供运算符重载,比直接提供加法方法要更加合适该领域内使用者的习惯

int类,几乎实现了所有操作符,可以作为参考

@functools.total_ordering 装饰器

__lt__,__le__,__eq__,__gt__,__ge__ 是比较大小必须实现的方法,但是全部写完太麻烦,使用 @functools.total_ordering 装饰器就可以大小简化代码。

import functools

@functools.total_ordering
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __eq__(self, other): # eq 和 ne 必须提供一个等或者不等
        return self.age == other.age

    def __gt__(self, other): #> 大于知道了, < 小于会被推导出来
        return self.age > other.age

    def __ge__(self, other): #>= 大于等于知道了, =< 小于等于被自动推导出来
        return self.age >= other.age

tom = Person('tom', 20)
jack = Person('jack', 16)

print(tom == jack) # False #如果没有eq,会用object类中提供的eq。
print(tom != jack) # True

print(tom > jack) # True
print(tom < jack) # False

print(tom >= jack) # True
print(tom <= jack) # False

上例中大小简化代码,但是一般来说比较实现等于或者小于方法也就够用了,其它可以不实现,所以这个 装饰器只是看着很美好,且可能会带来性能问题,建议需要什么方法就自己创建,少用装饰器。

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

    def __eq__(self, other): # eq 和 ne 必须提供一个等或者不等
        return self.age == other.age

    def __gt__(self, other):
        return self.age > other.age

    def __ge__(self, other): 
        return self.age >= other.age

tom = Person('tom', 20)
jack = Person('jack', 16)

print(tom == jack) # False #如果没有eq,会用object类中提供的eq。
print(tom != jack) # True

print(tom > jack) # True
print(tom < jack) # False

print(tom >= jack) # True
print(tom <= jack) # False
__eq__等于可以推断不等于
__gt__大于可以推断小于
__ge__大于等于可以推断小于等于
也就是用这3个方法,就可以把所有比较解决了,所以total_ordering可以不使用。

容器和大小 __len__,__iter__,__contains__,__getitem__,__setitem__,__missing__

__len__
内建函数len(),返回对象的长度(>=0的整数),如果把对象当做容器类型看,就如同list或者dict
bool()函数调用的时候,如果没有__bool()方法,则会看__len()__方法是否存在,存在返回非0为真

__iter__ 
迭代容器时,调用,返回一个新的迭代器对象

__contains__
in成员运算符,没有实现,就调用__iter__方法遍历

__getitem__
实现self[key]访问。序列对象,key接受整数位索引,或者切片
对于set和dict,key为hashable。key不存在引发KeyError异常

__setitem__
和__getitem__的访问类似,是设置值的方法

__missing__
字典或其子类使用__getitem__()调用时,key不存在执行该方法
class A(dict):
    def __missing__(self, key):
        print('Missing key :', key)
        return 0

a = A()
print(a['k'])

# 执行结果
Missing key : k
0
练习
购物车
  • 将购物车类改造成方便操作的容器类
class Cart:
    def __init__(self):
        self.items = []

    def __len__(self):
        return len(self.items)

    def additem(self, item):
        self.items.append(item)
        return self
        # return self + item 运用运算符的重载,会调用__add__方法

    def __iter__(self):
        # yield from self.items
        return iter(self.items)

    def __getitem__(self, index): # 索引访问
        return self.items[index]

    def __setitem__(self, key, value): # 索引赋值
        self.items[key] = value

    def __str__(self):
        return str(self.items)

    def __add__(self, other):
        self.items.append(other)
        return self

cart = Cart()
cart.additem(1)
cart.additem('abc')
cart.additem(3)

# 长度、bool
print(len(cart)) # 3
print(bool(cart)) # True

# 迭代
for x in cart:
    print(x) # 1 abc 3
# in
print(3 in cart) #True
print(2 in cart) #False

# 索引操作
print(cart[1]) #abc
cart[1] = 'xyz'
print(cart) # [1, 'xyz', 3]

# 链式编程实现加法
print(cart.additem(1).additem('abc').additem(3)) #[1, 'xyz', 3, 1, 'abc', 3]
print(cart + 4 + 5 + 6) # [1, 'xyz', 3, 1, 'abc', 3, 4, 5, 6]
print(cart.__add__(17).__add__(18)) # [1, 'xyz', 3, 1, 'abc', 3, 4, 5, 6, 17, 18]

可调用对象 __call__

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

def foo():
    print(foo.__module__, foo.__name__)

foo() # __main__ foo
# 等价于
foo.__call__() # __main__ foo
  • 函数即对象,对象foo加上(),就是调用此函数对象的__call__()方法
__call__    类中定义一个该方法,实例就可以像函数一样调用

可调用对象,定义一个类,并实例化得到其实例,将实例像函数一样调用

范例

class A:
    def __call__(self, *args, **kwargs):
        print(args, kwargs)

print(callable(A())) #A的实例是否是可调用对象 True
a = A()
a() #() {}
class B:
    pass
    # def __call__(self, *args, **kwargs):
    #     print(args, kwargs)

print(callable(B)) #类是否是可调用对象 True
print(B()) #<__main__.A object at 0x000001DF1FA87CB0>

有哪些是可调用对象?

  • 函数、方法,类,类的实例(如果实现了__call__的方法)
  • 以上3种都可以使用装饰器,混合起来就很难看懂了。
练习
斐波那契数列
  • 定义一个斐波那契数列的类,方便调用,计算第n项
  • 增加迭代的方法、返回容器长度、支持索引的方法
class Fib:
    def __init__(self):
        self.items = [0, 1, 1]

    def __call__(self, index):
        if index < 0:
            raise IndexError('Wrong Index')
        # if index < len(self.items):
        #     return self.items[index]

        for i in range(len(self.items), index+1):
            self.items.append(self.items[i-1] + self.items[i-2])
        return self.items[index]

    def __str__(self):
        return "{}".format(self.items)

    __repr__ = __str__

fib = Fib()
print(fib(5)) # 5
print(fib) #[0, 1, 1, 2, 3, 5]

在上例基础上,增加迭代的方法、返回容器长度、支持索引的方法

class Fib:
    def __init__(self):
        self.items = [0, 1, 1]

    def __call__(self, index):
        return self[index]

    def __iter__(self):
        return iter(self.items)

    def __len__(self):
        return len(self.items)

    def __getitem__(self, index):
        if index < 0:
            raise IndexError('Wrong Index')
        # if index < len(self.items):
        #     return self.items[index]

        for i in range(len(self), index+1):
            self.items.append(self.items[i-1] + self.items[i-2])
        return self.items[index]

    # __call__ = __getitem__

    def __str__(self):
        return str(self.items)

    __repr__ = __str__

fib = Fib()
print(fib(5), len(fib)) # 5 6
print(fib(10), len(fib)) # 55 11
for x in fib:
    print(x)

print(fib[5], fib[6]) # 5 8

可以看出使用类来实现斐波那契数列也是非常好的实现,还可以缓存数据,便于检索

上下文管理 __enter__,__exit__

文件IO操作可以对文件对象使用上下文管理,使用with..

with open('test') as f:
    pass

仿照上例写一个自己的类,实现上下文管理

class Pointpass

with Point() as p: # AttributeError: __exit__
    pass

提示属性错误,没有__exit__,看了需要这个属性 某些版本会显示没有__enter__

上下文管理对象

当一个对象同时实现了__enter()__和__exit__()方法,它就属于上下文管理的对象

__enter__   进入与此对象相关的上下文。如果存在该方法,with语法会把该方法的返回值作为绑定到as子句中指定的变量上
__exit__    退出与此对象相关的上下文
import time

class Point():
    def __init__(self):
        print('init =========')
        time.sleep(1)
        print('init over =====')

    def __enter__(self):
        print('enter ========')

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('exit =========')

with Point() as p: # p = Point().__enter__()
    print('in with =========')
    time.sleep(2)
    print('with over ========')

print('======end======')

# 执行结果
# init =========
# init over =====
# enter ========
# in with =========
# with over ========
# exit =========
# ======end======
  • 实例化对象的时候,并不会调用enter,进入with语句块调用__enter__方法,然后执行语句体,最后离开with语句块的时候,调用__exit__方法
  • with可以开启一个上下文运行环境,在执行前做一些准备工作,执行后做一些首位工作。
  • 注意,with并不开启一个新的作用域
上下文管理的安全性

异常对上下文的影响

虽然有异常弹出,但是enter和exit照样执行, 上下文管理是安全的

import time

class Point():
    def __init__(self):
        print('init =========')
        time.sleep(1)
        print('init over =====')

    def __enter__(self):
        print('enter ========')

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('exit =========')

with Point() as p:
    print('in with =========')
    raise Exception('error')
    time.sleep(2)
    print('with over ========')

print('======end======')

#输出结果
# init =========
# init over =====
# enter ========
# in with =========
# exit =========
# Traceback (most recent call last):
#   File "D:\project\pyproj\t1.py", line 17, in <module>
#     raise Exception('error')
# Exception: error

极端的例子

  • 调用sys.exit(),它会退出当前解释器
  • 打开python解释器,在里面输入 sys.exit(),窗口直接关闭,也就是说碰到这一句,Python运行环境直接退出
import time

class Point():
    def __init__(self):
        print('init =========')
        time.sleep(1)
        print('init over =====')

    def __enter__(self):
        print('enter ========')

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('exit =========')

with Point() as p:
    print('in with =========')
    import sys
    sys.exit(1)
    time.sleep(2)
    print('with over ========')

print('======end======')

#返回结果
# init =========
# init over =====
# enter ========
# in with =========
# exit =========
  • 从执行结果来看,依然执行了__exit__函数,哪怕是退出Python运行环境
  • 说明上下文管理很安全
with语句
class Point():
    def __init__(self):
        print('init')

    def __enter__(self):
        print('enter')

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('exit')

f = open('t3.py')
with f as p: #对文件对象调用__enter__()   p = open('t3.py')
    print(f)
    print(p)
    print(id(f), id(p)) #2811630081568 2811630081568
    print(1,f is p) # 打印什么 True
    print(2,f == p) # 打印什么 True

p = Point()
#f = p.__enter__()
with p as f:  #with操作会调用实例的__enter__方法, as子句后的标识符会得到 __enter__ 返回值
    print('in with-----')
    print(p) #<__main__.Point object at 0x000001E4AAD57CB0>
    print(f) #None
    print(3,p is f, '++++') #False ++++
    print(4,p == f) # __eq__ p.__eq__(f) -> object.__eq__(self, other) 转而调is方法
    print('with over')

print('======end======')

#返回结果
# <_io.TextIOWrapper name='t3.py' mode='r' encoding='cp936'>
# <_io.TextIOWrapper name='t3.py' mode='r' encoding='cp936'>
# 1779374446112 1779374446112
# 1 True
# 2 True
# init
# enter
# in with-----
# <__main__.Point object at 0x0000019E4ADA7CB0>
# None
# 3 False ++++
# 4 False
# with over
# exit
# ======end======

问题在于__enter__方法上,它将自己的返回值赋给f。修改上例

class Point():
    def __init__(self):
        print('init')

    def __enter__(self):
        print('enter')
        return self # 增加返回值

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('exit')

f = open('t3.py')
with f as p: #对文件对象调用__enter__()   p = open('t3.py')
    print(f)
    print(p)
    print(id(f), id(p)) #2811630081568 2811630081568
    print(1,f is p) # 打印什么 True
    print(2,f == p) # 打印什么 True

p = Point()
#f = p.__enter__()  f = p
with p as f:  #with操作会调用实例的__enter__方法, as子句后的标识符会得到 __enter__ 返回值
    print('in with-----')
    print(p) #<__main__.Point object at 0x000001E4AAD57CB0>
    print(f) #<__main__.Point object at 0x0000016C32497CB0>
    print(3,p is f, '++++') #True ++++
    print(4,p == f) # __eq__ p.__eq__(f) -> object.__eq__(self, other) 转而调is方法
    print('with over')

print('======end======')

#返回结果
# <_io.TextIOWrapper name='t3.py' mode='r' encoding='cp936'>
# <_io.TextIOWrapper name='t3.py' mode='r' encoding='cp936'>
# 1564213859872 1564213859872
# 1 True
# 2 True
# init
# enter
# in with-----
# <__main__.Point object at 0x0000016C32497CB0>
# <__main__.Point object at 0x0000016C32497CB0>
# 3 True ++++
# 4 True
# with over
# exit
# ======end======

with语句,会调用with后的对象的__enter__方法,如果有as,则将该方法的返回值赋给as子句的变量

上例,可以等价为f = p.__enter__()

方法的参数

__enter__方法 没有其他参数

__exit__方法有3个参数

__exit__(self, exc_type, exc_value, traceback)

这三个参数都与异常有关。

如果该上下文退出时没有异常,这3个参数都为None

如果有异常,参数意义如下:

  • exc_type,异常类型
  • exc_value,异常的值
  • traceback,异常的追踪信息

__exit__方法返回一个等效True的值,则压制异常;否则,继续抛出异常

class Point():
    def __init__(self):
        print('init')

    def __enter__(self):
        print('enter')
        return self # 增加返回值

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(1, exc_type)
        print(2, exc_val)
        print(3, exc_tb)
        print('exit')
        return 'abc' # eixt的返回值等效为True,将压制报错
        # return None # 0 # []  等效为False,无法拦截异常

p = Point()
with p as f:
    print('in with-----')
    raise Exception('Error')
    print('with over')

print('======end======')


#返回结果
# init
# enter
# in with-----
# 1 <class 'Exception'>
# 2 Error
# 3 <traceback object at 0x000001F47A7E7B40>
# exit
# ======end======
练习

为加法函数计时

  • 方法1、使用装饰器显示该函数的执行市场
  • 方法2、使用上下文管理方法来显示该函数的执行时长

装饰器实现

import time
import datetime
from functools import wraps

#装饰器实现
def timeit(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        delta = (datetime.datetime.now() - start).total_seconds()
        print("{} took {}s. ~~~~".format(fn.__name__, delta))
        return ret
    return wrapper

@timeit
def add(x, y):
    time.sleep(2)
    return x + y

print('-' * 30)
add(4, 5)

#返回结果
# ------------------------------
# add took 2.000634s. ~~~~

上下文实现

import time
import datetime
from functools import wraps

#装饰器实现
def timeit(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        delta = (datetime.datetime.now() - start).total_seconds()
        print("{} took {}s. ~~~~".format(fn.__name__, delta))
        return ret
    return wrapper

@timeit
def add(x, y):
    time.sleep(2)
    return x + y

print('-' * 30)
# add(4, 5)

#返回结果
# ------------------------------
# add took 2.000634s. ~~~~

#上下文管理实现为加法函数计时
class Timeit:
    def __init__(self, fn):
        self.fn = fn

    def __enter__(self): #开始计时
        self.start = datetime.datetime.now()
        return self.fn

    def __exit__(self, exc_type, exc_val, exc_tb): #计算差值
        delta = (datetime.datetime.now() - self.start).total_seconds()
        print("{} took {}s. ++++".format(self.fn.__name__, delta))

with Timeit(add) as f: # f = Timeit().__enter__()
    add(4, 5)
    f(4, 5)

# 返回结果
# ------------------------------
# add took 2.000524s. ~~~~
# add took 2.000139s. ~~~~
# add took 4.000771s. ++++

可调用对象实现

import time
import datetime
from functools import wraps

#装饰器实现
def timeit(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        delta = (datetime.datetime.now() - start).total_seconds()
        print("{} took {}s. ~~~~".format(fn.__name__, delta))
        return ret
    return wrapper

@timeit
def add(x, y):
    time.sleep(2)
    return x + y

print('-' * 30)
# add(4, 5)

#可调用对象实现为加法函数计时
class Timeit:
    def __init__(self, fn):
        self.__fn = fn

    def __enter__(self): #开始计时
        self.start = datetime.datetime.now()
        # return self.fn
        return self

    def __exit__(self, exc_type, exc_val, exc_tb): #计算差值
        delta = (datetime.datetime.now() - self.start).total_seconds()
        print("{} took {}s. ++++".format(self.__fn.__name__, delta))

    def __call__(self, *args, **kwargs):
        ret = self.__fn(*args, **kwargs)
        return ret

with Timeit(add) as f: # f = Timeit().__enter__()
    # add(4, 5)
    f(4, 5)

# 返回结果
# ------------------------------
# add took 2.000402s. ~~~~
# add took 2.000492s. ++++

类当装饰器使用

import time
import datetime

#类当装饰器使用
class Timeit:
    """Timeit class doc"""
    def __init__(self, fn):
        self.__fn = fn

@Timeit
def add(x, y): #等价式 add = Timeit(add)
    """add function doc"""
    time.sleep(2)
    return x + y

print('-' * 30)
print(add.__doc__) #Timeit class doc 文档不对,怎么改
print(add.__name__) #名字? 报错

解决文档字符串的问题

方法1-直接修改__doc__

class TimeIt:
  def __init__(self, fn=None):
    self.__fn = fn
    # 把函数对象的文档字符串赋给类
    self.__doc__ = fn.__foc__
    self.__name = self.__fn.__name__

方法2-使用functools.wraps函数

import time
import datetime
from functools import wraps

#类当装饰器使用
class Timeit:
    """Timeit class doc"""
    def __init__(self, fn):
        self.__fn = fn
        # 把函数对象的文档字符串赋给类
        # self.__doc__ = fn.__doc__
        # self.__name__ = fn.__name__
        # update_wrapper(self, fn)
        wraps(fn)(self)

    def __enter__(self):
        self.start = datetime.datetime.now()
        return self.__fn

    def __exit__(self, exc_type, exc_val, exc_tb):
        delta = (datetime.datetime.now() - self.start).total_seconds()
        print("{} took {}s".format(self.__fn.__name__, delta))

    def __call__(self, *args, **kwargs):
        self.start = datetime.datetime.now()
        ret = self.__fn(*args, **kwargs)
        self.delta = (datetime.datetime.now() - self.start).total_seconds()
        print("Timeis call. {}'s result={}, delta={}".format(self.__fn.__name__, ret,self.delta))
        return ret

@Timeit
def add(x, y): #等价式 add = Timeit(add)
    """add function doc"""
    time.sleep(2)
    return x + y

print('-' * 30)
print(add.__doc__) #文档不对,怎么改 Timeit obj.__doc__
print(add.__name__) 
print(add(4,5))


# 返回结果
# ------------------------------
# add function doc
# add
# Timeis call. add's result=9, delta=2.000169
# 9

上面的类即可以用在上下文管理,又可以用作装饰器

上下文应用场景
  • 增强功能

    在代码执行的前后增加代码,以增强其功能。类似装饰器的功能

  • 资源管理

    打开了资源需要关闭,例如文件对象、网络连接、数据库连接等

  • 权限验证

    在执行代码之前,做权限的验证,在__enter__中处理

contextlib.contexmangager

它是一个装饰器实现上下文管理,装饰一个 函数 ,而不用像类一样实现__enter__和__exit__方法

  • 对下面的函数有要求,必须有yield,也就是这个函数必须返回一个生成器,且只有yield一个值
  • 这个装饰器接受一个生成器对象作为参数
from contextlib import contextmanager

@contextmanager
def foo():
    print('enter ~~~~') # 相当于__enter__()
    yield  # yield 5, yield的值只能有一个,作为__enter__方法的返回值
    #yield [1,2]
    print('exit ~~~~') # 相当于 __exit__()

print(foo()) #<contextlib._GeneratorContextManager object at 0x000001DED1617CB0>

#只有用with语法才能起作用
with foo() as f: #f = yield值
    print('with ~~~~')
    print(f)

#返回结果
# <contextlib._GeneratorContextManager object at 0x000001E2C04D7CB0>
# enter ~~~~
# with ~~~~
# None
# exit ~~~~

as后的f接受yield语句的返回值

  • 上面程序如果打开 raise Exception()语句,则print('exit')不会执行
  • 解决办法:增加try finally
from contextlib import contextmanager

@contextmanager
def foo():
    print('enter ~~~~') # 相当于__enter__()
    try:
        yield  # yield 5, yield的值只能有一个,作为__enter__方法的返回值
    finally:
        print('exit ~~~~') # 相当于 __exit__()

#只有用with语法才能起作用
with foo() as f:
    print('with start ~~~~')
    1/0
    # raise Exception()
    print('with end ~~~~')

#返回结果
# enter ~~~~
# with start ~~~~
# exit ~~~~
# Traceback (most recent call last):
#   File "D:\project\pyproj\t1.py", line 14, in <module>
#     1/0
#     ~^~
# ZeroDivisionError: division by zero

上例当yield发生处为生成器函数增加了上下文管理。这就是为函数增加上下文机制的方式

  • 把yield之前的当做__enter__方法执行
  • 把yield之后的当做__exit__方法执行
  • 把yield的值作为__enter__的返回值
import time
import datetime
from contextlib import contextmanager

@contextmanager
def timeit():
    print('enter ~~~~')
    start = datetime.datetime.now()
    try:
        yield  # yield 5, yield的值只能有一个,作为__enter__方法的返回值
    finally:
        print('exit ~~~~') # 相当于 __exit__()
        delta = (datetime.datetime.now() - start).total_seconds()
        print('timeit delta = {}s'.format(delta))

def add(x, y):
    """add function doc"""
    time.sleep(2)
    return x + y

with timeit() as f:
    print('with start ~~~~')
    add(4, 5)
    print('with end ~~~~')

#返回结果
# enter ~~~~
# with start ~~~~
# with end ~~~~
# exit ~~~~
# timeit delta = 2.000897s
import time
import datetime
from contextlib import contextmanager

@contextmanager
def timeit(fn):
    print('enter ~~~~')
    start = datetime.datetime.now()
    try:
        yield fn # yield 5, yield的值只能有一个,作为__enter__方法的返回值
    finally:
        print('exit ~~~~') # 相当于 __exit__()
        delta = (datetime.datetime.now() - start).total_seconds()
        print('timeit delta = {}s'.format(delta))

def add(x, y):
    """add function doc"""
    time.sleep(2)
    return x + y

with timeit(add) as f:
    print('with start ~~~~')
    # add(4, 5)
    f(4, 5)
    print('with end ~~~~')

#返回结果
# enter ~~~~
# with start ~~~~
# with end ~~~~
# exit ~~~~
# timeit delta = 2.00045s

总结

如果业务逻辑加单可以使用函数加contextlib.contextmanager装饰器方式,如果业务复杂,用类的方式加__enter__和__exit__方法方便

反射

概述

运行时,区别于编译时,指的是程序被加载到内存中执行的时候

反射,reflection,指的是运行时获取类型定义信息

一个对象能够在运行时,像照镜子一样,反射处其类型信息

简单说,在python中,能够通过一个对象,找出其type、class、attribute或method的能力,称为反射或者自省

具有反射能力的函数有type()、isinstance()、callable()、dir()、getattr()等

反射相关的函数和方法

需求: 有一个Point类,查看它实例的属性,并修改它。动态为实例增加属性

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return "Point({}, {})".format(self.x, self.y)

    def show(self):
        print(self.x, self.y)

p = Point(4, 5)
print(p) # Point(4, 5)
print(p.__dict__) # {'x': 4, 'y': 5}
p.__dict__['y'] = 16 # {'x': 4, 'y': 16}
print(p.__dict__) # {'x': 4, 'y': 16, 'z': 10}
p.z = 10
print(p.__dict__)
print(dir(p)) #返回排序过的p的属性列表 ordered list
print(sorted(p.__dir__())) #list

上例通过属性字典__dict__来访问对象的属性,本质上也是利用的反射的能力。

但是,上面的例子中,访问的访问不优雅,Python提供了内置的函数

内建函数

getattr(object,name[,default])  通过name返回object的属性值。当属性不存在,将使用default返回,如果没有default,则抛出AttributeError。name必须为字符串

setattr(object,name,value)  object的属性存在,则覆盖,不存在,新增

hasattr(object,name)  判断对象是否有这个名字的属性,name必须为字符串
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def show(self):
        return "<Point {},{}>".format(self.x, self.y)

    __str__ = show

p = Point(4, 5)
print(p.x, p.y, p.__dict__)
#可替换为
print(getattr(p, 'x'), getattr(p, 'y'), getattr(p, '__dict__'))

p.z = 10
print(p.z)
print(p.__dict__)
#可替换为
setattr(p, 'z', 10)
print(getattr(p, 'z'))
print(getattr(p, '__dict__'))


p.__dict__['y'] = 100
#可替换为
# getattr(p, '__dict__')['y'] = 100
setattr(p, 'y', 200)

#动态调用方法
if hasattr(p, 'show'):
    print(getattr(p, 'show')()) #<Point 4,200>

# 动态增加方法
# 为类增加方法
if not hasattr(Point, 'add'): #类属性, 动态增加类属性,使用.运算符或者getattr通过类的实例访问,都可以有绑定效果
    # Point.add = lambda self, other: Point(self.x + other.y, self.x + other.y)
    #可替换为
    setattr(Point, 'add', lambda self, other: Point(self.x + other.x, self.y + other.y))

p2 = Point(10, 10)
print(p.add(p2).show())  #<Point 14,210>

# 为实例增加方法,未绑定
if not hasattr(p, 'sub'): #通过实例属性调用该方法,没有绑定效果
    setattr(p, 'sub', lambda self, other: Point(self.x - other.x, self.y - other.y))
print(p.sub(p, p2))
print(p.sub) #<function <lambda> at 0x00000121BB3D8CC0>

#返回结果
# 4 5 {'x': 4, 'y': 5}
# 4 5 {'x': 4, 'y': 5}
# 10
# {'x': 4, 'y': 5, 'z': 10}
# 10
# {'x': 4, 'y': 5, 'z': 10}
# <Point 4,200>
# <Point 14,210>
# <Point -6,190>
# <function <lambda> at 0x00000121BB3D8CC0>

思考

  • 这种动态增加属性的方式和装饰器修饰一个类、Mixin方式的差异?

    这种动态增删属性的方式是运行时改变类或者实例的方式,但是装饰器或Mixin都是定义时就决定了,因此反射能力具有更大的灵活性

#伪代码
def decr(cls):
    cls.prop = 'abc'
    return cls
@decr
class A:  #定义时
    pass

class Minxin:
    pass #写混合功能
class B(Minxin, A): #定义时
    pass

while True:
    propname = input('>>>') #运行时
    value = input('>>>')
    typ = input('>>>') @'int' -> int
    setattr(B, propname, typ(value)) #运行时我们可以动态改变一个类。灵活

学完以上就可以阅读 from functools import update_wrapper 的源代码了。

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
                       '__annotations__', '__type_params__')
WRAPPER_UPDATES = ('__dict__',)
def update_wrapper(wrapper,
                   wrapped,
                   assigned = WRAPPER_ASSIGNMENTS,
                   updated = WRAPPER_UPDATES):
    """Update a wrapper function to look like the wrapped function

       wrapper is the function to be updated
       wrapped is the original function
       assigned is a tuple naming the attributes assigned directly
       from the wrapped function to the wrapper function (defaults to
       functools.WRAPPER_ASSIGNMENTS)
       updated is a tuple naming the attributes of the wrapper that
       are updated with the corresponding attribute from the wrapped
       function (defaults to functools.WRAPPER_UPDATES)
    """
    for attr in assigned:
        try:
            value = getattr(wrapped, attr)
        except AttributeError:
            pass
        else:
            setattr(wrapper, attr, value)
    for attr in updated:
        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
    # Issue #17482: set __wrapped__ last so we don't inadvertently copy it
    # from the wrapped function when updating __dict__
    wrapper.__wrapped__ = wrapped
    # Return the wrapper so this can be used as a decorator via partial()
    return wrapper
反射相关的魔术方法 __getattr__(),__setattr__(),__delatrr__()

__getattr__()、__setattrr__()、__delattr__()

__getattr__()
class Base:
    n = 77

class Point(Base):
    z = 66

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

    def show(self):
        print(self.x, self.y)

    def __getattr__(self, item):
        #有了这个方法,不是通过实例访问属性就调用的,
        #而是通过实例访问引发AttribueError,如果有此魔术方法,则调用
        print(type(item)) #属性名称字符串,反射
        return "missing ~~~~ {}".format(item)  # item什么类型? str

p1 = Point(4,5)
print(Point.n, Point.z) #77 66
print(p1.x) # 4
print(p1.z) # 66
print(p1.n) # 77
print(p1.y) # 5
# print(p1.item) #如果没有__getattr__,属性的搜索顺序中找不到,就报AttributeError
print(p1.item) #missing ~~~~ item
print(p1.xyz) #missing ~~~~ xyz

实例属性会按照继承关系找,如果找不到,就会执行__getattr__()方法,如果没有这个方法,就会抛出AttrbuteError异常表示找不到属性

查找属性的顺序:

  • instance.__dict__ –> instance.__class__.__dict__ –> 继承的祖先类(直到object)的__dict –> 找不到调用__getattr__() –> 其他补充
__setattrr__()
class Base:
    n = 77

class Point(Base):
    z = 66

    def __init__(self, x, y):
        print('init start ~~~~')
        self.x = x #对实例赋值
        self.y = y
        print('init end ~~~~')

    # def __getattr__(self, item): #通过实例找不到属性报AttributeError,则进入这个方法
    #     pass

    def __setattr__(self, key, value): #只要是实例的属性赋值就调用它
        print('setattr ~~~', key)
        # super().__setattr__(key, value)
        self.__dict__[key] = value


p1 = Point(4,5)
print(p1.__dict__) #如果__setattr__方法内容为 pass, 则实例没有属性了 {}
print(1, p1.x) # 4
print(2, p1.y) # 5
print(3, p1.z) # 66
print(4, p1.n) # 77
# print(5, p1.xyz) #找不到属性报AttributeError,进行入__getattr__方法

#返回结果
# init start ~~~~
# setattr ~~~ x
# setattr ~~~ y
# init end ~~~~
# {'x': 4, 'y': 5}
# 1 4
# 2 5
# 3 66
# 4 77

实例通过.点号设置属性,例如self.x = x属性赋值,就会调用__setattr__(),属性要加到实例的__dict__中,就需要自己完成

setattr__()方法,可以拦截对实例属性的增加、修改操作,如果要设置生效,需要自己操作实例的__dict

范例: __getattr__和__setattr__综合使用

将字典换掉

class Base:
    n = 77

class Point(Base):
    z = 66
    d = {}
    def __init__(self, x, y):
        print('init start ~~~~')
        self.x = x #对实例赋值
        # self.y = y
        setattr(self, 'y', y) #等效 self.y = y
        self.__dict__['a'] = 'aaa'
        print('init end ~~~~')

    def __getattr__(self, key): #通过实例找不到属性报AttributeError,则进入这个方法
        print('missing prop = {}'.format(key))
        return self.d[key]

    def __setattr__(self, key, value): #只要是实例的属性赋值就调用它
        print('setattr ~~~', key)
        # super().__setattr__(key, value)
        # self.__dict__[key] = value
        self.d[key] = value

p1 = Point(4,5)
print(1, p1.x) # 1 4
print(2, p1.y) # 2 5
print(3, p1.a) # 3 aaa
print(4, p1.n) # 4 77
print(p1.__dict__)
print(Point.__dict__)

#返回结果
# init start ~~~~
# setattr ~~~ x
# setattr ~~~ y
# init end ~~~~
# missing prop = x
# 1 4
# missing prop = y
# 2 5
# 3 aaa
# 4 77
# {'a': 'aaa'}
# {'__module__': '__main__', '__firstlineno__': 4, 'z': 66, 'd': {'x': 4, 'y': 5}, '__init__': <function Point.__init__ at 0x0000018C9E3C4680>, '__getattr__': <function Point.__getattr__ at 0x0000018C9E3C4D60>, '__setattr__': <function Point.__setattr__ at 0x0000018C9E5271A0>, '__static_attributes__': ('x',), '__doc__': None}
__delattr__()
class Base:
    n = 77

class Point(Base):
    z = 66
    d = {}
    def __init__(self, x, y):
        print('init start ~~~~')
        self.x = x #对实例赋值
        # self.y = y
        setattr(self, 'y', y) #等效 self.y = y
        self.__dict__['a'] = 'aaa'
        print('init end ~~~~')

    def __getattr__(self, key): #通过实例找不到属性报AttributeError,则进入这个方法
        print('missing prop = {}'.format(key))
        return self.d[key]

    def __setattr__(self, key, value): #只要是实例的属性赋值就调用它
        print('setattr ~~~', key)
        # super().__setattr__(key, value)
        # self.__dict__[key] = value
        self.d[key] = value

    def __delattr__(self, item): #通过实例删除属性,进入这个方法
        print('cannot del {}'.format(item))

p1 = Point(4,5)
print(Point.__dict__.keys())
# del Point.z
del p1.abc
del p1.x
del p1.z
print(Point.__dict__.keys())


#返回结果
# init start ~~~~
# setattr ~~~ x
# setattr ~~~ y
# init end ~~~~
# dict_keys(['__module__', '__firstlineno__', 'z', 'd', '__init__', '__getattr__', '__setattr__', '__delattr__', '__static_attributes__', '__doc__'])
# cannot del abc
# cannot del x
# cannot del z
# dict_keys(['__module__', '__firstlineno__', 'z', 'd', '__init__', '__getattr__', '__setattr__', '__delattr__', '__static_attributes__', '__doc__'])

可以阻止通过实例来删除属性的操作。但是通过类依然可以删除属性

__getattribute__()
class Base:
    n = 77

class Point(Base):
    z = 66
    def __init__(self, x, y):
        print('init start ~~~~')
        self.x = x #对实例赋值
        self.y = y
        print('init end ~~~~')

    def __getattr__(self, key): #通过实例找不到属性报AttributeError,则进入这个方法
        print('missing prop = {}'.format(key))

    def __setattr__(self, key, value):
        print('setattr ++++')
        super().__setattr__(key, value)

    def __getattribute__(self, item):
        #找属性都进入这个方法, 截胡了
        #这个魔术方法,一般不用
        #如果写了,一定是通过实例访问属性的第一站
        print('*** {} ***'.format(item))

p1 = Point(4,5)
print(1, p1.x)
print(2, p1.y)
print(3, p1.z)
print(Point.z, Point.n)
print(4, p1.__dict__) #4 None
print(object.__getattribute__(p1, '__dict__')) #{'x': 4, 'y': 5}



#返回结果
# init start ~~~~
# setattr ++++
# setattr ++++
# init end ~~~~
# *** x ***
# 1 None
# *** y ***
# 2 None
# *** z ***
# 3 None
# 66 77
# *** __dict__ ***
# 4 None
# {'x': 4, 'y': 5}

实例的所有的属性访问,第一个都会调用__getattribute__方法,它阻止了属性的查找,该方法应该返回(计算后的)值或者抛出一个AttributeError异常

  • 它的return值将作为属性查找的结果
  • 如果抛出AttributeError异常,则会直接调用__getattr__方法,表示属性没有找到
class Base:
    n = 77

class Point(Base):
    z = 66
    def __init__(self, x, y):
        print('init start ~~~~')
        self.x = x #对实例赋值
        self.y = y
        print('init end ~~~~')

    def __getattr__(self, key): #通过实例找不到属性报AttributeError,则进入这个方法
        print('missing prop = {}'.format(key))

    def __getattribute__(self, item):
        #找属性都进入这个方法, 截胡了
        #这个魔术方法,一般不用
        #如果写了,一定是通过实例访问属性的第一站
        print('*** {} ***'.format(item))
        # raise AttributeError('找不到了') #有__getattr__就调用,没有抛异常AttributeError
        # return self.__dict__[item] # 递归报错 RecursionError
        # return object.__getattribute__(self, item) #默认方式,属性搜索方式
        return super().__getattribute__(item)

p1 = Point(4,5)
print(1, p1.x)
# print(2, p1.y)
# print(3, p1.z)
# print(Point.z, Point.n)
# print(4, p1.__dict__) #4 None
# print(object.__getattribute__(p1, '__dict__')) #{'x': 4, 'y': 5}

#返回结果
# init start ~~~~
# init end ~~~~
# *** x ***
# 1 4

__getattribute__方法中为了避免在该方法中无限的递归,它的实现应该永远调用基类的同名方法打以访问需要的任何属性,例如object.__getattribute__(self, item)

除非明确地知道__getattribute__方法用来做什么,否则不要使用它

总结

魔术方法  意义
__getattr__() 当通过搜索实例、实例的类及祖先类查不到属性,就会调用此方法
__setattr__() 通过.点访问实例属性,进行增加、修改都要调用它
__delattr__() 当通过实例来删除属性时调用此方法
__getattribute__()  实例所有的属性调用都从这个方法开始


属性查找顺序:
实例调用__getattribute__() --> instance.__dict__ --> instance.__class__.__dict__ --> 继承的祖先类(直到object)的__dict__–>调用__getattr__()

描述器 __get__(),__set__(),__delete__()

描述器的表现

用到的魔术方法:__get__()、__set__()、__delete__()

方法签名如下

  • object.__get__(self, instance, owner)
  • object.__set__(self, instance, owner)
  • object.__delete__(self, instance)
  • self值当前实例,调用者
  • instance是owner的实例
  • owner是属性的所属的类
class A:
    def __init__(self):
        print('A init ~~~~')
        self.a1 = 'a1'

class B:
    x = A()
    def __init__(self):
        print('B init ~~~~')

print('-' * 30)
print(B.x)
print(B.x.a1)

#返回结果
# A init ~~~~
# ------------------------------
# <__main__.A object at 0x0000021BDA247CB0>
# a1

__get__

class A:
    def __init__(self):
        print('A init ~~~~')
        self.a1 = 'a1'

    def __get__(self, instance, owner):
        # return self
        pass

class B:
    x = A()
    def __init__(self):
        print('B init ~~~~')

print('~' * 30)
a = A()
print(a.a1)

print('-' * 30)
print(B.x) #A的实例, 相当于 B.x => A().__get__()
print(B.x.a1)

#返回结果
# A init ~~~~
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# A init ~~~~
# a1
# ------------------------------
# None
# Traceback (most recent call last):
#   File "D:\project\pyproj\t1.py", line 22, in <module>
#     print(B.x.a1)
#           ^^^^^^
# AttributeError: 'NoneType' object has no attribute 'a1'

因为定义了__get__方法,类A就是一个描述器,对类B或者类B的实例的x属性读取,成为对A的实例的访问,就会调用__get__方法

class A:
    def __init__(self):
        print('A init ~~~~')
        self.a1 = 'a1'

    def __get__(self, instance, owner):
        print('A.__get__ self:{} instance:{} owner:{}'.format(self, instance, owner))
        #owner 属主类, B
        return self

    def __repr__(self):
        return '<A instance {}>'.format(id(self))

class B: #属主类
    x = A() #A() 描述器实例
    def __init__(self):
        print('B init ~~~~')

print('-' * 30)
print(B.x) #A的实例, 相当于 B.x => A().__get__()
# print(B.x.a1)

#返回结果
# A init ~~~~
# ------------------------------
# A.__get__ self:<A instance 1494204710064> instance:None owner:<class '__main__.B'>
# <A instance 1494204710064>

self、instance、owner三个参数的意思

  • self 对应都是A的实例
  • owner 对应都是B类
  • instance 说明
    • None表示不是B类的实例

使用返回值解决。返回self,就是A的实例,该实例有a1属性,返回正常

class A:
    def __init__(self):
        print('A init ~~~~')
        self.a1 = 'a1'

    def __get__(self, instance, owner):
        print('A.__get__ self:{} instance:{} owner:{}'.format(self, instance, owner))
        #owner 属主类, B
        return self

    def __repr__(self):
        return '<A instance {}>'.format(id(self))

class B: #属主类
    x = A() #A() 描述器实例
    #访问B或者B实例属性变成一个函数调用
    def __init__(self):
        print('B init ~~~~')

    def __repr__(self):
        return '<B id={}>'.format(id(self))

print('-' * 30)
print(B.x) #A的实例, 相当于 B.x => A().__get__()
print(B.x.a1)
print('=' * 30)

t = B()
print(id(t))
print(t.x) #t.x -> B.x -> A().__get__()
print(t.x.a1)

#返回结果
# A init ~~~~
# ------------------------------
# A.__get__ self:<A instance 1886684019888> instance:None owner:<class '__main__.B'>
# <A instance 1886684019888>
# A.__get__ self:<A instance 1886684019888> instance:None owner:<class '__main__.B'>
# a1
# ==============================
# B init ~~~~
# 1886684020224
# A.__get__ self:<A instance 1886684019888> instance:<B id=1886684020224> owner:<class '__main__.B'>
# <A instance 1886684019888>
# A.__get__ self:<A instance 1886684019888> instance:<B id=1886684020224> owner:<class '__main__.B'>
# a1

那么类B的实例属性也可以这样吗?

class A:
    def __init__(self):
        print('A init ~~~~')
        self.a1 = 'a1'

    def __get__(self, instance, owner):
        print('A.__get__ self:{} instance:{} owner:{}'.format(self, instance, owner))
        #owner 属主类, B
        return self

    def __repr__(self):
        return '<A instance {}>'.format(id(self))

class B: #属主类
    # x = A() #A() 描述器实例
    def __init__(self):
        print('B init ~~~~')
        self.y = A() #描述器不能用在实例属性上

    def __repr__(self):
        return '<B id={}>'.format(id(self))

print('-' * 30)
t = B()
print(t.y)

#返回结果
# B init ~~~~
# A init ~~~~
# <A instance 2048168394240>

从运行结果可以看出,只要 类属性是类的实例 才行

描述器定义

Python中,一个类实现了__get__、__set__、__delete__三个方法中的任何一个方法,就是描述器。实现三个中的某些方法,就支持了描述器协议

  • 仅实现__get__,就是非数据描述符 non-data descriptor
  • 实现了__get__和剩下两个任意一种,就是数据描述符 data descriptor

如果一个类的类属性设置为描述器实例,那么它被称为owner属主。

当该类的该类属性被查找、设置、删除时,就会调用描述器相应的方法

属性的访问顺序

为上例中的类B增加实例属性x

class A:
    def __init__(self):
        print('A init ~~~~')
        self.a1 = 'a1'

    def __get__(self, instance, owner):
        print('A.__get__ self:{} instance:{} owner:{}'.format(self, instance, owner))
        #owner 属主类, B
        return self

    def __repr__(self):
        return '<A instance {}>'.format(id(self))

class B: #属主类
    x = A() #A() 描述器实例
    #访问B或者B实例属性变成一个函数调用

    def __init__(self):
        print('B init ~~~~')
        self.x = 'b.x' #非数据描述器
        # self.y = A() #描述器不能用在实例属性上

    def __repr__(self):
        return '<B id={}>'.format(id(self))

print('-' * 30)
print(B.x) #A的实例, 相当于 B.x => A().__get__()
print(B.x.a1)
print('=' * 30)

t = B()
print(id(t))
print(t.x) #?t.x -> B.x -> A().__get__()



#返回结果
# A init ~~~~
# ------------------------------
# A.__get__ self:<A instance 2566041795760> instance:None owner:<class '__main__.B'>
# <A instance 2566041795760>
# A.__get__ self:<A instance 2566041795760> instance:None owner:<class '__main__.B'>
# a1
# ==============================
# B init ~~~~
# 2566041796096
# b.x

类A只实现了__get__()方法,b.x访问到了实例的属性,而不是描述器

__set__ ,__set_name__

3.6新增描述器方法__set_name__,它在属主类构建的时候就会调用

为类A增加__set__方法

class A:
    def __init__(self):
        print('A init ~~~~')
        self.a1 = 'a1'

    def __get__(self, instance, owner):
        print('A.__get__ self:{} instance:{} owner:{}'.format(self, instance, owner))
        #owner 属主类, B
        return self
    def __set__(self, instance, value):
        print('A.set')
        # self.data = value
        self.__dict__[self.prop_name] = value

    def __set_name__(self, owner, name):
        print(self, owner, name, '****')
        self.prop_name = name

    def __repr__(self):
        return '<A instance {}>'.format(id(self))

class B: #属主类
    x = A() #A() 描述器实例
    #访问B或者B实例属性变成一个函数调用

    def __init__(self):
        print('B init ~~~~')
        self.x = 'b.x' #数据描述器
        #数据描述器,如果这个属性x是类属性
        #而类属性是描述器,那么实例属性赋值
        #就成了调用 A().__set__()
        #优先级
        #数据描述器优先级 > __dict__ > 非数据描述器
        # self.y = A() #描述器不能用在实例属性上
        print('B init end ++++')

    def __repr__(self):
        return '<B id={}>'.format(id(self))

print('-' * 30)
print(B.x) #A的实例, 相当于 B.x => A().__get__()
print(B.x.a1)
print('=' * 30)

t = B() #实例中类属性是描述器,那么实例属性赋值就是成了调用A().__set__()
print(id(t))
print(t.x) #?t.x -> B.x -> A().__get__()
print(t.__dict__)
print(t.x.__dict__)
#数据描述器,t.x是什么


#返回结果
# A init ~~~~
# <A instance 2204892888240> <class '__main__.B'> x ****
# ------------------------------
# A.__get__ self:<A instance 2204892888240> instance:None owner:<class '__main__.B'>
# <A instance 2204892888240>
# A.__get__ self:<A instance 2204892888240> instance:None owner:<class '__main__.B'>
# a1
# ==============================
# B init ~~~~
# A.set
# B init end ++++
# 2204892888576
# A.__get__ self:<A instance 2204892888240> instance:<B id=2204892888576> owner:<class '__main__.B'>
# <A instance 2204892888240>
# {}
# A.__get__ self:<A instance 2204892888240> instance:<B id=2204892888576> owner:<class '__main__.B'>
# {'a1': 'a1', 'prop_name': 'x', 'x': 'b.x'}

所有的b.x就会访问描述器的__get__()方法,代码中返回的self就是描述器实例,它的实例字典中就保存着a1和data属性,可以打印b.x.__dict__看这些属性

class A:
    def __init__(self):
        print('A init ~~~~')
        self.a1 = 'a1'

    def __get__(self, instance, owner):
        print('A.__get__ self:{} instance:{} owner:{}'.format(self, instance, owner))
        #owner 属主类, B
        return self
    def __set__(self, instance, value):
        print('A.set')
        self.data = value
        self.__dict__[self.prop_name] = value

    def __set_name__(self, owner, name):
        print(self, owner, name, '****')
        self.prop_name = name

    def __repr__(self):
        return '<A instance {}>'.format(id(self))

class B: #属主类
    x = A() #A() 描述器实例
    #访问B或者B实例属性变成一个函数调用

    def __init__(self):
        print('B init ~~~~')
        self.x = 'b.x' #数据描述器
        #数据描述器,如果这个属性x是类属性
        #而类属性是描述器,那么实例属性赋值
        #就成了调用 A().__set__()
        #优先级
        #数据描述器优先级 > __dict__ > 非数据描述器
        # self.y = A() #描述器不能用在实例属性上
        print('B init end ++++')

    def __repr__(self):
        return '<B id={}>'.format(id(self))

print('-' * 30)
t = B()
t.x = 500
print(t.x)
print(t.x.a1)
print(t.x.data)
print('=' * 30)

B.x = 600
print(t.x) #600
print(t.x.a1) #报错 AttributeError
#返回结果
# A init ~~~~
# <A instance 2436740906160> <class '__main__.B'> x ****
# ------------------------------
# B init ~~~~
# A.set
# B init end ++++
# A.set
# A.__get__ self:<A instance 2436740906160> instance:<B id=2436740906496> owner:<class '__main__.B'>
# <A instance 2436740906160>
# A.__get__ self:<A instance 2436740906160> instance:<B id=2436740906496> owner:<class '__main__.B'>
# a1
# A.__get__ self:<A instance 2436740906160> instance:<B id=2436740906496> owner:<class '__main__.B'>
# 500
# ==============================
# 600
# Traceback (most recent call last):
#   File "D:\project\pyproj\t1.py", line 51, in <module>
#     print(t.x.a1)
#           ^^^^^^
# AttributeError: 'int' object has no attribute 'a1'
属性的访问顺序
  • 实例的__dict__优先于非数据描述器
  • 数据描述器优先于实例的__dict__

__delete__方法有同样的效果,有了这个方法,也就是数据描述器

Python的描述器

描述器在Python中应用非常广泛

Python的方法(包括staticmethod()和classmethod())都实现为非数据描述器。 因此,实例可以重新定义和覆盖方法。这允许单个实例获取与同一类的其他实例 不同的行为

property()函数实现为一个数据描述器。因此,实例不能覆盖属性和行为

class A:
    @classmethod
    def foo(cls): # 非数据描述器
        pass

    @staticmethod
    def bar(): # 非数据描述器
        pass

    @property
    def z(self): # 数据描述器
        return 5

    def getfoo(self): # 非数据描述器
        return self.foo

    def __init__(self): # 非数据描述器
        self.foo = 100
        self.bar = 200
        # self.z = 300

a = A()
print(a.__dict__) # {'foo': 100, 'bar': 200}
print(A.__dict__)

foo、bar都可以在事例中覆盖,但是z不可以

练习
实现StaticMethod装饰器

实现StaticMethod装饰器,完成staticmethod装饰器的功能

class StaticMethod:
    def __init__(self, fn):
        self._fn = fn

    def __get__(self, instance, owner):
        # print('get ~~~~')
        return self._fn

class A:
    @StaticMethod
    # 相当于 foo = StaticMethod(foo) => foo就是StaticMethod的实例,
    # foo是A的类属性,符合描述器的定义,描述器对于属主类来讲必须有个类属主指向描述器实例
    def foo():
        print('static method~~')

# print(A.__dict__)
print(A.foo)
A.foo()
A().foo()

# 执行结果
# <function A.foo at 0x000001EC027CF1A0>
# static method~~
# static method~~
实现ClassMethod装饰器

实现ClassMethod装饰器,完成classmethod装饰器的功能

from functools import partial

class ClassMethod:
    def __init__(self, fn):
        self._fn = fn

    #非数据描述器  只要是方法都是描述器
    def __get__(self, instance, owner):
        ret = partial(self._fn, owner) #固定owner返回新函数,可以使用偏函数
        return ret

class A:
    @ClassMethod
    def bar(cls): #bar = ClassMethod(bar) => bar就是ClassMethod的实例,
        print('class method~~', cls)

print(A.__dict__)
print(A.bar)
A.bar()
A().bar()

# 执行结果
# {'__module__': '__main__', '__firstlineno__': 12, 'bar': <__main__.ClassMethod object at 0x000002C692A07CB0>, '__static_attributes__': (), '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
# functools.partial(<function A.bar at 0x000002C692CC8CC0>, <class '__main__.A'>)
# class method~~ <class '__main__.A'>
# class method~~ <class '__main__.A'>
对实例的数据进行校验
class Person:
    def __init__(self, name:str, age:int):
        self.name = name
        self.age = age

思路:

一般方法

class Person:
    def __init__(self, name:str, age:int):
        params = [(name, str), (age, int)]
        if not self.check_type(params):
            raise TypeError()
        self.name = name
        self.age = age

    def check_type(self, params): #硬编码,不好 2 侵入式 很多都需要
        for value, typ in params:
            if not isinstance(value, typ):
                return False
        return True

p1 = Person('tom', 20)
class Person:
    def __init__(self, name:str, age:int):
        if not self.check_type(name=(name, str), age=(age, int)): #硬编码,不好 2 侵入式 很多都需要
            raise TypeError()
        self.name = name
        self.age = age

    def check_type(self, **params):
        for value, typ in params.values():
            if not isinstance(value, typ):
                return False
        return True

p1 = Person('tom', 20)

对上面的类的实例的属性name、age进行数据校验

class TypeCheck:
    def __init__(self):
        pass
    def __get__(self, instance, owner):
        pass
    def __set__(self, instance, value):
        print('set ~~~~', instance, value)

class Person:
    name = TypeCheck()
    # age = TypeCheck()
    def __init__(self, name:str, age:int):
        self.name = name #赋值时,调用描述器的set方法
        self.age = age

p1 = Person('tom', 20)
print(p1.name, p1.age)

#返回结果
# set ~~~~ <__main__.Person object at 0x000001F418CF7E00> tom
# None 20
class TypeCheck:
    def __init__(self):
        self.pname = None

    def __get__(self, instance, owner):
        print('get ++++', self)
        return self.data

    def __set__(self, instance, value):
        print('set ~~~~', instance, value)
        self.data = value

    def __set_name__(self, owner, name):
        self.pname = name

    def __repr__(self):
        return "<描述器TypeCheck {} id={}>".format(self.pname, id(self))

class Person:
    name = TypeCheck() #对Person的每一个类属性构建一个描述器实例
    age = TypeCheck()
    def __init__(self, name:str, age:int):
        self.name = name #赋值时,调用描述器的set方法
        self.age = age

p1 = Person('tom', 20)
print(p1.name, p1.age)
print(p1.__dict__) #被描述器截胡了 {}

#返回结果
# set ~~~~ <__main__.Person object at 0x000002212E997E00> tom
# set ~~~~ <__main__.Person object at 0x000002212E997E00> 20
# get ++++ <描述器TypeCheck name id=2341538987184>
# get ++++ <描述器TypeCheck age id=2341540950928>
# tom 20
# {}

解决被截胡的字典__dict__

class TypeCheck:
    def __init__(self):
        self.pname = None

    def __get__(self, instance, owner):
        print('get ++++', self)
        return self.data

    def __set__(self, instance, value):
        print('set ~~~~', instance, value)
        self.data = value
        instance.__dict__[self.pname] = value  #修改被截胡的字典,方法1 利用__set_name__

    def __set_name__(self, owner, name):
        self.pname = name

    def __repr__(self):
        return "<描述器TypeCheck {} id={}>".format(self.pname, id(self))

class Person:
    name = TypeCheck() #对Person的每一个类属性构建一个描述器实例
    age = TypeCheck()
    def __init__(self, name:str, age:int):
        self.name = name #赋值时,调用描述器的set方法
        self.age = age

p1 = Person('tom', 20)
print(p1.name, p1.age)
print(p1.__dict__) #被描述器截胡了,需要做成正常一样的

#返回结果
# set ~~~~ <__main__.Person object at 0x000001C06D747E00> tom
# set ~~~~ <__main__.Person object at 0x000001C06D747E00> 20
# get ++++ <描述器TypeCheck name id=1925981699248>
# get ++++ <描述器TypeCheck age id=1925984056208>
# tom 20
# {'name': 'tom', 'age': 20}
class TypeCheck:
    def __init__(self, pname):
        self.pname = pname

    def __get__(self, instance, owner):
        print('get ++++', self)
        # return self.data
        if instance: #因为描述器也可以通过类来访问,这里做一下判断
            return instance.__dict__[self.pname]
        return self
        #raise TypeError

    def __set__(self, instance, value):
        print('set ~~~~', instance, value)
        # self.data = value
        if instance:
            instance.__dict__[self.pname] = value  #修改被截胡的字典,方法2 利用传参

    def __repr__(self):
        return "<描述器TypeCheck {} id={}>".format(self.pname, id(self))

class Person:
    name = TypeCheck('name') #对Person的每一个类属性构建一个描述器实例
    age = TypeCheck('age')
    def __init__(self, name:str, age:int):
        self.name = name #赋值时,调用描述器的set方法
        self.age = age

p1 = Person('tom', 20)
print(p1.name, p1.age)
print(p1.__dict__) #被描述器截胡了,需要做成正常一样的

#返回结果
# set ~~~~ <__main__.Person object at 0x000001C06D747E00> tom
# set ~~~~ <__main__.Person object at 0x000001C06D747E00> 20
# get ++++ <描述器TypeCheck name id=1925981699248>
# get ++++ <描述器TypeCheck age id=1925984056208>
# tom 20
# {'name': 'tom', 'age': 20}

解决类型处理。

class TypeCheck:
    def __init__(self, pname, typ):
        self.pname = pname
        self.type = typ

    def __get__(self, instance, owner):
        print('get ++++', self)
        # return self.data
        if instance: #因为描述器也可以通过类来访问,这里做一下判断
            return instance.__dict__[self.pname]
        return self
        #raise TypeError

    def __set__(self, instance, value):
        print('set ~~~~', instance, value)
        # self.data = value
        if instance:
            if isinstance(value, self.type):  #处理类型
                instance.__dict__[self.pname] = value  #修改被截胡的字典,方法2 利用传参
            else:
                raise  TypeError('{} . {}'.format(self.pname, value))

    def __repr__(self):
        return "<描述器TypeCheck {} id={}>".format(self.pname, id(self))

class Person:
    name = TypeCheck('name', str) #手动传入名称和类型,好吗?
    age = TypeCheck('age', int)
    def __init__(self, name:str, age:int):
        self.name = name #赋值时,调用描述器的set方法
        self.age = age

p1 = Person('tom', 20)
print(p1.name, p1.age) #类型处理
print(p1.__dict__) #被描述器截胡了,需要做成正常一样的

#返回结果
# set ~~~~ <__main__.Person object at 0x000001C06D747E00> tom
# set ~~~~ <__main__.Person object at 0x000001C06D747E00> 20
# get ++++ <描述器TypeCheck name id=1925981699248>
# get ++++ <描述器TypeCheck age id=1925984056208>
# tom 20
# {'name': 'tom', 'age': 20}

解决硬编码问题,形参的定义:

import inspect

def propsinject(cls): #为类对象添加属性

    #返回值是什么
    return cls

@propsinject
class Person: #Person = propsinject(Person)
    # name = TypeCheck('name', str) #手动传入名称和类型,好吗?
    # age = TypeCheck('age', int)
    def __init__(self, name:str, age:int):
        self.name = name #赋值时,调用描述器的set方法
        self.age = age

sig = inspect.signature(Person)
print(sig) #(name: str, age: int) 签名跟init有关
params = sig.parameters
print(params) #OrderedDict({'name': <Parameter "name: str">, 'age': <Parameter "age: int">})
import inspect

class TypeCheck:
    def __init__(self, pname, typ):
        self.pname = pname
        self.type = typ

def propsinject(cls): #为类对象添加属性
    sig = inspect.signature(cls)
    print(sig)
    params = sig.parameters
    print(params)
    for name, param in params.items():
        print(name, param.name, param.kind, param.annotation, param.default)
        if param.annotation is not param.empty:
            setattr(cls, name, TypeCheck(name, param.annotation))
    #返回值是什么
    return cls

@propsinject
class Person: #Person = propsinject(Person)
    # name = TypeCheck('name', str) #手动传入名称和类型,好吗?
    # age = TypeCheck('age', int)
    def __init__(self, name:str, age:int):
        self.name = name #赋值时,调用描述器的set方法
        self.age = age


print(Person.__dict__) #可以看到添加类属性name, age. 'name': <__main__.TypeCheck object at 0x00000212EF0842F0>, 'age': <__main__.TypeCheck object at 0x00000212EEBC7ED0>

#返回结果
# (name: str, age: int)
# OrderedDict({'name': <Parameter "name: str">, 'age': <Parameter "age: int">})
# name name POSITIONAL_OR_KEYWORD <class 'str'> <class 'inspect._empty'>
# age age POSITIONAL_OR_KEYWORD <class 'int'> <class 'inspect._empty'>
# {'__module__': '__main__', '__firstlineno__': 20, '__init__': <function Person.__init__ at 0x00000212EF0A0400>, '__static_attributes__': ('age', 'name'), '__dict__': <attribute '__dict__' of 'Person' objects>, '__weakref__': <attribute '__weakref__' of 'Person' objects>, '__doc__': None, 'name': <__main__.TypeCheck object at 0x00000212EF0842F0>, 'age': <__main__.TypeCheck object at 0x00000212EEBC7ED0>}
import inspect

class TypeCheck:
    def __init__(self, pname, typ):
        self.pname = pname
        self.type = typ

    def __get__(self, instance, owner):
        print('get ++++', self)
        # return self.data
        if instance: #因为描述器也可以通过类来访问,这里做一下判断
            return instance.__dict__[self.pname]
        return self
        #raise TypeError

    def __set__(self, instance, value):
        print('set ~~~~', instance, value)
        # self.data = value
        if instance:
            if isinstance(value, self.type):  #处理类型
                instance.__dict__[self.pname] = value  #修改被截胡的字典,方法2 利用传参
            else:
                raise  TypeError('{} . {}'.format(self.pname, value))

    def __repr__(self):
        return "<描述器TypeCheck {} id={}>".format(self.pname, id(self))

def propsinject(cls): #为类对象添加属性
    sig = inspect.signature(cls)
    params = sig.parameters
    for name, param in params.items():
        # print(name, param.name, param.kind, param.annotation, param.default)
        if param.annotation is not param.empty:
            setattr(cls, name, TypeCheck(name, param.annotation))
    #返回值是什么
    return cls

@propsinject  #类属性通过装饰器动态输入
class Person:
    # name = TypeCheck('name', str) #手动传入名称和类型,好吗?
    # age = TypeCheck('age', int)
    def __init__(self, name:str, age:int):
        self.name = name #赋值时,调用描述器的set方法
        self.age = age

p1 = Person('tom', 20)
print(p1.name, p1.age) #类型处理
print(p1.__dict__) #被描述器截胡了,需要做成正常一样的

#返回结果
# set ~~~~ <__main__.Person object at 0x0000020F4F548440> tom
# set ~~~~ <__main__.Person object at 0x0000020F4F548440> 20
# get ++++ <描述器TypeCheck name id=2264778703600>
# get ++++ <描述器TypeCheck age id=2264773721808>
# tom 20
# {'name': 'tom', 'age': 20}

如果用类做装饰器,该怎么写?

import inspect

class TypeCheck:
    def __init__(self, pname, typ):
        self.pname = pname
        self.type = typ

def propsinject(cls): #为类对象添加属性
    sig = inspect.signature(cls)
    params = sig.parameters
    for name, param in params.items():
        # print(name, param.name, param.kind, param.annotation, param.default)
        if param.annotation is not param.empty:
            setattr(cls, name, TypeCheck(name, param.annotation))
    #返回值是什么
    return cls

class PropInject:
    def __init__(self, cls):
        self.cls = propsinject(cls)

    def __call__(self, *args, **kwargs):
        return self.cls(*args, **kwargs)

@PropInject
class Person: #Person = PropInject(Person)
    # name = TypeCheck('name', str) #手动传入名称和类型,好吗?
    # age = TypeCheck('age', int)
    def __init__(self, name:str, age:int):
        self.name = name #赋值时,调用描述器的set方法
        self.age = age

print(Person.__dict__) #{'cls': <class '__main__.Person'>}
print(Person.cls.__dict__) #可以看到添加类属性name, age. 'name': <__main__.TypeCheck object at 0x000001FAF3268440>, 'age': <__main__.TypeCheck object at 0x000001FAF2DA7ED0>

#返回结果
# {'cls': <class '__main__.Person'>}
# {'__module__': '__main__', '__firstlineno__': 25, '__init__': <function Person.__init__ at 0x000001FAF32805E0>, '__static_attributes__': ('age', 'name'), '__dict__': <attribute '__dict__' of 'Person' objects>, '__weakref__': <attribute '__weakref__' of 'Person' objects>, '__doc__': None, 'name': <__main__.TypeCheck object at 0x000001FAF3268440>, 'age': <__main__.TypeCheck object at 0x000001FAF2DA7ED0>}
import inspect

class TypeCheck:
    def __init__(self, pname, typ):
        self.pname = pname
        self.type = typ

    def __get__(self, instance, owner):
        print('get ++++', self)
        # return self.data
        if instance: #因为描述器也可以通过类来访问,这里做一下判断
            return instance.__dict__[self.pname]
        return self
        #raise TypeError

    def __set__(self, instance, value):
        print('set ~~~~', instance, value)
        # self.data = value
        if instance:
            if isinstance(value, self.type):  #处理类型
                instance.__dict__[self.pname] = value  #修改被截胡的字典,方法2 利用传参
            else:
                raise  TypeError('{} . {}'.format(self.pname, value))

    def __repr__(self):
        return "<描述器TypeCheck {} id={}>".format(self.pname, id(self))

def propsinject(cls): #为类对象添加属性
    sig = inspect.signature(cls)
    params = sig.parameters
    for name, param in params.items():
        # print(name, param.name, param.kind, param.annotation, param.default)
        if param.annotation is not param.empty:
            setattr(cls, name, TypeCheck(name, param.annotation))
    #返回值是什么
    return cls

class PropInject:
    def __init__(self, cls):
        self.cls = propsinject(cls)

    def __call__(self, *args, **kwargs):
        return self.cls(*args, **kwargs)

@PropInject
class Person:
    # name = TypeCheck('name', str) #手动传入名称和类型,好吗?
    # age = TypeCheck('age', int)
    def __init__(self, name:str, age:int):
        self.name = name #赋值时,调用描述器的set方法
        self.age = age

p1 = Person('tom', 20)
print(p1.name, p1.age) #类型处理
print(p1.__dict__) #被描述器截胡了,需要做成正常一样的

#返回结果
# set ~~~~ <__main__.Person object at 0x0000020F4F548440> tom
# set ~~~~ <__main__.Person object at 0x0000020F4F548440> 20
# get ++++ <描述器TypeCheck name id=2264778703600>
# get ++++ <描述器TypeCheck age id=2264773721808>
# tom 20
# {'name': 'tom', 'age': 20}
将前面的链表,封装成容器

要求:提供__getitem__、__iter__、__setitem__方法

class Node:
    def __init__(self, item, next=None, prev=None):
        self.item = item
        self.next = next # Node instance
        self.prev = prev # Node instance

    def __repr__(self):
        return "{} <== {} ==> {}".format(
            self.prev.item if self.prev else None,
            self.item,
            self.next.item if self.next else None
        )

class LinkedList:
    def __init__(self):
        self.head = None
        self.tail = None
        self._size = 0

    def append(self, value):
        node = Node(value) # 下一个是None的结点

        if self.tail is None: # empty
            self.head = node
        else:
            self.tail.next = node
            node.prev = self.tail
        self.tail = node
        self._size += 1
        return self

    def __add__(self, item):
        return self.append(item)

    # def insert(self, index, value):
    #     if index < 0:
    #         raise IndexError('Wrong index. Not negative index {}'.format(index))
    #
    #     current = None
    #     for i,node in enumerate(self.iternodes()):
    #         if i == index:
    #             current = node #找到了位置
    #             break
    #     else: #如果能执行self子句,说明? 没有找到,超界了
    #         return self.append(value) # 没找到,当做尾部追加
    #
    #     # 找到了,容器非空,就准备在中间某个位置插入数据
    #     newnode = Node(value)
    #     prev = current.prev
    #     # next = current
    #     #1 开头插入
    #     if prev is None: # i==0  index==0  self.head==current
    #         self.head = newnode
    #     else: #2 非开头
    #         newnode.prev = prev
    #         prev.next = newnode
    #     newnode.next = current
    #     current.prev = newnode
    #     self._size += 1
    #     return self

    def insert(self, index, value):
        if index > len(self):
            return self.append(value)
        if index < -len(self):
            index = 0

        current = self[index]

        # 找到了,容器非空,就准备在中间某个位置插入数据
        newnode = Node(value)
        prev = current.prev
        # next = current
        #1 开头插入
        if prev is None: # i==0  index==0  self.head==current
            self.head = newnode
        else: #2 非开头
            newnode.prev = prev
            prev.next = newnode
        newnode.next = current
        current.prev = newnode
        self._size += 1
        return self

    def pop(self):
        if self.tail is None:
            raise Exception("empty")

        tail = self.tail # 当前尾巴元素
        prev = tail.prev # 前尾巴一个元素
        # 1 only one 改头尾; 2 不止一个,改尾
        if prev is None: # self.head.next is None
            self.head = None
            self.tail = None
        else:
            prev.next = None
            self.tail = prev

        value = tail.item
        self._size -= 1
        return value

    def remove(self, index):
        if self.tail is None:
            raise Exception("empty")

        current = self[index]

        prev = current.prev #当前被移除节点的上一个
        next = current.next #当前被移除节点的下一个

        if prev is None and next is None: # 只有一个 # self.head == self.tail
            self.head = None
            self.tail = None
        elif prev is None: # 头部移除
            self.head = next
            next.prev = None
        elif next is None: # 尾部移除
            self.tail = prev
            prev.next = None
        else: # 中间移除
            prev.next = next
            next.prev = prev

        # del current # 删除临时变量,减少引用计数,释放内存
        self._size -= 1

    def iternodes(self, reverse=False):
        current = self.tail if reverse else self.head
        while current:
            yield current
            current = current.prev if reverse else current.next

    # @property
    # def size(self):
    #     return self._size
    size = property(lambda self: self._size)
    # 容器化
    def __len__(self):
        return self.size

    # def __iter__(self): #迭代器
    #     return self.iternodes()
    #     yield from self.iternodes()

    __iter__ = iternodes #高手写法 #self.__iter__() self.iternodes()

    def __reversed__(self): #反向遍历
        yield from self.iternodes(reverse=True)
        # return self.iternodes(True)

    # def __getitem__(self, index):
    #     #假设 index >= 0 正向超界
    #     if index >= len(self):
    #         raise IndexError('Wrong Index {}'.format(index))
    #     #没有正向超界
    #     for i,node in enumerate(self.iternodes()):
    #         if i == index:
    #             return node
    #     ######
    #     if index < -len(self):
    #         raise IndexError('Wrong Index {}'.format(index))
    #     for i,node in enumerate(self.iternodes(reverse=True)):
    #         if i == -index:
    #             return node

    def __getitem__(self, index):
        #假设 index >= 0 正向或负向超界
        if index >= len(self) or index < -len(self):
            raise IndexError('Wrong Index {}'.format(index))
        #没有正向超界
        reverse = True if index < 0 else False
        start = 1 if index < 0 else 0

        for i, node in enumerate(self.iternodes(reverse), start):  # index >=0
            if i == abs(index):
                return node

    def __setitem__(self, index, value):
        self[index].item = value

ll = LinkedList() # 实例化
ll.append(1).append(2).append(3)
print(*ll)
for x in reversed(ll):
    print(x)
print('-' * 30)
print(ll[1])
print(ll[-1], ll[-3])
ll[-3] = 100
print(*ll)
print('=' * 30)
ll.insert(-10, 'start').insert(100,'end')
print(*ll)

# 执行结果
# None <== 1 ==> 2 1 <== 2 ==> 3 2 <== 3 ==> None
# 2 <== 3 ==> None
# 1 <== 2 ==> 3
# None <== 1 ==> 2
# ------------------------------
# 1 <== 2 ==> 3
# 2 <== 3 ==> None None <== 1 ==> 2
# None <== 100 ==> 2 100 <== 2 ==> 3 2 <== 3 ==> None
# ==============================
# None <== start ==> 100 start <== 100 ==> 2 100 <== 2 ==> 3 2 <== 3 ==> end 3 <== end ==> None
进阶

实现类property装饰器,类名称为Property.

基本结构如下,是一个数据描述器。

class Property:
    def __init__(self, fget, fset=None):
        pass

    def __get__(self, instance, owner):
        pass

    def __set__(self, instance, value):
        pass

class A:
    def __init__(self, data):
        self._data = data

    @Property
    def data(self):
        return self._data

    @data.setter
    def data(self, value):
        self._data = value

内置实例

class A:
    def __init__(self, x):
        self._x = x

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        self._x = value

a = A(5)
print(a.x)
print(a.__dict__)
a.x = 100
print(a.x)
print(a.__dict__)

#返回结果
# 5
# {'_x': 5}
# 100
# {'_x': 100}

实现一样的功能

class Property:  #数据描述器
    def __init__(self, fn):
        self.__fn = fn

    def __get__(self, instance, owner):
        print('get ~~~~', instance)
        print('**** {} ****'.format(self.__fn))  #是普通函数A.x, 没有绑定效果
        return self.__fn(instance)

    def __set__(self, instance, value):
        print('set ~~~~')

class A:
    def __init__(self, x):
        self._x = x

    # x = Property(x)
    @Property
    def x(self): #等价式 x = Property(x) #a.x()
        return self._x

    # @x.setter
    # def x(self, value):
    #     self._x = value

a = A(5)
print(a.x)
print(a.__dict__)
# a.x = 100
# print(a.x)
# print(a.__dict__)

#返回结果
# get ~~~~ <__main__.A object at 0x0000016DD9CE4EC0>
# **** <function A.x at 0x0000016DD9D1F1A0> ****
# 5
# {'_x': 5}
class Property:  #数据描述器
    def __init__(self, fget, fset=None):
        self.fget = fget
        self.fset = fset

    def __get__(self, instance, owner):
        print('get ~~~~', instance)
        print('**** {} ****'.format(self.fget))  #是普通函数A.x, 没有绑定效果
        if instance:
            return self.fget(instance)
        else:
            return None

    def __set__(self, instance, value):
        print('set ~~~~')
        if instance:
            if self.fset is None:
                raise AttributeError('不能修改')
            else:
                self.fset(instance, value)

    def setter(self, fn):
        self.fset = fn
        return self


# property
class A:
    def __init__(self, x):
        self._x = x

    # x = Property(x)
    @Property
    def x(self): #等价式 x = Property(x, None) #a.x()
        return self._x

    # x = x.setter(x) #x 应该是什么? 必须是Property实例
    # x = Property实例.setter(x)
    # x = Property实例
    @x.setter
    def x(self, value): #x = x.setter(x)
        self._x = value

a = A(5)
print(a.x)
print(a.__dict__)
a.x = 5000
# print(a.x)
print(a.__dict__)

#返回结果
# get ~~~~ <__main__.A object at 0x0000017CE52F4D70>
# **** <function A.x at 0x0000017CE532F240> ****
# 5
# {'_x': 5}
# set ~~~~
# {'_x': 5000}

其他杂项

trancemalloc

标准库trancemalloc,可以统计内存使用情况

https://docs.python.org/3/library/tracemalloc.html

import tracemalloc # from 3.4

tracemalloc.start() # 开始跟踪内存分配

# ... run your application ...
count = 1000000
# count = 2
d = [dict(zip('xy',(5, 6))) for i in range(count)] #容器里放了若干字典 184MiB [{'x': 5, 'y': 6}, {'x': 5, 'y': 6}]
t = [tuple(zip('xy',(5, 6))) for i in range(count)] #容器里放了若干元组 168MiB [(('x', 5), ('y', 6)), (('x', 5), ('y', 6))]

snapshot = tracemalloc.take_snapshot() # 快照,当前内存分配
top_stats = snapshot.statistics('lineno') # 快照对象的统计
#filename:统计整个文件内存
#lineno:分开统计

for stat in top_stats:
    print(stat)

#返回结果
# D:\project\pyproj\t1.py:8: size=184 MiB, count=1999998, average=96 B
# D:\project\pyproj\t1.py:9: size=168 MiB, count=3000004, average=59 B
# D:\project\pyproj\t1.py:6: size=400 B, count=1, average=400 B
# D:\py\python3130\Lib\tracemalloc.py:558: size=56 B, count=1, average=56 B

所以可以得知内存使用上字典还是较为占用空间的

__slots__
问题的引出
class A:
    def __init__(self):
        self.x = 4
        self.y = 5

a = A()
print(a.x, a.y)
print(a.__dict__) #{'x': 4, 'y': 5} 如果有100个 实例呢? 怎样保存数据更省内存? 元组
  • 字典为了提升查询效率,必须用空间换时间
  • 一般来说一个实例,属性多一点,都存储在字典中便于查询,问题不大
  • 但是如果数百万个实例,那么字典占的总空间就有点大
  • Python提供了__slots__可以把属性字典__dict__省略
class A:
    M = 'MN'
    __slots__ = ('x', 'y') # 元组
    # __slots__ = ['x', 'y']  # 等同上面
    # __slots__ = 'x', 'y'  # 等同上面. 类属性,控制实例的属性。如果提供了该类属性,则实例字典默认取消

    def __init__(self):
        self.x = 4
        self.y = 5

a = A()
print(a.x, a.y)
# print(a.__dict__) #{'x': 4, 'y': 5}
print(a.M)
# a.z = 200 #不可以,因为z属性不允许
a.x = 100
print(a.x)
print(a.__slots__)

#返回结果
# 4 5
# MN
# 100
# ('x', 'y')
  • __slots__告诉解释器,实例的属性都叫什么,一般来说,既然要节约内存,最好还是使用元组比较好
  • 一旦类提供了__slots__,就阻止实例产生__dict__来保存实例的属性

动态增加属性

# 为实例动态增加属性
a.newx = 5 # AttributeError: 'A' object has no attribute 'newx'

# 为类动态增加属性
A.newx = 5 # 不报错,因为是类属性

说明实例不可以动态增加属性,而类可以

继承

使用继承,看看__slots__是否会继承

class A:
    M = 'MN'
    __slots__ = '_x', '_y'  # 等同上面. 类属性,控制实例的属性。如果提供了该类属性,则实例字典默认取消

    def __init__(self):
        self._x = 4
        self._y = 5

    @property
    def x(self):
        return self._x

class B(A): # __slots__ 不继承
    pass

a = A()
# a.z = 300 #不可以

b = B()
b.z = 200
print(b.z)
print(b.__dict__)
print(b.x)
print(b._y)
b._y = 400
print(b.__dict__)
print(b._y)

#返回结果
# 200
# {'z': 200}
# 4
# 5
# {'z': 200}
# 400

结果表明,__slots__不影响子类实例,不会继承下去,除非子类里面自己也定义了__slots__

应用场景

使用需要构建在数百万以上众多对象,且内存容量较为紧张,实例的属性简单、固定且不用动态增加的场景

可以使用tracemalloc看看内存使用的差异。

建议使用stats = snapshot.statistics('filename')查看总内存使用

import tracemalloc

class A:
    M = 'MN'
    __slots__ = '_x', '_y'  # 等同上面. 类属性,控制实例的属性。如果提供了该类属性,则实例字典默认取消

    def __init__(self):
        self._x = 4
        self._y = 5

# print(a.__dict__) #{'x': 4, 'y': 5} 如果有100个 实例呢? 怎样保存数据更省内存? 元组

tracemalloc.start()

instances = [A() for i in range(1000000)] #100万个实例字典 92.0 MiB ,使用__slots__ 53.8 MiB

snapshot = tracemalloc.take_snapshot() # 快照,当前内存分配
top_stats = snapshot.statistics('lineno') # 快照对象的统计
#filename:统计整个文件内存
#lineno:分开统计

for stat in top_stats:
    print(stat)

#不使用__slots__ 返回结果
# D:\project\pyproj\t1.py:15: size=92.0 MiB, count=1000001, average=96 B
# D:\py\python3130\Lib\tracemalloc.py:558: size=56 B, count=1, average=56 B

#使用__slots__ 返回结果
# D:\project\pyproj\t1.py:15: size=53.8 MiB, count=1000001, average=56 B
# D:\py\python3130\Lib\tracemalloc.py:558: size=56 B, count=1, average=56 B


#应用场景
# 1 实例属性简单,固定,很少
# 2 大量使用,如100万
# 3 内存紧张
未实现和未实现异常
print(NotImplemented, type(NotImplemented)) #未实现,值 NotImplemented <class 'NotImplementedType'>
print(NotImplementedError, type(NotImplementedError)) #异常类 <class 'NotImplementedError'> <class 'type'>

print(None, type(None)) #None <class 'NoneType'>

try:
    # raise NotImplemented # raise后应该放个异常类,不是BaseException的子类。 TypeError: exceptions must derive from BaseException
    raise NotImplementedError # 抛出 未实现异常
except Exception as e:
    print('有异常', e)
  • NotImplemented是个值,单值,是NotImplementedType的实例
  • NotImplementedError是类型,是异常类,返回type
运算符重载中的反向方法
class A:
    def __init__(self, x):
        self.x = x

    def __add__(self, other):
        print('add~~~~')
        return self.x + other.x

    def __iadd__(self, other):
        print('iadd~~~~')
        return A(self.x + other.x)

    def __radd__(self, other):
        print('radd~~~~')
        return self.x + other.x

a = A(4)
b = A(5)
print(id(a), id(b))
print(a + b) #add 9  a.__add__(b)
print(id(a), id(b))
a += b #iadd #A(9)
print(id(a), id(b))
print(b + a) #add 14  b.__add__(a)

#返回结果
# 1882495745200 1882498364304
# add~~~~
# 9
# 1882495745200 1882498364304
# iadd~~~~
# 1882498364624 1882498364304
# add~~~~
# 14
class A:
    def __init__(self, x):
        self.x = x

    def __add__(self, other):
        print('add~~~~', self, other)
        return self.x + other.x

    def __iadd__(self, other):
        print('iadd~~~~')
        return A(self.x + other.x)

    def __radd__(self, other):
        print('radd~~~~')
        return self.x + other.x

    def __repr__(self):
        return '<A {}>'.format(self.x)

a = A(4)
b = A(5)
# print(a + b) #add 9  a.__add__(b)
# print(b + a) #add 14  b.__add__(a)
print('-' * 30)
print(a + 1) #a.__add__(1)

#返回结果
# ------------------------------
# add~~~~ <A 4> 1
# Traceback (most recent call last):
#   File "D:\project\pyproj\t1.py", line 25, in <module>
#     print(a + 1)
#           ~~^~~
#   File "D:\project\pyproj\t1.py", line 7, in __add__
#     return self.x + other.x
#                     ^^^^^^^
# AttributeError: 'int' object has no attribute 'x'

上面可以的看到,1是没有x属性的。

class A:
    def __init__(self, x):
        self.x = x

    def __add__(self, other):
        print('add~~~~', self, other)
        return self.x + other.x

    def __iadd__(self, other):
        print('iadd~~~~')
        return A(self.x + other.x)

    def __radd__(self, other):
        print('radd~~~~')
        return self.x + other.x

    def __repr__(self):
        return '<A {}>'.format(self.x)

a = A(4)
b = A(5)
# print(a + b) #add 9  a.__add__(b)
# print(b + a) #add 14  b.__add__(a)
print('-' * 30)
# print(a + 1) #a.__add__(1)
print(1 + a) #调用radd

#返回结果
# ------------------------------
# radd~~~~
# Traceback (most recent call last):
#   File "D:\project\pyproj\t1.py", line 26, in <module>
#     print(1 + a)
#           ~~^~~
#   File "D:\project\pyproj\t1.py", line 15, in __radd__
#     return self.x + other.x
#                     ^^^^^^^
# AttributeError: 'int' object has no attribute 'x'

为什么会报错:

  • 1 + a等价于1.__add__(a),也就是int.__add__(1, a),而int类型实现了__add__方法,做了特殊处理,转而调用了后面的实例的魔术方法 实例a的__radd__方法
print(int.__add__(1, 2)) #3 int类型实现了 + += - 这些方法

class A:
    def __init__(self, x):
        self.x = x

    def __radd__(self, other):
        print('radd~~~~')
        return 'radd^^^^'

a = A(4)
print('abc' + a) #str.__add__('abc', a)
print([] + a) #list.__add__([], a) 内建数据类型

分析下面例子代码

class A:
    def __init__(self, x):
        self.x = x

    def __add__(self, other):
        print('add~~~~', self, other)
        return self.x + other.x

    def __iadd__(self, other):
        print('iadd~~~~')
        return A(self.x + other.x)

    def __radd__(self, other):
        print('radd~~~~', self, other)
        return self.x + other.x

    def __repr__(self):
        return '<A {}>'.format(self.x)

class B:
    def __init__(self, x):
        self.x = x

    # 要么是未实现,要么是未实现异常

    def __repr__(self):
        return '<B {}>'.format(self.x)

a = A(4)
b = B(10)
print(a + b) #a.__add__(b)
print(b + a) #b.__add__(a) ? a.__radd__(?)


#返回结果
# add~~~~ <A 4> <B 10>
# 14
# radd~~~~ <A 4> <B 10>
# 14
class A:
    def __init__(self, x):
        self.x = x

    def __add__(self, other):
        print('add~~~~', self, other)
        return self.x + other.x

    def __iadd__(self, other):
        print('iadd~~~~')
        return A(self.x + other.x)

    def __radd__(self, other):
        print('radd~~~~', self, other)
        return self.x + other.x

    def __repr__(self):
        return '<A {}>'.format(self.x)

class B:
    def __init__(self, x):
        self.x = x

    # 要么是未实现,要么是未实现异常
    def __add__(self, other):
        print('B add %%%%', self, other)
        # return 1000
        # raise NotImplementedError('未实现异常') #不能用在这,用在抽象基类中
        # raise NotImplemented  #这里不能用raise
        return NotImplemented

    def __repr__(self):
        return '<B {}>'.format(self.x)

a = A(4)
b = B(10)
print(a + b) #a.__add__(b)
print(b + a) #b.__add__(a) ? a.__radd__(?)
#按道理b.__add__(a),但如果没有实现,转而调用 a.__radd__(?)
#如果b有__add__,那么调用它,返回值就是结果
#如果b有__add__,return NotImplemented,则a.__radd__()


#返回结果
# add~~~~ <A 4> <B 10>
# 14
# B add %%%%
# radd~~~~ <A 4> <B 10>
# 14
  • b + a 等价于b.__add__(a),但是类B没有实现__add__方法,反而去找a的__radd__方法
  • 1 + a等价于1.__add__(a),而int类型实现了__add__方法,不过这个方法对于这种加法的返回值是NotImplemented,解释器发现是这个值,就会发起对第二操作对象的__radd__方法的调用

B类也等价于下面的实现

class B:
    def __init__(self, x ):
        self.x = x

    def __add__(self, other):
        if isinstance(other, type(self)):
            return self.x + other.x
        else:
            return NotImplemented

1 + a 用如下方法解决


Python的对象模型

Python的继承层次

单一继承,多复用,OCP覆盖
多继承,二义性、单调性,C3, mro, 线性化,object
一切类都从object类继承,object也是类,根基类
自定义类
  class A:pass, 新式类,父类就是object
  class B(A):pass, 父类A
type是类,继承关系,父类也是object

子孙类的实例和object
  type(实例) => 自身的类型
子孙类和object有什么关系
  都是从object派生
object的父类是谁?没有父类

type用来干什么?

内建函数,类,元类
判断一个实例的类型
type(A()) => A类,说明A的实例由A构造实例化
type(A) => type,A是type的实例,由type构造实例化成的类对象
type(type) => type,type类对象,有type构造的实例
类对象的类型是type,说明他们都是type的实例
type(object) => type,object类也是由type构造的类对象
type的父类 object,从object派生

在Python中,任何对象多有类型,可以使用type()或者__class__查看

但是类型也是对象即类对象,它也有自己的类型

img_20250218_154241.png

所有新类型的缺省类型是type(可以使用元类来改变)

  • 特殊类型type是所有对象的缺省类型,也包括type自己。但它又是一个对象,因此从object继承
  • 特殊类型object是继承树的顶层,它是python所有类型的最终基类

也就是说,继承都来自object,类型都看type。type也是对象继承自object,object也有类型是type。

这俩又特殊,type类型是它自己,obkect没有基类

可迭代对象
可迭代对象
for in 对象:

列表、元组、set、dict、range、生成器、迭代器,统称为容器,若干个元素
自定义类型,实现 __iter__(self) 返回迭代器,类型的实例就是可迭代对象
可迭代对象,一定在类上实例了__iter__方法
class A:
    def __init__(self):
        self.items = [100, 200, 300]

    def __iter__(self):
        # return iter(self.items)
        yield from self.items #生成器对象

    def __reversed__(self): #没有实现len、getitem魔术方法
        #必须返回迭代器
        # return reversed(self.items) #list内部肯定实现了__reversed__方法
        yield from reversed(self.items)


a = A()
for x in a:
    print(x)
print('-' * 30)

#反向迭代
for x in reversed(a): #reversible
    print(x)

类提供了__iter__魔术方法,返回一个可迭代器。类的实例就是可迭代对象,确切地讲是正向迭代。

如果提供了__reversed__魔术方法,返回一个迭代器,就可以使用内建函数reversed来逆向迭代。

迭代器

https://docs.python.org/3/glossary.html#term-iterator

用来表示一连串数据流的对象。 重复调用迭代器的 __next__() 方法 (或将其传给内置函数 next()) 将逐个返回流中的项。

class Inc:
    def __init__(self):
        self.count = -1

    # def __iter__(self):

    def __next__(self):
        return 123

inc = Inc()
print(next(inc)) #123
print(next(inc)) #123

for 循环本质

class Inc:
    def __init__(self):
        self.count = -1

    def __iter__(self):
        print('iter~~~~')
        return self #self 是不是迭代器

    def __next__(self): #迭代器,必须有__next__方法,但请把__iter__也实现了
        print('next~~~~')
        self.count += 1
        if self.count >5:
            raise StopIteration
        return self.count

inc = Inc()
# print(inc) #<__main__.Inc object at 0x000002362A977CB0>
# print(next(inc)) #0
# print(next(inc)) #1
# print(next(inc)) #2
print('-' * 30)

import time
print(next(inc, 'end____'))
print('-' * 30)
for x in inc: #inc是迭代器,但是
    time.sleep(1)
    print(x)
print('=' * 30)
print(next(inc, 'end____'))

#for 循环本质
#for in 可迭代对象
#1 可迭代对象__iter__拿到一个迭代器
#2 对迭代器.__next__逐个元素迭代,直到出StopIterration

for循环in后面是一个可迭代对象,本质上,调用了__iter__魔术方法拿到了一个迭代器, for循环驱动的是一个迭代器,相当于不断调用一个迭代器的__next__方法。

生成器
  • Python中由生成器表达式或生成器函数生成,快速得到迭代器的方式
  • 生成器一定是迭代器,内部实例了迭代器接口。

Python可迭代对象,迭代器,生成器的关系

  • 可迭代对象
    • 迭代器
      • 生成器对象
        • 由生成器表达式或生成器函数得到的

可迭代对象与

  • 能够通过迭代一次次返回不同的元素的对象
  • 所谓相同,不是指值是否相同,而是元素在容器中是否是同一个,例如列表中值可以重复的,['a', 'a'],虽然这个列表有2个元素,值一样,但是两个'a'是不同的元素
  • 可以迭代,但是未必有序,未必可索引
  • 可迭代对象有:list、tuple、string、bytes、bytearray、range、set、dict、生成器、迭代器等
  • 可以使用成员操作符in、not in
  • 对于线性数据结构,in本质上是在遍历对象,时间复杂度为O(n)

迭代器

  • 特殊的对象,一定是可迭代对象,具备可迭代对象的特征
  • 如果一个对象拥有__iter__方法,其是可迭代对象;如果一个对象拥有next方法,其是迭代器。
  • 定义可迭代对象,必须实现__iter__方法;定义迭代器,必须实现__iter__和next方法

生成器generator

  • 生成器指的是生成器对象,可以由生成器表达式得到,也可以使用yield关键字编写一个生成器函数, 调用这个函数得到一个生成器对象
  • 生成器对象,是一个可迭代对象,是一个迭代器
  • 生成器对象,是延迟计算、惰性求值的

模块化

一般来说,变成语言中,库、包、模块是同一种概念,是代码组织方式。

Python中只有一种模块对象类型,但是为了模块化组织模块的便利,提供了“包”的概念。

模块module,指的是Python的源代码文件。

包package,指的是模块组织在一起的包名同名的目录及其相关文件。

导入语句

import模块1[,模块2,…]   完全导入
import…as…    模块别别名

import语句

  1. 找到指定的模块,加载和初始化它,生成模块对象。找不到,抛出异常
  2. 在import所在的作用域的局部命名空间中,增加名称和上一步创建的对象关联

单独运行以下例子,体会区别

import functools #import后看似是模块类型,临时加载,全局变量以模块为边界
import os

print(dir()) #返回类或者对象的所有成员名称列表
print(type(os), os) #不同的模块指向不同的py文件
print(type(functools), functools)

#返回结果
# [... 'functools', 'os']
# <class 'module'> <module 'os' (frozen)>
# <class 'module'> <module 'functools' from 'D:\\py\\python3130\\Lib\\functools.py'>
import functools #import后看似是模块类型,临时加载,全局变量以模块为边界

print(dir()) #[... 'functools']
print(type(functools), functools) #<class 'module'> <module 'functools' from ''path/to/functools.py'>
print(functools.wraps) #<function wraps at 0x000001C045DA6D40>
print(locals())
print(globals().keys())

#返回结果
# <class 'module'> <module 'functools' from 'D:\\py\\python3130\\Lib\\functools.py'>
# <function wraps at 0x000001C045DA6D40>
# {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000001C045CF44B0>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'D:\\project\\pyproj\\t1.py', '__cached__': None, 'functools': <module 'functools' from 'D:\\py\\python3130\\Lib\\functools.py'>}
# dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', '__file__', '__cached__', 'functools'])
import os.path # 导入os.path,os加入当前名词空间
print(dir()) #[..., 'os']
print(os) # # <module 'os' from ''path/to/os.py'>
print(os.path) # 完全限定名词访问path
import os.path as osp # 导入os.path并赋给osp
print(dir()) # [..., 'osp']
print(osp) # <mdule 'ntpath' form 'path/to/path.py'>
def testimport():
  import os.path # 局部
  print(dir())

testimport() # ['os']
print(globals().keys()) # 没有‘os’,但有'testimport'
# dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', '__file__', '__cached__', 'testimport'])
import os.stat # 不可导入
# ModuleNotFoundError: No module named 'os.stat'; 'os' is not a package

总结

  • 导入顶级模块,其名词会加入到本地名词空间中,并绑定到其模块对象
  • 导入非顶级模块,只将其顶级模块名称加入到本地名词空间中。导入的模块必须使用完全限定名称来访问。
  • 如果使用了as,as后的名词直接绑定到导入的模块对象,并将该名词加入到本地名词空间中
  • import之后只能是模块类型

导入语句

from…import…   部分导入
from…import…as… 别名

from语句

from pathlib import Path, PosixPath # 在当前名词空间导入该模块指定的成员
print(dir()) # [..., 'Path', 'PosixPath']
from pathlib import * # 在当前名词空间导入该模块所有公共成员(非下划线开头成员)或指定成员
print(dir()) # [...,'Path', 'PosixPath', 'PurePath', 'PurePosixPath', 'PureWindowsPath', 'WindowsPath']
from functools import wraps as wr,partial # 别名
print(dir()) # [..., 'wr', 'partial]
from os.path import exists

if exists('c://'):
  print('Found') # Found
else:
  print('Not Found')

print(dir()) # [..., 'exists']
print(exists) # <function exists at 0x000001C57FF9AD08>

#4种方式获得同一个对象exists
import os

print(os.path.exists)
print(exists)
print(os.path.__dict__['exists']) # 字符串
print(getattr(os.path, 'exists')) # 字符串
# 打印结果都为
#<built-in function _path_exists>

总结

  • 找到from子句中指定的模块,加载并初始化它(注意不是导入)
  • 对于import子句后的名词
    1. 先查from子句导入的模块是否具有该名词的 属性
    2. 如果不是,则尝试导入该名词的子模块
    3. 还没有找到,则抛出ImportError异常
    4. 这个名称保存到本地名词空间中,如果有as子句,则使用as子句后的名称
from pathlib import Path
print(id(Path), Path)

import pathlib as p1
print(id(p1.Path), p1.Path)

print(p1.Path is Path)

#返回结果
# 2856710771152 <class 'pathlib._local.Path'>
# 2856710771152 <class 'pathlib._local.Path'>
# True

可以看出,导入的名词Path和p1.Path是同一个对象

自定义模块

自定义模块:.py文件就是一个模块

# t1.py文件
print('This is {} module'.format(__name__)) #主模块

class A:
    def showmodule(self):
        print(1, self.__module__, self)
        print(2, self.__class__, id(self.__class__))

a = A()
a.showmodule()

# t2.py文件
import t1 #从磁盘加载到内存,t1模块对象创建,t1现在不是主模块,是被导入模块,顶层代码被执行

print('-' * 30)
print(t1.A, id(t1.A), t1.a)

a = t1.A()
a.showmodule()

# t3.py文件
from t1 import A as cls

a = cls()
a.showmodule()
自定义模块命名规范
  • 模块名就是文件名
  • 模块名必须符合标识符的要求,是非数字开头的字母、数字和下划线的组合。
    • 如 test-module.py这样的文件名不能作为模块名,也不要使用中文
  • 不要使用系统模块名来命名,避免冲突
  • 通常模块名为全小写,下划线来分割单词

模块搜索顺序

使用sys.path查看搜索顺序

import sys

print(*sys.path, sep='\n')
# for p in sys.path:
#     print(p)

返回结果

D:\project\pyproj\.venv\Scripts\python.exe D:\project\pyproj\t1.py 
D:\project\pyproj
D:\project\pyproj
D:\project\pyproj\.venv\Scripts\python313.zip #虚拟环境,内含标准库
D:\py\python3130\DLLs #动态库
D:\py\python3130\Lib #静态库
D:\py\python3130 #系统目录
D:\project\pyproj\.venv #虚拟环境根目录
D:\project\pyproj\.venv\Lib\site-packages #第三方包安装目录

显示结果为,Python模块的路径搜索顺序

当加载一个模块的时候,需要从这些搜索路径中从前到后依次查找,并不搜索这些目录的子目录。

搜索到模块就加载,搜索不到就抛异常

路径也可以为字典、zip文件、egg文件名。

.egg文件,由setuptools库创建的包,第三方库常用的格式。添加了元数据(版本号、依赖项等)信息的zip文件

路径顺序为

  • 程序主目录,程序运行的主程序脚本所在的目录
  • PYTHONPATH目录,环境变量PYTHONPATH设置的目录也是搜索模块的路径
  • 标准库目录,Python自带的模块所在目录

sys.path虽可以被修改,增加新的目录,但不建议修改

模块重复导入

# t1.py文件
print('This is {} module'.format(__name__)) #主模块

class A:
    def showmodule(self):
        print(1, self.__module__, self)
        print(2, self.__class__, id(self.__class__))

a = A()
a.showmodule()

# t2.py文件
import t1
print('-' * 30)
import t1
import t1
print('=' * 30)

返回结果

This is t1 module
1 t1 <t1.A object at 0x0000023478A97E00>
2 <class 't1.A'> 2424389032576
------------------------------
==============================

从执行结果来看,不会产生重复导入的现象。

所有加载的模块都会记录在 sys.modules 中,sys.modules是储存已经加载过的所有模块的的字典

打印sys.modules可以看到builtins、os、os.path、sys等模块都已经加载了

# t2.py文件
import sys
print(*sys.modules.items(), sep='\n')
print('t1' in sys.modules.keys())
import t1
print('t1' in sys.modules.keys())

#返回结果
# False
# This is t1 module
# 1 t1 <t1.A object at 0x0000016B758B7CB0>
# 2 <class 't1.A'> 1561048488352
# True

模块运行

__name__ ,每个模块都会定义一个 __name__ 特殊变量来储存当前模块的 名词,如果不指定,则默认为源代码文件名,如果是包则有限定名。

解释器初始化的时候,会初始化sys.modules字典(保存已加载的模块),加载 builtins(全局函数、常量)模块、 __main__ 模块、sys模块,以及初始化 模块搜索路径sys.path

Python是脚本语言,任何一个脚本都可以直接执行,也可以作为模块被导入。

当从标准输入(命令行方式敲代码)、脚本($ python t1.py) 或交互式读取的 时候,会将模块的 __name__ 设置为 __main__ ,模块的顶层代码就在 __main__ 这个作用域中执行。顶层代码:模块中缩进最外层的代码

如果是import导入的,其 __name__ 默认就是模块名

#t1.py
print(__name__)

if __name__ == '__main__': #判断模块是否以程序的方式运行 python t1.py
    print("in main module running") #程序的方式运行的代码
    #测试代码
else:
    print('in module running') #模块导入的方式运行的代码

#返回结果
# __main__
# in main module running
# t2.py文件
import t1

#返回结果
# t1
# in module running
if__name__ == '__main__'
  • 本模块的功能测试

    对于非主模块,测试本模块内的函数、类

  • 避免主模块变更的副作用

    顶层代码,没有封装,主模块使用时没有问题。但是,一旦有了新的主模块,老的主模块成了被导入模块,由于原来代码没有封装,就会一并执行

模块的属性
__file__  字符串,源文件路径
__cached__  字符串,编译后的字节码文件路径
__spec__  显示模块的规范
__name__  模块名
__package__ 当模块是包,同__name__;否则,可以设置为顶级模块的空字符串

范例:

print(dir())
print(*locals().items(), sep='\n') #t3没有包

import os
print(os.path.dirname(__file__))

#返回结果
# ['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__']
# ('__name__', '__main__') #模块名
# ('__doc__', None)
# ('__package__', None) #当模块是包,同__name__;否则,可以设置为顶级模块的空字符串
# ('__loader__', <_frozen_importlib_external.SourceFileLoader object at 0x000001E0929644B0>) 源代码的加载器
# ('__spec__', None) #显示模块的规范
# ('__annotations__', {})
# ('__builtins__', <module 'builtins' (built-in)>)
# ('__file__', 'D:\\project\\pyproj\\t3.py') #源文件路径
# ('__cached__', None) #编译后的字节码文件路径
# D:\project\pyproj
import t1

for k,v in t1.__dict__.items():
    print(k,str(v)[:80])

print('-' * 30)
print(dir(t1))
for name in dir(t1):
    print(getattr(t1, name))

  • 包,特殊的模块
  • Python模块支持目录

实验

新建一个目录m, 使用下面代码

import m
print(m, type(m))
print(*m.__dict__.items(), sep='\n') #package m , __file__呢? 因为是目录有 __path__

print('-' * 30)
import t1
print(*t1.__dict__.items(), sep='\n') # py __file__ 文件路径,__package__ 空

#返回结果
# <module 'm' (namespace) from ['D:\\project\\pyproj\\m', 'D:\\project\\pyproj\\m']> <class 'module'>
# ('__name__', 'm')
# ('__doc__', None)
# ('__package__', 'm')
# ('__loader__', <_frozen_importlib_external.NamespaceLoader object at 0x0000023536F97E00>) #被名字空间加载
# ('__spec__', ModuleSpec(name='m', loader=<......
# ('__file__', None)
# ('__path__', _NamespacePath(['D:\\project\\pyproj\\m', 'D:\\project\\pyproj\\m']))
# ------------------------------
# t1
# in module running
# ('__name__', 't1')
# ('__doc__', None)
# ('__package__', '')
# ('__loader__', <_frozen_importlib_external.SourceFileLoader object at 0x0000025C8A3157E0>)
# ('__spec__', ModuleSpec(name='t1', loader=<_frozen_importlib_external.SourceFileLoader object at 0x0000025C8A3157E0>, origin='D:\\project\\pyproj\\t1.py'))
# ('__file__', 'D:\\project\\pyproj\\t1.py')
# ('__cached__', 'D:\\project\\pyproj\\__pycache__\\t1.cpython-313.pyc')
# ('__builtins__', {'__name__': ......

一个目录文件没有__file__,但是有__path__

上例中可以看出,可以导入目录m,目录也是文件,所以可以导入。

Python为了解决能在目录模块中写代码,建立了一个特殊文件 __init__.py

('__file__', 'D:\\project\\pyproj\\m\\__init__.py')

pycharm中,创建Directory和创建Pyhon pcakage不同,前者是创建普通的目录,后者是创建一个带有__init__.py文件的目录即包。

Python中,目录可以作为模块,这就是包,不过代码需要写在该目录下 __init__.py 中。

包的 __file__ 就指向 __init__.py 这个文件。

子模块

包目录下的py文件、子目录都是其子模块

m
|-- __init__.py
|-- m1.py
|-- m2
  |-- __init__.py
  |-- m21
    |-- __init__.py
  |-- m22.py

如上建立子模块和目录和文件,所有的py文件中都写入代码print(name)

# 注意观察已经加载的模块、当前名词空间的名词
# import m
# import m.m1
# from m import m1
# from m.m2 import m21
import m.m2.m21

print('-' * 30)
print(dir())

print('-' * 30)
import sys
# print(sys.modules.keys())
print(sorted(filter(lambda x:x.startswith('m'), sys.modules.keys())))

# 执行结果
# m
# m.m2
# m.m2.m21
# ------------------------------
# ['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'm']
# ------------------------------
# ['m', 'm.m2', 'm.m2.m21', 'marshal']

删除__init__.py文件,并不影响导入,但不建议这样操作。

模块和包的总结

包能够更好的组织模块,尤其是大的模块,其代码行数很多,可以把它拆分成很多子模块,便于使用某些功能就加载相应的子模块

包目录中__init__.py是在包第一次导入的时候就会执行,内容可以为空,也可以是用于该包初始化工作的代码,不要删除(低版本不可删除__init__.py文件)

导入子模块一定会加载父模块,但导入父模块一定不会导入子模块

包目录之间只能使用.点号作为间隔符,表示模块及其子模块的层级关系

模块也是封装,如同类、函数,不过它能够封装变量、类、函数

模块就是命名空间,其内部的顶层标识符,都是它的属性,可以通过__dict__或dir(module)查看

包也是模块,但模块不一定是包,包是特殊的模块,是一种阻止方式,它包含__path__属性

范例:

  • from json import encoder 之后,json.dump 函数用不了,为什么?

    encoder是进入到名词空间中的,没有json,但是json模块已经加载过了,没有json的引用,无法使用dump函数。

  • import json.encoder之后呢?json.dump函数能用吗?

    说明encoder一定是模块,会加载json和encoder2个模块,当前名词空间有json,所以json.dump也能用。

绝对导入、相对导入

绝对导入
  • 在import语句或者from导入模块,模块名称最前面不是以.点开头的
  • 绝对导入总是去模块搜索路径中找
相对导入
  • 只在包内使用,且只能用在from语句中
  • 使用.点号,表示当前目录内
  • ..两点表示上一级目录
  • …三点表示上上一级
  • 只在包内使用,一般 不要在顶层模块中使用相对导入
  • 一旦一个模块中使用相对导入,就可以做主模块运行了

范例:a.b.c模块,a、b是目录,c是模块c.py

#c的代码中,使用以下代码
from . import d # imports a.b.d
from .. import e # imports a.e
from .d import x # a.b.d.x
from ..e import x # a.e.x

使用下面结构的包,体会相对导入的使用

m
|-- __init__.py
|-- m1.py
|-- m2
    |-- __init__.py
    |-- m21
        |-- __init__.py
    |-- m22.py

访问控制

下划线开头的模块名
  • _或者__开头的模块都可以导入,因为他们都是合法的标识符,就可以用作模块名
模块内的标识符

范例:创建文件名为_xyz.py或者__xyz.py测试

# xyz.py
print(__name__)
A = 5
_B = 6
__C = 7
__my__ = 8

# abc.py
import xyz

import sys
print(sorted(sys.modules.keys())) # [..., 'xyz']
print(dir()) # [..., 'xyz']

print(xyz.A, xyz._B, xyz.__C, xyz.__my__) # 5 6 7 8 

普通变量、保护变量、私有变量、特殊变量,都没有被隐藏,也就是说模块内没有私有的变量,在模块中定义不做特殊处理

from xyz import A, _B as B, __my__, __C as C

import sys
print(sorted(sys.modules.keys()))
print(dir())

print(A, B, C, __my__)

使用from语句依然可以访问所有变量

from … import * 和__all__
使用from … import * 导入
# xyz.py
print(__name__)
A = 5
_B = 6
__C = 7
__my__ = 8

# abc.py
from xyz import *

import sys
print(sorted(sys.modules.keys()))
print(dir()) # [..., 'A']
print(locals()['A']) # 5

A = 55
print(locals()['A']) # 55

结果是只导入了A,下划线开关的都没有导入

使用__all__

__all__是一个列表,元素是字符串,每一个元素都是一个模块内的变量名。

# xyz.py
__all__ = ["X", "Y"]

print(__name__)
A = 5
_B = 6
__C = 7

__my__ = 8

X = 10
Y = 20

# abc.py
from xyz import *

import sys
print(sorted(sys.modules.keys()))
print(dir()) # [...,'X', 'Y']

print(locals()['X']) # 10

修改__all__列表,加入下划线开头变量

# xyz.py
__all__ = ["X", "Y", "_B", "__C"]

print(__name__)
A = 5
_B = 6
__C = 7

__my__ = 8

X = 10
Y = 20

# abc.py
from xyz import *

import sys
print(sorted(sys.modules.keys()))
print(dir()) # [..., 'X', 'Y', '_B', '__C']

print(locals()['X']) # 10
print(locals()['Y']) # 20
print(locals()['_B']) # 6
print(locals()['__C']) # 7

可以看到使用from xyz import * 导入__all__列表中的名称

包和子模块
m 
|-- __init__.py
|-- m1.py
# __init__.py
print(__name__)
x = 1

# m1.py中
print(__name__)
y = 5

test.py如何访问到m1.py中的变量y

  • 访问到m.m1变量y的几种实现:
# 方法1,直接导入m1模块
import m.m1
print(m.m1.y)

# 方法2,直接导入m.m1的属性y
from m.m1 import y
print(y)
from m import m1
print(m1.y)

# 方法3, from m import *
# print(dir())
# 该方法导入后,无法看到子模块m1,无法访问y
# 在__init__.py增加__all_- = ['X','m1'],使用__all__提供导出的名称
from m import *
print(m1.y)

# 方法4,不适用__all__
# 在__init__.py增加from . import m1
from m import *
print(m1.y)
#__init__.py
print(__name__)
from .m1 import y #m1 y
x = 1
print(dir())

#test.py
import m

print(m.m1.y)

#返回结果
# m
# m.m1
# ['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'm1', 'x', 'y']
# 5
总结

使用from xyz import *导入

  1. 如果模块没有__all__,from xyz import *只导入非下划线开头的该模块的变量。如果是包,子模块也不会导入,除非在__all__中设置,或__init__.py中导入它们
  2. 如果模块有__all__,from xyz import *只导入__all__列表中指定的名称,哪怕这个名词是下划线开头的,或者是子模块
  3. from xyz import *方式导入,使用简单,但是其副作用是导入大量不需要使 用的变量,甚至有可能造成名词的冲突。而__all__可以控制被导入模块在这 种导入方式下能够提供的变量名词,就是为了复制from xyz import *导入过 多的模块变量,从而避免冲突。因此,编写模块时,应该尽量加入__all__

from module import name1,name2导入

  • 这种方式打导入是明确的,哪怕是导入子模块,或者导入下划线开头的名称,可以有控制的导入名称和其对应的对象

模块变量的修改

# xyz.py
print(__name__)
X = 10

# test2.py
import xyz

print(xyz.X)

# test.py
import xyz

print(xyz.X)
xyz.X = 50

import test2

模块对象是同一个,因此模块的变量也是同一个,对模块变量的修改,会影响所有使用者

  • 建议不要修改模块的变量
  • 可以通过前面介绍的猴子补丁,也可以通过打补丁的方式,修改模块的变量、类、函数等的内容。

包管理

为什么使用包管理

Python的模块或者源文件直接可以复制到目标项目目录中,就可以导入使用了。

但是为了更多项目调用使用,或者共享给别人,就需要打包,或发布到网络,以便供人使用。

目的也是为了复用。

Pypi(Python Package Index),公共的模块存储中心, https://pypi.python.org/pypi

主要工具

distutils

官方库distutils,使用安装脚本setup.py 来构建、安装包。

从1998年就是标准库的一部分,直到2000年停止开发。

setuptools

它是替代distutils的增强版工具集,包含esay_install工具,使用ez_setup.py文件。支持egg格式的构建和安装。

提供查询、下载、安装、构建、发布、管理等包管理功能。

setuptools是包管理的核心模块。

后来,setuptools开发缓慢了,出现基于setuptools的distribute来替代 setuptools。2013年,这两个项目重新合并,distribute被废弃,setuptools依 然是Python安装打包的标准方式。

set.py创建一个源代码分发包的例子 可以再帮助文档chm上搜索索引setup,点击最上面的Distributing Python Modules(Legacy version),然后选择Writing the Setup Script,参照例子

from distutils.core import setup

setup(name='Distutils',
      version='1.0',
      description='Python Distribution Utilities',
      author='Greg Ward',
      author_email='[email protected]',
      url='https://www.python.org/sigs/distutils-sig/',
      packages=['distutils', 'distutils.command'],
     )
name 名字
version 版本
description 描述信息
author 作者
author_email 作者邮箱
url 包的主页,可以不写
packages = [] 打包列表
packages=[‘distutils’]
指定distutils,就会把distutils所有的非目录子模块打包
packages=[‘distutils’, ‘distutils.command’]
逐级建立目录,但是只把distutils的所有非目录子模块打包,把distutils.command打包
pip

pip目前包管理的事实标准。

构建在setuptools之上,替代easy_install的。同样提供丰富的包管理功能

Python3.4之前,需要单独安装,从Python3.4开始直接包含在安装文件中

wheel

wheel格式定义在PEP427中。

wheel是zip打包的扩展名为.whl的文件,文件中不包含.pyc文件.

提供bdist_wheel作为setuptools的扩展命令,这个命令可以用来生成新打包格式wheel。

pip从1.4版本开始提供了一个wheel子命令来安装wheel包。当然,需要先安装wheel模块。

它可以让Python库以二进制形式安装,而不需要再本地编译

https://packaging.python.org/

https://packaging.python.org/en/latest/tutorials/packaging-projects/

包制作

准备构建包的文件

文件目录结构

 packaging_tutorial/
├── LICENSE
├── pyproject.toml
├── README.md
├── src/
│   └── example_package_YOUR_USERNAME_HERE/
│       ├── __init__.py
│       └── example.py
└── tests/

说明:

  • packaging_tutorial 可以是任意的目录名
  • pyproject.toml 是打包配置文件
  • src 里面存放的就是你的程序代码
  • example_package_YOUR_USERNAME_HERE 需要改成你期望发布的模块包名(官方建议是在包名后面加上自己的用户名,防止和现有库名重复)
  • LICENSE 是许可信息
  • README.md 就是 readme 文件。
  • tests 存放测试文件的目录

build-system 选择构建后端,支持 Hatchling(默认)、setuptools、Flit、PDM

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

pyproject.toml

[project]
name = "example_package_YOUR_USERNAME_HERE"
version = "0.0.1"
authors = [
  { name="Example Author", email="[email protected]" },
]
description = "A small example package"
readme = "README.md"
requires-python = ">=3.8"
classifiers = [
    "Programming Language :: Python :: 3",
    "Operating System :: OS Independent",
]
license = "MIT"
license-files = ["LICEN[CS]E*"]

[project.urls]
Homepage = "https://github.com/pypa/sampleproject"
Issues = "https://github.com/pypa/sampleproject/issues"

说明:

  • name 是软件包的分配名称。这可以是任何名称,只要它只包含字母、数字、.、_ 和 - 。
  • version 是包的版本。
  • authors 用于标识包的作者;您可以为每个作者指定一个姓名和一个电子邮件。您还可以以相同的格式列出维护员。
  • description 是包的简短单句摘要。
  • readme 是包含包详细描述的文件的路径。这显示在 PyPI 上的包详细信息页面上。pyproject.toml 指南中还描述了一个更高级的 table 形式。
  • requires-python 提供项目支持的 Python 版本。
  • classifiers 为 index 和 pip 提供了一些关于您的包的其他元数据。

    在这种情况下,该软件包仅与 Python 3 兼容,并且与作系统无关。您应该始终至少包含您的包适用于哪些 Python 版本以及您的包将适用于哪些作系统。有关分类器的完整列表,请参阅 https://pypi.org/classifiers/

  • license 是软件包的 SPDX 许可证表达式。尚不支持所有构建后端。
  • license-files 是许可证文件的 glob 路径列表,相对于 pyproject.toml 所在的目录。尚不支持所有构建后端。
  • urls 允许您列出任意数量的额外链接以在 PyPI 上显示。通常,这可能是源、文档、问题跟踪器等。

其他请参阅 pyproject.toml 指南

构建包

1.安装 build 库

#首先确保您安装了最新版本pip
python -m pip install --upgrade pip
#安装 build 库
python -m pip install --upgrade build

2.打包构建

切换到 pyproject.toml 文件所在目录,运行下面的指令

python -m build

此命令应输出大量文本,完成后应在 dist 目录中生成两个文件:

dist/
├── example_package_YOUR_USERNAME_HERE-0.0.1-py3-none-any.whl
└── example_package_YOUR_USERNAME_HERE-0.0.1.tar.gz

tar.gz 文件是一个源发行版,而 .whl 文件是构建发行版。

上传模块包到PyPI
  1. 注册 PyPI 账号

在 PyPI 上注册一个帐号:https://pypi.org/account/register/。 按提示完成电子邮件地址验证、以及一些安全设置。

2.创建 API Token

访问:https://pypi.org/manage/account/token/

3.创建 .pypirc 文件

在系统 HOME 目录创建 .pypirc 配置文件:

  • Linux : ~/.pypirc
  • Windows: C:\Users\用户名\.pypirc

文件内容如下:

[distutils]
index-servers=pypi

[pypi]
repository = https://upload.pypi.org/legacy/
username = __token__
password = <Token>

其中,<Token> 改成上一步创建的 token(以pypi-开头的)。如果觉得不安全也可以 username 和 password 两行删除,在后面的上传过程中会提示让手动输入

4.安装 twine 库

pip install --upgrade twine

上传模块包 终端里运行下面指令上传 dist 目录的模块包到 PyPI 平台

twine upload dist/*

logging日志处理模块

日志级别

日志级别Level 数值
CRITICAL      #50
ERROR         #40
WARNING       #30,默认级别
INFO          #20
DEBUG         #10
NOTSET        #0

日志级别指的是产生日志的事件的严重程度

设置一个级别后,严重程度低于设置值的日志消息将被忽略

级别定义一般都写在配置文件里

其方法有: debug()、 info()、 warning()、 error() 和 critical()

import logging

logging.basicConfig(level=logging.INFO)

logging.info('test')

# 执行结果
INFO:root:test
import logging

logging.basicConfig(level=logging.INFO)
logging.info('info ~~~~')      #logger默认打印到控制台,info将消息包装成INFO 20
logging.warning('waring ~~~~') #红色,stderr, 30
logging.debug('debug ~~~~')    #10 < 30
logging.error('errror ~~~~')   #40 > 30  

#缺省logger默认格式 消息级别:名字:消息

#返回结果
# INFO:root:info ~~~~
# WARNING:root:waring ~~~~
# ERROR:root:errror ~~~~

格式字符串

%(message)s     #日志消息内容。he logged message, computed as msg % args. 当调用Formatter.format()时设置
%(asctime)s     #asctime。创建LogRecord时的可读时间。默认情况下,它的格式为'2003-07-08 16:49:45,896'(逗号后面的数字是毫秒部分的时间)
%(funcName)s    #函数名。日志调用所在的函数名
%(levelname)s   #日志级别名称。 消息的级别名称'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'
%(levelno)s     #日记级别数值。消息的级别数字 ,对应DEBUG, INFO, WARNING, ERROR, CRITICAL
%(lineno)d      #行号。日志调用所在的源码行号
%(module)s      #模块。  模块(filename的名字部分)
%(process)d     #进程 ID
%(thread)d      #线程 ID
%(processName)s #进程名
%(threadName)s  #线程名字
%(name)s        #logger名字

注意:funcName、threadName、processName都是小驼峰

范例

默认级别
import logging

FORMAT = '%(asctime)-15s\tThread info:%(thread)d %(threadName)s %(message)s'
logging.basicConfig(format=FORMAT)

logging.info('I am {}'.format(20))
logging.warning('I am {}'.format(20))

#返回结果
# 2025-02-23 11:28:55,489       Thread info:10584 MainThread I am 20

由于默认级别是WARNING,所以INFO的结果不显示

构建消息
import logging

FORMAT = '%(asctime)-15s\tThread info:%(thread)d %(threadName)s %(message)s'
logging.basicConfig(format=FORMAT, level=logging.INFO)

logging.info('I am {}'.format(20)) # 单一字符串
logging.warning('I am %d %s', 20, 'years old.') # c风格

#返回结果
# 2025-02-23 11:37:40,339       Thread info:23112 MainThread I am 20
# 2025-02-23 11:37:40,339       Thread info:23112 MainThread I am 20 years old.

上例是基本的使用方法,大多数时候,使用的是info,正常运行信息的输出

日志级别和格式字符串扩展的例子

可插入指定打印字符,使用字典

import logging

FORMAT = '%(asctime)-15s\tThread info:%(thread)d %(threadName)s %(message)s %(hello)s'
logging.basicConfig(format=FORMAT, level=logging.WARNING) # 级别WARNING

d = {'hello': 'world'}
logging.info('I am {}'.format(20), extra = d) # 级别INFO
logging.warning('I am %d %s', 20, 'years old.',extra = d) # 级别WARNING

#返回结果
# 2025-02-23 11:38:17,867       Thread info:7220 MainThread I am 20 years old. world
修改日期格式
import logging

logging.basicConfig(format='%(asctime)s %(message)s', datefmt='%Y/%m/%d %h:%M:%S')
logging.warning('this event was logged.')

#返回结果
# 2025/02/23 Feb:38:47 this event was logged.
输出到文件
import logging

logging.basicConfig(format='%(asctime)s %(message)s', filename='d:/test.log')
logging.warning('this event was logged.')
  • filename设置日志文件
  • filemode设置读写模式。默认a,追加模式,将结果写入文件,文件没有 创建文件
import logging

FORMAT = "%(asctime)s\x01%(thread)s\x01%(threadName)s\x01%(message)s, %(school)s"  #\x01可以用来切段
logging.basicConfig(level=logging.INFO, format=FORMAT, datefmt='%Y年-%m-%d %H:%M:%S',
                    filename='d:/tmp/test.log', filemode='w') #文件默认a,追加模式

d = {'school': 'CN'}
logging.info('I am {}'.format(20), extra=d)
logging.info('I am %d %s', 20, "years old", extra=d)

Logger类

在logging模块中,顶层代码中有

root = RootLogger(WARNING) # 大约在1995行处。指定根Logger对象的默认级别。就在basicConfig函数上面
Logger.root = root # 为类Logger增加类属性root

logging模块加载的时候,会创建一个全局对象root,它是一个RootLogger实例,即root logger。根Logger对象的默认级别是WARNING

RootLogger类继承自Logger类

类Logger初始化方法签名是(name, level=0)

类RootLogger初始化方法签名(level),本质上调用的是 Logger.__init__(self, "root", WARNING)

调用logging.basicConfig来调整级别,就是对这个 根Logger 的级别进行修改

构造

Logger实例的构建,使用Logger类也行,但推荐getLogger函数

Logger.manager = Manager(Logger.root) # 1997行,为Logger类注入一个manager类属性

def getLogger(name=None):
    """
    Return a logger with the specified name, creating it if necessary.

    If no name is specified, return the root logger.
    """
    if not name or isinstance(name, str) and name == root.name:
        return root # 没有指定名称,返回根logger
    return Logger.manager.getLogger(name)

使用工厂方法返回一个Logger实例

指定name,返回一个名称为name的Logger实例。如果再次使用相同的名字,返回同一个实例。背后使用一个字典保证同一个名称返回同一个Logger实例。

未指定name,返回根Logger实例

import logging

a = logging.Logger('hello', 20)
b = logging.Logger('hello', 30)
print(a, id(a))
print(b, id(b))

c = logging.getLogger('hello')
d = logging.getLogger('hello')
print(c, id(c))
print(d, id(d))

# 执行结果
<Logger hello (INFO)> 35484168
<Logger hello (WARNING)> 35484560
<Logger hello (WARNING)> 42988600
<Logger hello (WARNING)> 42988600
层次结构

Logger是层次结构的,使用 . 点号分割,如’a’、‘a.b’或’a.b.c.d’,a是a.b的父parent,a.b是a的子child。对于foo来说,名字为foo.bar、foo.bar.baz、foo.bam都是 foo的后代

import logging

root = logging.root
print(root.name, id(root), type(root), root.parent) #根没有父。 root 2162635612784 <class 'logging.RootLogger'> None

log1 = logging.getLogger(__name__) #__main__ 模块级logger, 在当前模块中都可以使用log1
print(log1.name, id(log1), type(log1), log1.parent, id(log1.parent)) #parent 指向 root
# __main__ 2162630622384 <class 'logging.Logger'> <RootLogger root (WARNING)> 2162635612784

log2 = logging.getLogger('hello') #parent 也是 root
print(log2.name, log2.parent, id(log2.parent)) #hello <RootLogger root (WARNING)> 2162635612784

#有父子关系
log3 = logging.getLogger(__name__ + '.child') #child可任意字符串.
print(log3.name, log3.parent, id(log3.parent)) # __main__.child <Logger __main__ (WARNING)> 2162630622384
import logging

root = logging.root
print(root.name, id(root), type(root), root.parent) #根没有父。

log1 = logging.getLogger('s') #__main__ 模块级logger, 在当前模块中都可以使用log1
print(log1.name, id(log1), type(log1), log1.parent, id(log1.parent)) #parent 指向 root

log2 = logging.getLogger('s.s1')
print(log2.name, id(log2), log2.parent, id(log2.parent))

#有父子关系
log3 = logging.getLogger('s.s1.s11')
print(log3.name, log3.parent, id(log3.parent))

#返回结果 父子孙子
# : root 1888229155104 <class 'logging.RootLogger'> None
# : s 1888224898624 <class 'logging.Logger'> <RootLogger root (WARNING)> 1888229155104
# : s.s1 1888227152016 <Logger s (WARNING)> 1888224898624
# : s.s1.s11 <Logger s.s1 (WARNING)> 1888227152016
Level级别设置

每一个logger实例都有级别

import logging

FORMAT = '%(asctime)-15s\tThread info: %(thread)d %(threadName)s [%(message)s]'
logging.basicConfig(format=FORMAT, level=logging.INFO)

log1 = logging.getLogger(__name__)
print(log1.name, type(log1), log1.level) # 新的logger实例的level是什么 #__main__ <class 'logging.Logger'> 0
log1.info('1 info')
print(log1.getEffectiveLevel()) # 等效级别,从哪里来的 #20

log1.setLevel(28) # 设置logger是level
print(log1.getEffectiveLevel(), log1.level) #28 28
log1.info('2 info')

log1.setLevel(42)
log1.warning('3 warning')
log1.error('4 error')
log1.critical('5 critical')

root = logging.getLogger() #根logger
root.info('6 root info') #不影响

#返回结果
# 2025-02-24 23:20:34,215       Thread info: 20352 MainThread [1 info]
# 2025-02-24 23:20:34,215       Thread info: 20352 MainThread [5 critical]
# 2025-02-24 23:20:34,215       Thread info: 20352 MainThread [6 root info]
# __main__ <class 'logging.Logger'> 0
# 20
# 28 28

每一个logger实例,都有一个等效的level。

logger对象可以在创建后动态的修改自己的level。

等效level决定着logger实例能输出什么级别信息。

注意:推荐使用setLevel方法修改级别

Handler

Handler 控制日志信息的输出目的地,可以是控制台、文件。

  • 可以单独设置level
  • 可以单独设置格式
  • 可以设置过滤器

Handler类层次

  • Handler
    • StreamHandler # 不指定使用sys.stderr
      • FileHandler # 文件
      • _StderrHandler # 标准输出
    • NullHandler # 什么都不做

日志输出其实是Handler做的,也就是真正干活的是Handler

在logging.basicConfig函数中,如下:

if handlers is None:
    filename = kwargs.pop("filename", None)
    mode = kwargs.pop("filemode", 'a')
    if filename:
        h = FileHandler(filename, mode)
    else:
        stream = kwargs.pop("stream", None)
        h = StreamHandler(stream)
    handlers = [h]

如果设置文件名,则为根Logger加一个输出到文件的FileHandler;如果没有设置文件名,则为根

Logger加一个StreamHandler,默认输出到sys.stderr

也就是说,根logger一定会至少有一个handler的。

Handler的初始的level

范例:同时输出到多个位置

import logging

FORMAT = '%(asctime)s %(name)s %(message)s'
logging.basicConfig(format=FORMAT, level=logging.INFO)

logger = logging.getLogger('test')
print(logger.name, type(logger))

logging.info('line 1')

handler = logging.FileHandler('d:/tmp/l1.log', 'w') # 创建handler
logger.addHandler(handler) # 给logger对象绑定一个handler

logger.info('line 2')

#控制台输出
# test <class 'logging.Logger'>
# 2025-02-25 00:31:16,225 root line 1
# 2025-02-25 00:31:16,226 test line 2

#l1.log文件内容
# line 2

Handler 的初始的level是 0

日志流
Level的继承
import logging

logging.basicConfig(format="%(asctime)s %(name)s [%(message)s]")

log1 = logging.getLogger('s')
print(log1.name, log1.level, log1.getEffectiveLevel())
log1.info('1 info')

log2 = logging.getLogger('s.s1')
print(log2.name, log2.level, log2.getEffectiveLevel())
log2.info('2 info')
print('-' * 30)

log1.setLevel(20)
log1.info('3 info')
print(log1.name, log1.level, log1.getEffectiveLevel())
print(log2.name, log2.level, log2.getEffectiveLevel())
log2.info('4 info')

log2.setLevel(30)
print(log2.name, log2.level, log2.getEffectiveLevel())
log2.info('5 info')

#返回结果
# s 0 30
# s.s1 0 30
# ------------------------------
# 2025-02-26 14:27:35,816 s [3 info]
# s 20 20
# s.s1 0 20
# 2025-02-26 14:27:35,816 s.s1 [4 info]
# s.s1 30 30

logger实例

  • 如果不设置level,则初始level为0
  • 如果设置了level,就优先使用自己的level,否则,继承最近的祖先的level
  • 信息是否能够从该logger实例上输出,就是要看当前函数的level是否大于等于logger的有效level,否则输出不了
继承关系及信息传递
  • 每一个Logger实例的level如同入口,让水流进来,如果这个门槛太高,信息 就进不来。例如log3.warning('log3'),如果log3定义的级别高,就不会有信 息通过log3
  • 如果level没有设置,就用父logger的,如果父logger的level没有设置,继续 找父的父的,最终可以找到root上,如果root设置了就用它的,如果root没有 设置,root的默认值是WARNING
  • 消息传递流程
    1. 如果消息在某一个logger对象上产生,这个logger就是 当前logger ,首 先消息level要和当前logger的EffectiveLevel比较,如果低于当前logger 的EffectiveLevel,则流程结束;否则生成log记录
    2. 日志记录会交给当前logger的所有handler处理,记录还要和每一个handler的级别分别比较,低的不处理,否则按照handler输出日志记录
    3. 当前logger的所有handler处理完后,就要看自己的propagate属性,如果是True表示向父logger传递这个日志记录,否则到此流程结束
    4. 如果日志记录传递到了父logger,不需要和父logger的level比较,而是直 接交给父的所有handler,父logger成为当前logger。重复2、3步骤,直到 当前logger的父logger是None退出,也就是说当前logger最后一般是root logger(是否能到root logger要看中间的logger是否允许propagate)
  • logger实例初始的propagate属性为True,即允许向父logger传递消息
  • logging.basicConfig函数
    • 如果root没有handler,就默认创建一个StreamHandler,如果设置了 filename,就创建一个FileHandler
    • 如果设置了format参数,就会用它生成一个Formatter对象,否则会生成缺 省Formatter,并把这个formatter加入到刚才创建的handler上,然后把这 些handler加入到root.handlers列表上。level是设置给root logger的
    • 如果root.handlers列表不为空,logging.basicConfig的调用什么都不做

官方日志流转图

img_20250226_150759.png

范例:

import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)-10s [%(message)s]")

# 根logger操作
root = logging.getLogger()
root.setLevel(logging.ERROR) 
print('root', root.handlers) #root [<StreamHandler <stderr> (NOTSET)>]

# 增加handler
import sys
h0 = logging.StreamHandler(sys.stdout)
h0.setLevel(logging.WARNING)
root.addHandler(h0)
print('root', root.handlers)  #root [<StreamHandler <stderr> (NOTSET)>, <StreamHandler <stdout> (WARNING)>]

for h in root.handlers:
    print('root handler = {}, formattrt = {}'.format(
        h, h.formatter._fmt if h.formatter else ''))
# root handler = <StreamHandler <stderr> (NOTSET)>, formattrt = %(asctime)s %(name)-10s [%(message)s]      
# root handler = <StreamHandler <stdout> (WARNING)>, formattrt =

logging.error('root test~~~~~')

# logger s
log1 = logging.getLogger('s')
log1.setLevel(logging.ERROR)
h1 = logging.FileHandler('d:/test.log')
h1.setLevel(logging.WARNING)
log1.addHandler(h1)
print(log1.name, log1.handlers)
log1.warning('s test ------') # 打印不了,但是创建了文件
print('-' * 30)

# logger s.s1
log2 = logging.getLogger('s.s1')
log2.setLevel(logging.CRITICAL)
h2 = logging.FileHandler('d:/test.log')
h2.setLevel(logging.WARNING)
log2.addHandler(h2)
print(log2.name, log2.handlers)
log2.error('s.s1 test ====') #当前级别CRITICAL不可打印,文件等级WARNING,error>warning可创建文件,但当前logger级别为CRITICAL,不可打印
print('-' * 30)

# logger s.s1.s2
log3 = logging.getLogger('s.s1.s2')
log3.setLevel(logging.INFO)# 级别最低
print(log3.getEffectiveLevel())
log3.info('s.s1.s2 test info ++++')  #可打印。因为log3.propagate默认为True,传递给父作为当前logger比较
print(log3.name, log3.handlers)

#返回结果
# root [<StreamHandler <stderr> (NOTSET)>]
# root [<StreamHandler <stderr> (NOTSET)>, <StreamHandler <stdout> (WARNING)>]
# root handler = <StreamHandler <stderr> (NOTSET)>, formattrt = %(asctime)s %(name)-10s [%(message)s]
# root handler = <StreamHandler <stdout> (WARNING)>, formattrt =
# 2025-02-26 16:50:43,485 root       [root test~~~~~]
# root test~~~~~
# s [<FileHandler d:\test.log (WARNING)>]
# ------------------------------
# s.s1 [<FileHandler d:\test.log (WARNING)>]
# ------------------------------
# 20
# 2025-02-26 16:50:43,488 s.s1.s2    [s.s1.s2 test info ++++]
# s.s1.s2 []
Formatter

logging的Formatter类,它允许指定某个格式的字符串。如果提供None,那么'%(message)s'将会作为默认值

范例:

import logging
import sys

FORMATSTR = "%(asctime)s %(thread)s %(name)-10s [%(message)s]"
logging.basicConfig(level=logging.INFO, 
                    datefmt="%Y-%m-%d %H:%M:%S", format=FORMATSTR)

root = logging.getLogger()
hroot = logging.StreamHandler(sys.stdout)
root.addHandler(hroot)
for h in root.handlers:
    print('root handler = {}, formattrt = {}'.format(
        h, h.formatter._fmt if h.formatter else '空格式串'))
#print(log1.handlers[0].formatter)

logging.info('root info ~~~~')

#开始设置hroot formatter
f1 = logging.Formatter(fmt='++ %(asctime)s [%(message)s] ++')
hroot.setFormatter(f1)

logging.info('test string info')

#返回结果
# root handler = <StreamHandler <stderr> (NOTSET)>, formattrt = %(asctime)s %(thread)s %(name)-10s [%(message)s]
# root handler = <StreamHandler <stdout> (NOTSET)>, formattrt = 空格式串
# 2025-02-26 17:44:24 20476 root       [root info ~~~~]
# root info ~~~~
# 2025-02-26 17:44:24 20476 root       [test string info]
# ++ 2025-02-26 17:44:24,136 [test string info] ++

范例:

import logging
import sys

FORMATSTR = "%(asctime)s\t%(name)s\t%(thread)s,%(message)s"

logging.basicConfig(format=FORMATSTR, datefmt="%Y/%m/%d %H:%M:%S", level=logging.INFO)

log1 = logging.getLogger('log1')
log1.propagate = True

h = logging.FileHandler('d:/tmp/{}.log'.format(__name__))
log1.addHandler(h)
print(log1.handlers[0].formatter)

f = logging.Formatter('*** %(message)s ***')
h.setFormatter(f)
print(log1.handlers[0].formatter._fmt)

h.setLevel(logging.WARNING)

log1.info('log1 test info string') # %(message)s

#返回结果
# None
# *** %(message)s ***
# 2025/02/26 18:04:24     log1    27980,log1 test info string

import logging

修改颜色为黑色, 在logging.basicConfig()增加一句stream=sys.stdout

logging.basicConfig(format=FORMATSTR, datefmt="%Y/%m/%d %H:%M:%S", level=logging.INFO,stream=sys.stdout)
import logging
import sys

FORMATSTR = "root\t[%(name)s]\t%(thread)s,%(message)s"

logging.basicConfig(format=FORMATSTR,level=logging.INFO,stream=sys.stdout)

log1 = logging.getLogger('log1')
log1.propagate = True

h = logging.StreamHandler(sys.stdout)
log1.addHandler(h)

f = logging.Formatter('log1\t[%(name)s]\t%(message)s')
h.setFormatter(f)

log2 = logging.getLogger('log1.log2')

log2.info('log2 test info string') # %(message)s

# 执行结果
# log1    [log1.log2]     log2 test info string
# root    [log1.log2]     8032,log2 test info string
Filter

Filter分为2种:

  • logger的过滤器,数据必须通过此过滤器,才能交给handlers处理

可以为handler增加过滤器,所以这种过滤器只影响某一个handler,不会影响整个处理流程 如果过滤器增加到logger上,就会影响流程 filter如果为空 恒等True

import logging

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)-19s [%(message)s]")

log1 = logging.getLogger('s')
print(log1.level, log1.getEffectiveLevel()) #继承

log1.info('log1 info test1') #通过

# 增加Filter
filter = logging.Filter('x') # 过滤器
log1.addFilter(filter) #增加过滤器后,消息不能通过
log1.info('log1 info test2') #不能通过
  • 可以为handler增加过滤器,所以这种过滤器只影响某一个handler,不会影响整个处理流程
import logging

FORMAT = "%(asctime)s %(name)-19s [%(message)s]"
logging.basicConfig(level=logging.INFO, format=FORMAT)

log1 = logging.getLogger('s')
print(log1.level, log1.getEffectiveLevel()) #继承
log1.addFilter(logging.Filter('x'))
log1.info('log1 info test1') #不通过

log2 = logging.getLogger('s.s1')
print(log2.level, log1.getEffectiveLevel())

h1 = logging.StreamHandler()
f1 = logging.Formatter('** %(message)s **')
h1.setFormatter(f1)
h2 = logging.StreamHandler()
f2 = logging.Formatter('++ %(message)s ++')
h2.setFormatter(f2)
print(h1.level, h2.level)
h2.addFilter(logging.Filter('s'))

log2.addHandler(h1)
log2.addHandler(h2)

log2.info('log2 info test string') #交给h1, h2并向上传播

#返回结果
# 0 20
# 0 20
# 0 0
# ** log2 info test string **
# ++ log2 info test string ++
# 2025-02-27 00:15:03,580 s.s1                [log2 info test string]

过滤器核心源代码, 在logging.Filter类的filter方法中

class Filter(object):
    def __init__(self, name=''):
        self.name = name
        self.nlen = len(name)

    def filter(self, record):
        if self.nlen == 0:
            return True
        elif self.name == record.name:
            return True
        elif record.name.find(self.name, 0, self.nlen) != 0:
            return False
        return (record.name[self.nlen] == ".")

record.name.find(self.name, 0, self.nlen) != 0本质上就是等价于record.name.startswith(filter.name)

如果上面测试通过,还要求return (record.name[self.nlen] == ".",也就是说不但要以filter.name开头,而且下一个字符必须是.点号,否则返回False。

debuuger

来学学debugger吧,不能只会用print调试呀!

cat <<\EOF> t.py
import inspect

def g(data):
    return data * data

def f(x):
    lst = []
    for i in range(x):
        frame = inspect.currentframe()
        val = g(i)
        lst.append(val)
    retrun lst

f(3)
EOF

python -m pdb t.py

# 断点
# 方法1-源代码中加 breakpoint()
# 方法2
# b 行号
# b 代码名称 。 b inspect.currentfram
  • 常用调试命令
name function
break 新增断点, break main, break 12(行号)
info 查看断点或者局部变量信息 info breakpoints, info locals
run 开始调试
next 类似 step over
step 跳转到函数内部
continue 继续运行到下一个断点
quit 退出调试
watch 内存断点
display 类似 IDE 里面的 watch 功能
break 11 if xxx 条件断点
print 变量名 打印
list 显示源代码 l . , ll
where 查看调用栈
up 上一层
down 下一层
retval 返回值
until 运行直到比现在的行数大为止 until 10
return 运行到返回为止 return lst
clear 删除所有断点。删一个断点 clear 1
emacs

Emacs

org-mode

Orgmode

Donations

打赏

Copyright

© 2025 Jasper Hsu

Creative Commons

Creative Commons

Attribute

Attribute

Noncommercial

Noncommercial

Share Alike

Share Alike