侧边栏壁纸
博主头像
ahao

A student who writes the code of Python and Java.

  • 累计撰写 15 篇文章
  • 累计创建 22 个标签
  • 累计收到 12 条评论

【Python学习】从基础用法近阶高级用法

ahao
2022-05-07 / 0 评论 / 1 点赞 / 24 阅读 / 15,418 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2022-05-15,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

一、序列构成的数组

内置序列类型

Python中内置的序列类型如下

容器序列:list,tuple,collection.deque。

扁平序列:str,bytes,bytearray,memoryview,array.array。

注意

容器序列:可以存放不同类型的数据,存放的是所包含的任意类型的对象的引用。

扁平序列:是一段连续的内存空间,只能存放值而不能是引用,如字符,字节,数值这种基本类型。

序列类型还可以按照能否被修改来分类

可变序列:list,bytearray,collection.deque,memoryview。

不可变序列:tuple,str,bytes。

列表推导式及生成器表达式

列表推导式是构建列表的快捷方式(只能用于生成列表),生成器表达式可以用来创建其他任何类型的序列。

列表推导式的使用

首先看代码清单1-1,这是一个很常见的代码,其功能是把一个字符串变成 Unicode 码位的列表,主要流程是使用for循环遍历字符串symbols,把每个字符转换为Unicode 码位,并追加到codes列表中。

代码清单1-1
symbols = '$¢£¥€¤'
codes=[]
for symbol in symbols:
    codes.append(ord(symbol))
print(codes)  # [36, 162, 163, 165, 8364, 164]

然后再看代码清单1-2,使用列表推导式以后,明显代码逻辑更加清晰。

代码清单1-2
symbols = '$¢£¥€¤'
codes = [ord(symbol) for symbol in symbols]
printf(codes)  # [36, 162, 163, 165, 8364, 164]

当然,在使用列表推导式的过程中需要遵循一些原则:只用列表推导式来创建新的列表,并且尽量保持简短。如果列表推导式的代码超过两行,就要考虑是否需要用for循环重写。

注意:在 { }, [ ] 和 ( )中的换行会被Python自动忽略,因此无需添加换行符 \。

使用列表推导式计算笛卡尔积

首先提出一个示例,一个音乐培训机构有2名老师,3名学生,现有一个音乐节目,需要一名老师和一名学生组合共同参加,问有多少种老师学生组合方式。这其实是一个笛卡尔积的计算过程,实现代码如代码清单1-3所示,使用列表推导式即可完成。

代码清单1-3
teachers = ['陈奕迅', '周杰伦']
students = ['罗翔', 'ahao', '张三']
result = [(teacher, student) for teacher in teachers for student in students]
print(result)  
# [('陈奕迅', '罗翔'), ('陈奕迅', 'ahao'), ('陈奕迅', '张三'), ('周杰伦', '罗翔'), ('周杰伦', 'ahao'), ('周杰伦', '张三')]

代码清单1-4未使用列表推导式,使用常规嵌套for循环完成,明显看出列表推导式更加简洁。

代码清单1-4
teachers = ['陈奕迅', '周杰伦']
students = ['罗翔', 'ahao', '张三']
result = []
for teacher in teachers:
    for student in students:
        result.append((teacher, student))
print(result)  
# [('陈奕迅', '罗翔'), ('陈奕迅', 'ahao'), ('陈奕迅', '张三'), ('周杰伦', '罗翔'), ('周杰伦', 'ahao'), ('周杰伦', '张三')]
生成器表达式的使用

生成器表达式相比列表推导式有一个突出的优点,生成器表达式遵循了迭代器协议,可以逐个地产出元素,而不是先建立一个完整的列表,然后再把这个列表传递到某个构造函数里。

生成器表达式跟列表推导式差不多,只是把方括号[] 换成了 圆括号()。

代码清单1-5用生成器表达式初始化元组和数组,需要注意,如果生成器表达式是一个函数调用过程中的唯一参数,那么不需要额外再用括号把它围起来。如代码清单1-5第3行。如果不是唯一参数就需要使用括号把它围起来,如第8行。

代码清单1-5
symbols = '$¢£¥€¤'
codes_tuple = tuple(ord(symbol) for symbol in symbols)
print(codes_tuple)
# (36, 162, 163, 165, 8364, 164)

import array
arr = array.array('I', (ord(symbol) for symbol in symbols))
print(arr)
# array('I', [36, 162, 163, 165, 8364, 164])

代码清单1-6使用生成器表达式计算音乐培训机构示例。

代码清单1-6
teachers = ['陈奕迅', '周杰伦']
students = ['罗翔', 'ahao', '张三']
for result in ('{0}, {1}'.format(teacher, student) for teacher in teachers for student in students):
    print(result)
    
陈奕迅, 罗翔
陈奕迅, ahao
陈奕迅, 张三
周杰伦, 罗翔
周杰伦, ahao
周杰伦, 张三

代码清单1-6与代码清单1-3不同的是,用生成器表达式之后,内存里不会留下一个有6个组合的列表,因为生成器表达式会在每次for循环运行时才生成下一个组合。如果要计算两个各有1000个元素的列表的笛卡尔积,生成器表达式就可以节省for循环带来的内存开销,即一个含有100万个元素的列表。

元组

元组与列表非常类似,但元组初始化以后无法改变,即没有增删元素功能。

元组和记录

元组可以用于没有字段名的记录,元组中的每个元素都存放了记录中一个字段的数据,外加这个字段的位置,由于这个位置信息,数据才具有了意义。

代码清单1-7是元组用于记录的示例。第3行是成都市的一些信息:市名,年份,人口(单位:万),人口变化(单位:百分比)和面积(单位:平方公里)。第4-6行定义了一个元组列表,元组的形式为 (城市名,城市代码)。在迭代过程中traveler变量被绑定到每个元组上,并且元组的每一个元素自动与%s对应。第12-13行是拆包(unpacking)过程,即提取元组元素的过程。在拆包过程中使用了_占位符,表示缺省,因为我们这里只需要城市名。在进行拆包的时候,可能不是对所有数据都感兴趣,可能只需要其中部分数据,因此使用占位符的方式特别有用。

代码清单1-7
city, year, pop, chg, area = ('ChengDu', 2020, 1500.07, 1.63, 4335)

traveler_ids = [("CD", "028"), ("Zunyi", "0852"), ("Guiyang", "0851")]
for traveler in traveler_ids:
    print("%s,%s" % traveler)

# CD,028
# Zunyi,0852
# Guiyang,0951

for city, _ in traveler_ids:
    print("%s" % city)
# CD
# Zunyi
# Guiyang
元组拆包

在代码清单1-8中,把元组('ChengDu', 2020, 1500.07, 1.63, 4335)里的元素分别赋值给变量city,year,pop,chg,area。可以看到只用了一个等号即完成赋值过程。遍历traveler_ids时,traveler元组里的元素自动对应到了print函数的格式字符串中。以上两个都是元组拆包的应用。

思考:Python中的元组拆包,与Java中的装箱拆箱比较。

Java装箱拆箱:装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型。

Python元组拆包是将元组中的元素自动与相应的变量对应起来。

img

在C语言和Java语言中,两个变量值的交换是需要使用到中间变量的。代码清单1-8是C语言实现代码两变量值交换的函数。利用了中间变量temp。

代码清单1-8
void switchAB(int *a, int *b){
    int temp=*a;
    *a=*b;
    *b=temp;
}

在Python中只需要一行代码即可解决两变量值交换。代码清单1-9中第四行为a,b两变量值的交换。无需使用中间变量即可完成。

代码清单1-9
a=10
b=20
a, b = b, a
print("a={0}, b={1}".format(a, b))
# a=20, b=10
具名元组

collections.namedtuple 是一个工厂函数,它可以用来构建一个带字段名的元组和一个有名字的类。创建一个具名元组需要两个参数,一个是类名,另一个是类的各个字段的名字。后者可以是由数个字符串组成的可迭代对象,或者是由空格分隔开的字段名组成的字符串。存放在对应字段里的数据要以一串参数的形式传入到构造函数中(注意,元组的构造函数只能接受单一的可迭代对象)。

代码清单1-10中创建了一个具名元组City,其含有四个字段。在初始化一个City时,传递元组为参数时,只能对应其中的一个字段。第4行在初始化成都,(666, 333) 对应 坐标coordinates字段。读取字段数据时,可以通过字段名或者索引实现。

代码清单1-10
import collections
City = collections.namedtuple("City", "name country population coordinates")
chengdu = City('成都', '中国', 39.88, (666, 333))
print(chengdu)  # City(name='成都', country='中国', population=39.88, coordinates=(666, 333))
print(chengdu.country)  # 中国
print(chengdu[1])  # 中国

切片Slice

在Python中,列表(list)、元组(tuple)和字符串(str)这类序列类型都支持切片操作。在切片和区间操作时,是不包含区间范围的最后一个元素。这是因为Python也像大多数语言一样以0作为起始下标。如C,Java等。

这样做有如下好处。

  • 当只有最后一个位置信息时,可以快速看出切片和区间里有几个元素:range(3)和 my_list[:3] 都返回 3 个元素。(索引为0,1,2)
  • 当起止位置信息都可见时,可以快速计算出切片和区间的长度,用后一个数减去第一个下标(stop - start)即可。如my_list[2:8],生成的列表有8-2=6个元素。
image-20220506192608297

可以用 s[a :b :c] 的形式对 s 在 a 和 b 之间以 c 为间隔取值。c 的值还可以为负,负值意味着反向取值。

代码清单1-11
s = 'bicycle'
s1 = s[::3]
print(s1)  # bye
s2 = s[::-1]
print(s2)  # elcycib
s3 = s[::-2]
print(s3)  # eccb

代码清单1-12中使用了切片对象Slice,定义了多个具有名字的切片,如ITEMPRICE,这时使用有名字的切片比用硬编码的数字区间要方便得多。

代码清单1-12
invoice = """
项目编号******数量******单价******金额
1001          5        20        100
1002          3        30        90
1003          6        50        300
1004          1        60        60
"""
ITEMCODE = slice(0, 14)
ITEMNUM = slice(14, 23)
ITEMPRICE = slice(23, 33)
ITEMPTOTAL = slice(33, None)
items = invoice.split('\n')[2:]
for item in items:
    print(item[ITEMPRICE], item[ITEMPTOTAL])

20         100
30         90
50         300
60         60

多维切片和省略

[] 运算符里可以使用以逗号分开的多个索引或者是切片,二维的 numpy.ndarray 就可以用 a[i, j] 这种形式来获取,或是用 a[m:n, k:l]的方式来得到二维切片。如果要得到 a[i, j] 的值,Python 会调用 a.getitem((i, j))。Python 内置的序列类型都是一维的,因此它们只支持单一的索引。

Ellipsis就是省略号(…),省略号(…)就是Ellipsis**。**而Ellipsis是ellipsis类的唯一实例(singleton object),这种唯一实例的模式也称为单例模式(singleton pattern)

代码清单1-13
print(type(...))            # output: <class 'ellipsis'>
print(Ellipsis == ...)      # True
print(...)                  # Ellipsis
Python中的省略号(…)的2个用途

1.函数内部,相当于pass

代码清单1-14
def foo1(): pass
def foo2(): ...

2.numpy中的索引

在numpy中 … 用作多维数组切片的快捷方式。代码清单1-15中,初始化一个2*2*2的三维数组。

arr[..., 0, 0] 等价于arr[:, 0, 0] 保留第一维的所有元素,第二维只保留索引为0的元素,第三维只保留索引为0的元素。

arr[..., 0] 等价于 arr[:, :, 0] 第三维只保留索引为0的元素,其余维度保持不变。

arr[0, ...] 等价于 arr[0, :, :]第一维只保留索引为0的元素,其余维度保持不变。

代码清单1-15
import numpy as np
arr = np.array([ [[0.5, 0.1], [0.8, 0.3]], [[0.4, 0.3], [0.6, 0.6]] ])

**************************************************
print(arr)
[[[ 0.5  0.1]
  [ 0.8  0.3]]

 [[ 0.4  0.3]
  [ 0.6  0.6]]]
**************************************************
print(arr[..., 0, 0])
[ 0.5  0.4]

print(arr[:2, 0, 0])
[ 0.5  0.4]
**************************************************
print(arr[1, 0, 1])
0.3
**************************************************
print(arr[..., 0])
[[ 0.5  0.8]
 [ 0.4  0.6]]
**************************************************
print(arr[0, ...])
[[ 0.5  0.1]
 [ 0.8  0.3]]

省略号和冒号的区别:

  1. ...可以代表任意多维的元素,而每个:只能代表一个维度。
  2. :可以指定代表的维度的区间范围,...不能。
  3. ...只能出现一次,而:可以出现多次,但不能超过矩阵的维度。

由列表组成的列表

列表的乘法操作可以实现把该列表复制几份拼接起来。代码清单1-16中,使用乘法操作,将word列表复制了3份并拼接起来。

代码清单1-16
word = ['a', 'b', 'c']
w = word * 3
print(w)  # ['a', 'b', 'c', 'a', 'b', 'c', 'a', 'b', 'c']
w[2] = 'H'
print(w)  # ['a', 'b', 'H', 'a', 'b', 'c', 'a', 'b', 'c']

列表list是一个容器序列,除了能存放基本数据类型外,还能存放任意类型对象的引用。因此可以在列表中存放列表,即列表嵌套。那能不能也使用乘法的方式实现列表嵌套呢?代码清单1-17中,我们使用乘法的方式实现列表嵌套。但是这样是有一个问题存在的,当修改最内层某一个位置对应的元素时,则其他位置的部分也跟着改变了。如这里修改索引为(1,2)的元素,可以看到索引为(0,2)和(2,2)位置的元素均改变了。这是因为,采用这种方式生成的列表嵌套,内层的每一个列表都是对同一个列表的引用。所以改变一个位置,其余位置均会发生变化。

代码清单1-17
weird_board = [['_'] * 3] * 3
print('weird_board1', weird_board) 
# weird_board1 [['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
weird_board[1][2] = 'X'
print('weird_board2', weird_board)
# weird_board2 [['_', '_', 'X'], ['_', '_', 'X'], ['_', '_', 'X']]

# 以下代码与上面的逻辑等价
row = ['_'] * 3
weird_board = []
for i in range(3):
  weird_board.append(row)

以上实现的并不是我们相应的列表嵌套,那么正确的方式是乘法结合列表推导式实现列表嵌套。代码清单1-18中,使用了这种方式,当改变其中某一个位置的元素时,其余位置的元素不会发生改变。

代码清单1-18
board = [['_'] * 3 for i in range(3)]
print('board1', board)  # board1 [['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
board[1][2] = 'X'
print('board2', board)  # board2 [['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]

# 以下代码与上面的逻辑等价
board = []
for i in range(3):
  row = ['_'] * 3
  board.append(row)

总结

Python列表使用乘法时

  • 对于不可变对象(如数字、字符串)而言是复制值

  • 对可变对象(如列表、字典)而言则是复制引用

因此对于包含可变对象的列表不能使用列表乘法,可使用列表推导式式代替。

序列排序

列表可以使用list.sort()进行就地排序,即不会把原来的列表复制一份。其返回值为None。Python内置函数sorted()与list.sort()刚好相反,它会新建一个列表作为返回值,而不改变原来的列表。

代码清单1-19
t = ['grape', 'raspberry', 'apple', 'banana']

b = sorted(t)
print(t)  # ['grape', 'raspberry', 'apple', 'banana']
print(b)  # ['apple', 'banana', 'grape', 'raspberry']

a = t.sort()
print(a)  # None
print(t)  # ['apple', 'banana', 'grape', 'raspberry']

二、字典和集合

散列

原子不可变数据类型(str、bytes 和数值类型)都是可散列类型。frozenset 也是可散列的,因为根据其定义,frozenset 里只能容纳可散列类型。

对于元组,只有当它所包含的所有元素都是可散列类型,它才是可散列的。

字典

列表推导式

字典推导(dictcomp)可以从任何以键值对作为元素的可迭代对象中构建出字典。

代码清单2-1
city_codes = [('028', '成都'), ('0852', '遵义'), ('0851', '贵阳')]
city_code = {city: code for code, city in city_codes}
print(city_code)  # {'成都': '028', '遵义': '0852', '贵阳': '0851'}
用setdefault处理找不到的键

dict.get 并不是处理找不到的键的最好方法。要更新某个键对应的值的时候,不管使用 _getitem_ 还是 get效率都很低。

代码清单2-2
my_dict.setdefault(key, []).append(new_value)

if key not in my_dict:
  my_dict[key] = []
my_dict[key].append(new_value)
散列表算法

使用my_dict[search_key]读取值时,需要经历以下步骤:

① Python 首先会调用 hash(search_key) 来计算search_key 的散列值,把这个值最低的几位数字当作偏移量,在散列表里查找表元。若找到的表元是空的,则抛出 KeyError 异常。若不为空则执行②。

② 不为空的表元里面有一对 found_key:found_value。python会检验 search_key == found_key 是否为真,如果它们相等的话,就会返回 found_value。如果 search_key 和 found_key 不匹配的话,这种情况称为散列冲突。则执行③。

③ 在散列值中另外再取几位,然后用特殊的方法处理一下,把新得到的数字再当作索引来寻找表元。返回执行②。

image-20220513134316958

从字典中取值;给定一个键,这个算法要么返回一个值,要么抛出 KeyError 异常。

三、函数

函数基本知识点

函数的定义

代码清单3-1中,使用def关键字定义了reverse函数,该函数将word参数进行反转并返回。

代码清单3-1
def reverse(word):
    return word[::-1]
函数注解

Python3中用于为函数声明中的参数和返回值附加元数据,称为函数注解。代码清单3-2中使用了函数注解。add函数中参数的注解可以在 : 之后增加注解表达式。如果参数有默认值,注解放在参数名和 = 号之间。如果想注解返回值,在 ) 和函数声明末尾的 : 之间添加 -> 和一个表达式。

代码清单3-2
def add(source: int, target: int = 80) -> int:
    return target * source

需要注意的两个问题:

  1. Python 不做检查、不做强制、不做验证,什么操作都不做。
  2. 当函数既有默认值参数,又有无默认值参数时,无默认值的参数必须放在有默认值参数的前面。否则会报错。

def add(target: int = 80, source: int) -> int: 是错误的函数定义

位置参数

按照与函数定义中的参数对应的顺序传递参数。

代码清单3-3
def student(firstname, lastname):
     print(firstname, lastname)

student('Geeks', 'Practice')  # Geeks Practice
student('Practice', 'Geeks')  # Practice Geeks
关键字参数

以任意顺序传递参数并按名称指定每个参数,同时参数的数量都必须与函数定义中的参数数量相匹配。

代码清单3-4
def student(firstname, lastname):
     print(firstname, lastname)
     
# 关键字参数
student(firstname ='Geeks', lastname ='Practice')
student(lastname ='Practice', firstname ='Geeks')
不定长参数 (*args,**kwargs)

1.元组:在形参前加一个*,接收不定数量、不定名字实参;

2.字典:在形参前加两个* (**),用于接受关键参数。

代码清单3-5中,tag 函数用于生成 HTML 标签。使用了位置参数,关键字参数和不定长参数。定义函数时若想指定仅限关键字参数,要把它们放到前面有 * 的参数后面。即这里的cls参数。

代码清单3-5
def tag(name, *content, cls=None, **attrs):
    if cls is not None:
        attrs['class'] = cls
    if attrs:
        attr_str = ''.join(' {0}="{1}"'.format(attr, value) for attr, value in sorted(attrs.items()))
    else:
        attr_str = ''
    if content:
        return '\n'.join('<{0}{1}>{2}</{3}>'.format(name, attr_str, c, name) for c in content)
    else:
        return '<{0}{1}/>'.format(name, attr_str)
      
# 第一个参数后面的任意个参数会被 *content 捕获,存入一个元组。
print(tag('br'))  # <br/>
print(tag('p', 'hello'))  # <p>hello</p>
print(tag('p', 'hello', 'world'))  # <p>hello</p> # <p>world</p>

# tag 函数签名中没有明确指定名称的关键字参数会被 **attrs 捕获,存入一个字典。即这里的 id 不是tag函数参数明确指定的关键字参数
print(tag('p', 'hello', id=33))  # <p id="33">hello</p>

# cls 参数只能作为关键字参数传入。
print(tag('p', 'hello', 'world', cls='sidebar'))
# <p class="sidebar">hello</p>
# <p class="sidebar">world</p>

# 调用 tag 函数时,即便不是第一个定位参数,也能作为关键字参数传入。
print(tag(content='testing', name='img'))  # <img content="testing"/>

# cls 参数只能通过关键字参数指定,它一定不会捕获未命名的定位参数

# 在 my_tag 前面加上 **,字典中的所有元素作为单个参数传入,同名键会绑定到对应的具名参数上(关键字参数),余下的则被 **attrs 捕获。
my_tag = {'name': 'img', 'title': 'Sunset Boulevard', 'src': 'sunset.jpg', 'cls': 'framed'}
print(tag(**my_tag))  # <img class="framed" src="sunset.jpg" title="Sunset Boulevard"/>

函数装饰器及闭包

装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)。装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。

代码清单3-6
def deco(func):
    def inner():
        print('running inner()')
    return inner()

@deco
def target():
    print('running target()')
代码清单3-7
@deco
def target():
    print('running target()')
    
等价于

def target():
    print('running target()')

target = deco(target)

装饰器的三大特性:

  1. 能把被装饰的函数替换成其他函数
  2. 装饰器在加载模块时立即执行
  3. 函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时运行。

Java中的注解 与 Python中的函数装饰器 的比较

代码清单3-8中,利用FastAPI (基于Python的高性能web框架) 定义了一个数据接口,将Http请求 GET: /user/getInfo 路由到 getInfo() 函数。路由的实现使用的函数装饰器@app.get()实现。

代码清单3-8
@app.get("/user/getInfo")
def getInfo():
    return {"user": "ahao"}

代码清单1-26中,利用Spring(基于Java的web框架)用注解的方式定义了数据接口。实现的功能与FastAPI实现的一样。

代码清单3-9
@RestController
@RequestMapping("/user")
public class UserController {
  
  @GetMapping("getInfo")
	public R getInfo(){
    ...........
    return R.ok().data("user", "ahao");
}

Python 的装饰器(decorator) 和 Java 的注解(annotation) 是完全不同的概念。Python 的装饰器本质上是返回另一个函数的函数用于装饰函数的时候,作为语法糖,实际上是函数的调用

而在 Java 中,注解其实就是一种特殊的注释,相当于对类、方法等贴一个标签,不会改变代码的行为。但为什么在代码清单3-9中,@ReqeustMapping 和 @GetMapping() 注解实现了 FastAPI 相同的路由机制呢?其实是因为 Spring 框架发现某个方法被注解了,提供对应的功能实现而已。

总结:Java 的注解本身什么都不做,而 Python 的装饰器是函数,会改变被装饰函数的行为;如果想改变 Java 被注解方法的行为,需要另外的代码判断某个方法是否被某个注解(名词)注解(动词),对被注解的方法提供不同的实现。

四、面向对象(类)

类的使用

image-20220515155553746

上图是经典的电商“策略”模式UML类图,图的含义是根据客户的属性或订单中的商品计算折扣。

假如折扣规则如下(假定一个订单一次只能享用一个折扣):

  1. 有 1000 或以上积分的顾客,每个订单享 5% 折扣。

  2. 同一订单中,单个商品的数量达到 20 个或以上,享 10% 折扣。

  3. 订单中的不同商品达到 10 个或以上,享 7% 折扣。

该“策略”模式实现的代码如代码清单4-1所示。

代码清单4-1
from abc import ABC, abstractmethod
from collections import namedtuple

Customer = namedtuple('Customer', 'name fidelity')

class LineItem:
    """
    购物车单个具体产品
    """
    def __init__(self, product, quantity, price):
        self.product = product  # 产品
        self.quantity = quantity  # 数量
        self.price = price  # 单价

    def total(self):
        """
        该产品总价
        """
        return self.price * self.quantity

class Order:
  	"""订单类"""
    def __init__(self, customer, cart, promotion=None):
        self.customer = customer
        self.cart = cart  # 购物车
        self.promotion = promotion  # 策略

    def total(self):
        if not hasattr(self, '__total'):
            self.__total = sum(item.total() for item in self.cart)
        return self.__total

    def due(self):
        if self.promotion is None:
            discount = 0
        else:
            discount = self.promotion.discount(self)  # 调用策略函数 计算折扣
        return self.total() - discount

    def __repr__(self):  # 对象转字符串
        fmt = '<Order total: {:.2f} due : {:.2f}>'
        return fmt.format(self.total(), self.due())

class Promotion(ABC):  # 策略: 抽象基类
    @abstractmethod
    def discount(self, order):
        """返回订单金额,正值"""

class FidelityPromo(Promotion):
    """
    第一个策略:有 1000 或以上积分的顾客,每个订单享 5% 折扣。
    """
    def discount(self, order):
        return order.total() * .05 if order.customer.fidelity >= 1000 else 0

class BulkItemPromo(Promotion):
    """
    第二个策略:单个商品为20个或以上时提供10%折扣
    """
    def discount(self, order):
        discount = 0
        for item in order.cart:
            if item.quanity >= 20:
                discount += item.total() * .1
        return discount

class LargeOrderPromo(Promotion):
    """
    第三个策略:订单中的不同商品达到10个或以上时提供7%折扣
    """
    def discount(self, order):
        distinct_items = [item.product for item in order.cart]
        if len(distinct_items) >= 10:
            return order.total() * .07
        else:
            return 0
0

评论区