WorkShp-Python应用技术底层原理剖析与实现

一份价值500个达不溜的大礼

双色球投注规则:

  • 投注区分为红色球号码区和蓝色球号码区
  • 红色球号码区由1-33共三十三个号码组成
  • 蓝色球号码区由1-16共十六个号码组成
  • 投注号码组成:6个红色球号码 + 1个蓝色球号码

通过这个小程序,了解Python的简洁之美

版本一:

import random

def getWealth():
    balls = []
    n = 6
    while n != 0:
        ball = random.randint(1, 33)
        length = len(balls)
        i = 0
        isExist = False
        while i < length:
            b = balls[i]
            if b == ball:
                isExist = True
                n += 1
                break
            i += 1
        if not isExist:
            balls.append(ball)
        n -= 1

    balls.sort()
    ball = random.randint(1, 16)
    balls.append(ball)

    print(balls)


if __name__ == '__main__':
    getWealth()

简化版

import random

def getWealth():
    balls = []
    n = 6
    while n != 0:
        ball = random.randint(1, 33)
        if ball not in balls:
            balls.append(ball)
            n -= 1

    balls.sort()
    balls.append(random.randint(1, 16))

    print(balls)


if __name__ == '__main__':
    getWealth()

Python 常用知识原理剖析

一、for-in循环与迭代器

思考:

为什么 Python 中的字符串,元组,列表,字典等类型的数据,可以直接使用 for-in 直接进行遍历?
自定义的数据集合是否可以实现呢?

iter() 函数

在使用 for-in 循环对这些类型进行遍历时,for-in 会对被遍历的对象执行 iter() 函数,将被遍历的对象包装成一个迭代器对象。

datas = [1,2,3,4,5]
print(type(datas))    # <class 'list'>

iter_data = iter(datas)
print(type(iter_data))     # <class 'list_iterator'>

next() 函数

此时,for-in 循环会在内部调用 next() 函数,逐一取出迭代器中的元素数据,当取出最后一个元素后,再执行 next(),会抛出 StopIteration 异常停止循环。

在一个对象没有调用 iter() 函数之前,是不能使用 next() 函数的,否则会报类型错误 。

datas = [1,2,3,4,5]
print(type(datas))
# next(datas)  # TypeError: 'list' object is not an iterator

iter_data = iter(datas)
print(type(iter_data))

print(next(iter_data))
print(next(iter_data))
print(next(iter_data))
print(next(iter_data))
print(next(iter_data))
print(next(iter_data)) # StopIteration

for-in 就是通过捕捉 StopIteration 这个异常,判断何时退出循环。

自定义可迭代类型

通过前面的代码可知,只有可迭代的对象才可以使用 for-in,那什么样的数据才算是可迭代的呢?

根据官方文档描述:

迭代器协议的幕后机制,给你的类添加迭代器行为非常容易。
在类中定义一个 iter() 方法来返回一个带有 next() 方法的对象。
如果当前类中已定义了 next(),则 iter() 可以简单地返回 self:

也就是说:

  1. 当在使用 iter() 函数时,实际是调用了类中实现的 __iter__() 魔法方法,只要实现了该方法,就可以将对象放入 for-in 中使用。
  2. 实现 __iter__() 函数,只是获取了使用 for-in 的资格,如果真正能够遍历 ,还要实现另外一个方法
  3. next() 函数在执行时,实际上通过可迭代对象实现的 __next__() 函数来获取数据。

实战:实现一个可迭代类,可以逆序遍历各种容器类型中的元素

实现过程分析:

  1. 该类可以使用 for-in 循环进行逆序遍历,那么就要实现 iter()next() 方法
  2. 字符串,元组,列表因为可以使用下标,可以直接进行遍历,但是字典不运行索引值形式的下标,需要特殊处理。
  3. 逆序遍历,记录元素个数做为实始索引,递减取值,如果个数为 0,则抛出 StopIteration 异常。
  4. 如果是字典类型,需要组合键值对返回数据,其它类型直接返回即可。

代码:

class Reverse:
    """通过实现迭代器,使用for-in逆序遍历数据"""
    def __init__(self, data):
        # 初始保存被遍历的数据
        self.data_dict = None
        self.data = None
        self.index = -1
        # 如果是字典类型,特殊处理
        if type(data) == dict:
            # 保存字典原数据
            self.data_dict = data
            # 默认的key不能使用下标,转换成列表
            self.data = list(data.keys())
        else:
            # 不是字典,直接保存数据
            self.data = data
        # 下标初值为最大元素个数
        self.index = len(self.data)

    # 返回当前迭代器对象
    def __iter__(self):
        return self

    # 实现迭代器对象取数据操作
    def __next__(self):
        # 如果下标为0,抛出异常
        if self.index == 0:
            raise StopIteration
        # 下标递减
        self.index = self.index - 1
        # 通过下标逆序找数据
        data = self.data[self.index]
        # 判断,如果字典不为空,说明遍历的是字典,返回字典元素数据
        if self.data_dict:
            return (data,self.data_dict[data])
        # 不是字典,直接返回
        return data

obj = Reverse({"a":1,"b":2,"c":3})
# obj = Reverse([1,2,3,4,5])
# obj = Reverse((1,2,3,4,5,5,5))
# obj = Reverse("abcdef")

for c in obj:
    print(c)
# print(next(obj))
# print(next(obj))
# print(next(obj))
# print(next(obj))
# print(next(obj))
# print(next(obj))
# print(next(obj))

二、sorted排序方法实现原理与lambda应用

在 Python 中,提供了两个排序的方法,一个是列表的实例方法 sort(), 另一个是公共方法 sorted()

不同点:

  • sort() 方法,会直接在原列表中排序。
  • sorted 方法会返回一个排序后的新列表。

相同点:

  • 两个方法只能对基本类型的元素进行排序,比如数字,字符串。
  • 如果是复杂类型元素,比如字典,自定义类对象,默认无法进行排序。

思考:

  • 复杂类型元素的数据如何排序?

函数引用

在讲解正式内容前,需要先掌握一些前置知识。
在 Python 中,函数名表示的是运行过程中函数在内存中的地址,这个地址,称为函数引用。
可以将这个地址赋值给其它变量,从而代替函数名来调用函数。

def show():
    print("Show Run ...")

show()
func = show
func()

一般情况下,函数名赋值给其它变量会在将函数名作为参数进行传参,和实现装饰器装饰过程的时候使用。

def show():
    print("Show Run ...")

def display():
    print("Display Run ...")

def call(func):
    print("Call 函数接收一个函数引用,并调用")
    func()

call(show)
call(display)

Lambda

在大多数情况下,使用函数做为参数时,函数都只在参数传递时使用一次,而在其它位置不会再使用,这样就没有必要单独定义一个函数。
此时,就可以使用 Lambda 定义匿名函数来做为参数使用。
Lambda 可以用来定义简单的函数表达式,不能使用 if, while, for-in,return 等关键字。

def call(func,n):
    print("Call 函数接收一个函数引用,并调用")
    s = func(n)
    print(s)

a = lambda : "Hello"
print(a())
call(lambda x: f"匿名函数做为函数参数{x}","100")

sort() 与 sorted() 方法解析

sort()方法格式:

sort(key=None, reverse=False)

sorted()方法格式:

sorted(obj, key=None, reverse=False)

参数解析:

  • key: 接收一个排序规则函数
  • reverse: 是否排序后逆序,默认是False。
  • obj: 由于 sorted 方法是公共方法,obj 参数用来接收排序对象。

通过 key 参数传入一个排序规则的函数,这样就可以对复杂结构的数据进行排序了。

students = [
    {'name': 'Alice', 'id': '1001', 'class': 'Class1'},
    {'name': 'Eve', 'id': '1005', 'class': 'Class2'},
    {'name': 'Charlie', 'id': '1003', 'class': 'Class1'},
    {'name': 'David', 'id': '1004', 'class': 'Class2'},
    {'name': 'Bob', 'id': '1002', 'class': 'Class1'},
    {'name': 'Frank', 'id': '1006', 'class': 'Class2'}
]

# students.sort(key=lambda item: item["name"])
students = sorted(students,key=lambda item: item["name"])
for item in students:
    print(item)

实战: Sorted函数实现原理

实现过程分析:

  1. 模拟 sorted() 函数,实现公共排序函数,接收 排序对象,排序规则,是否逆序 三个参数
  2. 函数需要返回一个排序后的新列表,不影响原列表的数据
  3. 根据实现排序方式不同,可以选择创建新空列表,也可以使用深拷贝获得新列表
  4. 如果 key 参数没有传参,则使用默认排序,如果传参,则使用参数规则进行排序
  5. 这里选择使用空列表方式排序,根据条件如果找到位置则将元素插入到该位置,如果没找到合适的位置 ,就将元素追加到最后。
  6. 如果传入 reverse 参数,则根据参数值返回结果。

课后可以自行完成另一种深拷贝方式实现 sorted 排序。

代码:

students = [
    {'name': 'Alice', 'id': '1001', 'class': 'Class1'},
    {'name': 'Eve', 'id': '1005', 'class': 'Class2'},
    {'name': 'Charlie', 'id': '1003', 'class': 'Class1'},
    {'name': 'David', 'id': '1004', 'class': 'Class2'},
    {'name': 'Bob', 'id': '1002', 'class': 'Class1'},
    {'name': 'Frank', 'id': '1006', 'class': 'Class2'}
]

def mySorted(obj, key=None, reverse=False):
    # 将参数转换成列表
    obj = list(obj)
    print(obj)
    # 用来保存新数据的列表
    newList = []
    # 遍历原数据中的元素
    for s in obj:
        # 遍历新数据中的元素
        for n in newList:
            # 如果有规则使用规则排序
            if key:
                # 使用规则比较原数据元素和新数据中的每个元素
                if(key(s) > key(n)):
                    # 如果满足条件,则在新数据中拿到下标
                    idx = newList.index(n)
                    # 将原数据中的元素插入到新数据的下标中
                    newList.insert(idx, s)
                    # 进入下一轮循环
                    break
            else:
                # 无排序规则时比较方式
                if (s > n):
                    idx = newList.index(n)
                    newList.insert(idx, s)
                    break
        # 如果比较一轮没有符合的位置,直接将元素添加到新数据的最后
        else:
            newList.append(s)
    # 根据 reverse 参数返回结果
    return newList if reverse else newList[::-1]

if __name__ == '__main__':

    students = mySorted(students, key=lambda s: s["name"])
    # students.sort(key=lambda s: s["name"])

    # students = [1,4,2,6,7,8,4,3,3]
    # students = mySorted(students, reverse=True)
    print(students)
    # for s in students:
        # print(s)

    # print(mySorted({"name": "tom", "age": 12, "gender": "male"}))
    # print(mySorted("hdollo"))

其它应用场景

与 Sorted 原理类似的,还有在大数据应用非常多的三个函数 map, reduce, filter, 在使用时也需要像 Sorted 函数一样,能用 lambda 传入操作规则进行使用

  • map 将传入的数据序列以指定的规则映射成新序列

    upperString = map(lambda s: s.upper(), ["hello", "world", "pytHon"])
    for s in upperString:
        print(s)
    
  • reduce 将传入的数据序列中的数据根据指定的规则进行累计

    print(reduce(lambda x, y: x * y, [1, 2, 3, 4, 5])) # 累乘
    print(reduce(lambda x, y: x if x > y else y, [5, 1, 2, 3, 4])) # 找最大值
    print(reduce(lambda x,y: x * 10 + y, [3, 5, 8, 1])) # 数字组合
    
  • filter 将传入的数据序列中的数据根据指定的规则进行过滤。

    students = [{"Name": "Tom", "age": 20}, {"Name": "Tom", "age": 10}, {"Name": "Tom", "age": 22}, {"Name": "Tom", "age": 8}]
    new_stu = filter(lambda x: x["age"] >= 18, students)
    for s in new_stu:
        print(s)
    

三、装饰器与数据驱动原理实现

装饰器是 Python 中非常重要的语法,可以通过装饰器,在不修改函数定义和调用的前提下,简洁高效的为函数扩展额外的功能。

装饰器语法:@装饰器名

系统中有很多预制装饰器,比如:@staticmethod, @classmethod 等。

如果想要实现自定义装饰器,要配合闭包一起实现。

闭包

闭包是一种函数的特殊定义形式,定义闭包需要满足以下几个条件:

  1. 在函数内部嵌套定义另外一个函数
  2. 内函数使用外函数中定义的变量
  3. 外函数返回内函数的引用

正常情况下,一旦函数执行结束,那么函数内部定义局部变量就会随着函数的执行结束而销毁。

而闭包是由内函数和外函数局部变量构成的一个环境。

因为内函数中使用了外函数的局部变量,所以此时外函数的局部变量并不会销毁,而是被保存到了闭包环境中。

当使用内函数时,就可以通过内函数间接使用外函数的局部变量。

def outer():
    data = "外函数变量的数据"
    def inner():
        print("内函数执行,并使用了外函数中的局部变量")
        print(data)
    return inner

func = outer()
func()

装饰器

利用闭包的这个特性,就可以自定义装饰器使用,对其它函数进行功能的扩展。

import time
def count_time(func):
    def inner():
        start_time = time.time()
        func()
        stop_time = time.time()
        print(f"函数共执行了{stop_time - start_time}秒")
    return inner

@count_time
def show():
    for i in range(1,4):
        print(f"第 {i} 次循环输出")
        time.sleep(1)

show()

通用装饰器

前面定义的装饰器过于简陋,只为理解自定义装饰器如何使用。

理论上一个装饰器可以对任何函数进行装饰,但前面的代码却无法实现。

这是因为函数在定义时,由于参数不同,返回值不同,处理也不尽相同。

为了能让装饰器适配于所有的函数,就需要对实现装饰器的闭包进行重新定义

import time
def count_time(func):
    def inner(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        stop_time = time.time()
        print(f"函数共执行了{stop_time - start_time}秒")
        return result
    return inner

装饰器的原理

装饰器的本质就是函数的调用,而装饰器的语法形式是 Python 提供的一种语法糖。

装饰器的装饰过程就是将被装饰函数的引用做为参数,传递到闭包的外函数中,然后返回内函数的引用重新赋值给被装饰函数。

此过程是 Python 解释器在解释代码时,自动传入的,不需要手动实现。

一旦装饰器的装饰过程执行完毕,那么被装饰函数的引用就发生了变化,保存的是闭包的内函数引用。

当调用被装饰函数时,实际调用的是闭包的内函数,再由内函数间接调用被装饰函数。

import time
def count_time(func):
    def inner(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        stop_time = time.time()
        print(f"函数共执行了{stop_time - start_time}秒")
        return result
    return inner

def show():
    for i in range(1,4):
        print(f"第 {i} 次循环输出")
        time.sleep(1)
# 装饰过程
show= count_time(show)
show()

该装饰过程只为帮助理解装饰器如何工作,实际使用时不需要这样操作。

带参数的装饰器

在使用装饰器的时候还可以给装饰器携带参数,这种情况在 Web 框架的路由,Pytest 框架的数据驱动都非常常见。

语法格式:@装饰器名(参数列表...)

可以问题随之而来,传递参数容易,接收参数难,装饰器的函数是由解释器自动传入的,额外的参数该如何接收呢?

解决办法也很简单,将实现装饰器的闭包外再定义一层函数,形成三层函数嵌套,使用最外层函数来接收装饰器的参数。

import time
def wrapper(*args,**kwargs):
    def count_time(func):
        print("接收的装饰器参数为:", *args,**kwargs)
        def inner(*args, **kwargs):
            start_time = time.time()
            result = func(*args, **kwargs)
            stop_time = time.time()
            print(f"函数共执行了{stop_time - start_time}秒")
            return result
        return inner
    return count_time
@wrapper(1,2,3,4)
def show():
    for i in range(1,4):
        print(f"第 {i} 次循环输出")
        time.sleep(1)
show()

带参装饰器装饰过程

实际上,带参的装饰器装饰过程也非常简单,万变不离其宗。只用最简单的 Python 语法就可以解释这个过程。

装饰过程步骤如下:

  1. @wrapper(1,2,3,4) 可拆分为两个运算符 @(),一个标识符为 wrapper
  2. 根据运算符运算规则,括号具有最高优先级,结合后变为 wrapper(1,2,3,4)
  3. 此时,wrapper(1,2,3,4) 就变成了普通的函数调用,得到函数执行结果中层函数的引用 count_time
  4. 函数执行结果再与 @ 结合,就回到了最初的装饰器的形式 @count_time
  5. 执行 show = count_time(show) 后,完成完整装饰过程

实战:带参数装饰实现数据驱动

在使用 Pytest 测试框架进行测试时,可以使用不同的数据集,对同一个测试用例进行测试。

import pytest
@pytest.mark.parametrize("a,b,c",[(1,2,3),(4,5,9),(7,8,9)])
def test_case(a,b,c):
    assert a + b == c

数据驱动的方式就是使用带参数装饰器来完成的。

装饰器的参数有两个:

  • 参数一:使用字符串形式表示的参数列表,参数列表中的参数名要和用例中的参数名保持一致,并使用逗号分隔
  • 参数二:使用列表或其它可迭代对象保存数据集,数据集中的每个元素是用例执行的一组数据。

实现过程分析:

  1. 实现装饰器使用的三层闭包函数,外层函数用来接收装饰器的参数
  2. 在中间层函数中,处理外层函数接收的参数,将参数和数据组合成字典列表(难点)
  3. 在中间层函数中,遍历组合好的字典列表数据,将数据做为参数调用内层函数

代码:

# 接收装饰器参数的函数
# 参数一:以字符串形式接收被装饰函数的参数列表,需要与被装饰函数参数名保持一致,例:"a,b,c"
# 参数二:以[(),(),()] 形式传入驱动数据。
def decorator_args(vars, datas):
    def decorator(func):
        # 将字符串参数分割备用
        v_keys = vars.split(",")
        # 定义保存 [{},{},{}] 形式的数据
        new_datas = []
        # 遍历数据,取出一组元素数据
        for item in datas:
            # 定义一个新字典,用来保存 变量名与传入数据组成的字典
            d_item = {}
            # 使用 zip 函数,同时遍历两个元组,变量名做为key, 元素数据做为value
            for k, v in zip(v_keys, item):
                # 将 变量名和值对应保存到字典中
                d_item[k] = v
            # 将组合好的字典追加到新数据中备用
            new_datas.append(d_item)
        def inner(*args, **kwargs):
            return func(*args, **kwargs)
        # 遍历新数据,取出元素字典
        for item in new_datas:
            # 将字典中的数据解包传给内函数
            inner(**item)
        return inner
    return decorator

# 数据驱动数据
data = [(1,2,3),(4,5,6),(7,8,9)]

# 装饰器传参
@decorator_args("a,b,c", data)
def show(a,b,c):
    print("接收的数据:", a,b,c)

课后调查表:

1 个赞