1 解包* **

*可以分别对列表、元组、集合进行解包操作(可以理解为拆除括号)

nums = [1, 2, 3]
print("{}, {}, {}".format(*nums))
#[Output] 1, 2, 3

**可以对字典进行解包操作,可以理解为拆除大括号。下面是一个快速合并列表的操作(需要python3.5+)

x = {'a': 1, 'b': 2}
y = {'b': 3, 'c': 4}
z = {**x, **y}
# z = {'c': 4, 'a': 1, 'b': 3}

2 logging的使用

日志模块使用案例:

logging.basicConfig(
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    level=logging.INFO,
    filename="log/test.log"
)

3 ini配置文件读取

import configparser

parser = configparser.ConfigParser()
parser.read("config.ini", encoding="utf8")
campus = parser["User"]["campus"]

config.ini:

[User]
campus = xianlin

4 项目依赖包管理

使用pipreqs工具,自动解析当前项目依赖的包,生成requirments.txt:

pip3 install pipreqs

pipreqs .

5 Pytest测试

可以使用pytest编写单元测试。

首先安装pip3 install pytest

pytest会自动找到Test开头的类test_开头或_test结尾的函数,完成运行和测试。

在测试函数中,使用assert <actual> <predict>来判断结果是否正确,也可以以assert <expr>的形式来使用。

可以使用临时目录,只需在test函数的参数中传入tmp_path参数即可,pytest会自动建立临时目录,并传入测试以供使用。这个被称为fixtures,pytest还提供了其他一系列fixtures,可以用pytest --fixtures命令查看。

自定义fixture:fixture可以理解为测试运行的依赖,只要把一个fixture作为参数传给测试的函数,就表示这个测试依赖于这个fixture。fixture本身是一个函数,在放入参数中被依赖的时候,pytest会找到和参数同名的函数(也就是fixture)先执行一下做好准备,如果有返回值就把返回值传回原来测试中的参数位置。fixture可以提前帮助测试准备好需要的变量、以及完成上下文的构建。自定义fixture可以用@pytest.fixture注解来实现,并且可以带上参数来规定fixture的作用域(默认是function级别的,即每个测试函数使用的fixture都是单独的)。fixture的作用域有:function, class(一个类别中的所有测试方法共享一个fixture,共享意味着只会执行一次), module(模块级), package(包级) 和 session(回话级,也就是一次完整的测试只共享一个)

fixture可以相互依赖,fixture还可以保证在一个测试中只运行一次,例如test_1依赖于fixture1和fixture2,而fixture1也依赖于fixture2,但是执行的时候可以保证fixture2只会被执行一次,不会被重复执行。

一段使用fixture的示例如下:

class TestSolution():
    @pytest.fixture(scope="class")
    def solution(self):
        return Solution()

    def test_get_priority(self, solution):
        item_str = "p"
        item_str2 = "L"
        assert solution.get_priority(item_str) == 16
        assert solution.get_priority(item_str2) == 38
        assert 0
    
    def test_haha(self, solution):
        assert 0

上述例子中,test_get_priority和test_haha使用的是同一个Solution对象。

然后在命令行中使用python -m pytest -q <myfile.py>进行测试。

6 遍历字典

用到三个函数:

  • dictionary.keys() 返回所有键的列表
  • dictionary.values() 返回所有值的列表
  • dictionary.items() 返回所有键值对的列表

直接用for遍历这三个列表即可。

7 Python引用传递

Python中所有函数参数的传递都是基于引用传递的,因为一切皆对象

平时看起来像值传递的,其实也是引用传递,下面的代码可以证明:

def ha1(a):
    print(id(a))
    a += 1
    print(id(a))
x = 4
print(id(x))
ha1(x) 
# 输出
139663802974544
139663802974544
139663802974576

可见,仅在参数传递时,只是新建了一个引用,指向同一块内存地址,也就是指向同一个对象。但是赋值之类的操作才会改变引用指向的对象。a += 1等同于a = a + 1,先计算a + 1的值,然后产生一个新的对象再把a重新指向这个新的对象。

可以使用id(a)查看a指向的对象的内存地址(十进制)

这样就证明了,给list添加元素时使用li.append(a)li = li + [a]更好:

li = li + [a]

image-20221214144343244

li.append(a):

image-20221214144533452

但是,li += [a]却被优化成了append一样的效果了:

image-20221214145318357

上述可视化图形由Online Python compiler and debugger - Python Tutor - Learn Python by visualizing code生成,非常方便好用,查看代码运行究竟发生了什么的好工具

集合操作

Python中的元素如果想放入集合(或者字典)中,必须是hashable的。如果对于list这种不可hash的,可以先转换成hashable的tuple,再放入集合。

向set中插入和删除元素的操作如下:

s = set()
s.add(1)# {1}
s.update([1,2,3]) #{1,2,3}
s.remove(3) #{1,2}

其中add和update对于一个普通元素的操作没有什么区别,但是update如果接受的是一个列表,会把列表中的每个元素插入到集合中(add会把list作为整体插入到集合中,当然这是会报错的,因为list是unhashable)。

解包赋值

在Python中一开始最为惊艳的语句之一便是:a, b = b, a 直接绕过了编程新手不为熟悉的变量,给出交换变量的非常方便的语法糖。

但是在后来的使用中,也时常被坑到,下面会解析一下Python在执行类似的语法糖语句时究竟发生了什么。

  1. 先计算右侧所有值的值。也就是说,在赋值给变量(引用)之前,所有右侧的对象值已经被确定了,不会再在赋值过程中更改。例如a, b = 1, a + 1 b的值不是2,而是a之前的值加上1.
  2. 从左到右依次给左边的引用赋值上右边对应的引用。a, a = 1, 2的结果a是2。
  3. 左项的引用会依次在轮到自己赋值的时候得到,这一点在链表中经常出错。例如p1.next, p1.next.next = None, p1会报错,因为第一次p1.next=None之后,在第二次赋值时取p1.next.next就会变成None.next,导致出错。

栈和队列操作

栈在Python中可以直接使用List,因为Python中List的底层实现就是变长的数组,因此在最后插入和删除元素代价很小,可以直接代替链表用来做栈。

stack = []
stack.append(1) # push
if stack: # isEmpty
    print(stack[-1]) # top
    a = stack.pop() # top and pop

但是队列就不可以使用List,因为在任意位置或者开头插入删除元素代价比较高。可以使用deque或者LinkedList。这里先说明deque的用法:

q = deque(range(1, n + 1))
q.append(1) #push
print(len(q))
while q: # isEmpty
    print(q[-1]) # top
    print(q.popleft()) # top and pop

二分查找

二分查找使得从有序的数组中查找元素(或者插入)的复杂度变成O(logn)。如果每次自己去写二分查找比较麻烦,可以直接借助Python提供的二分查找库bisect.

li = [1, 4, 6, 90, 90, 899, 10030]
t = 90
pos = bisect.bisect_left(li, t) #pos = 3
pos = bisect.bisect(li, t) # pos = 5
bisect.insort(li, 1) 

bisect_left表示插入一个新元素后新元素该在的位置(如果有相同元素,新元素插入到最左边)。效果等同于查找元素。

bisect_right或者bisect则表示插入一个新元素后新元素该在的位置(如果有相同元素,新元素插入到最右边)

也就是说,不管是insort还是bisect函数,bisect的默认处理方式都是假设元素插入到已有的相同元素的后面。