🌀Jarson Cai's Blog
头脑是日用品,不是装饰品
Python进阶用法(一)
使用Python的高级特性更加高效地完成任务

Python进阶笔记(一)

面向对象编程

在之前的Python类编程中,只知道类似的self调用类内方法,今天来学习一下类变量、类方法、静态方法。

类变量

来看一个类的例子:

1
2
3
4
5
6
class Student:
    def __init__(self, name, sex):
        self.name = name
        self.sex = sex

s1 = Student("Caicai", "male")

可以看到name和sex属性都代表了单个实例的信息,这时,假设我需要统计学生类的人数,也就是学生类实例的个数,我们就可以使用类变量的方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Student:
    student_num = 0

    def __init__(self, name, sex):
        self.name = name
        self.sex = sex
        Student.student_num += 1

s1 = Student("Caicai", "male")
print(f"Student.student_num: {Student.student_num}")
# Student.student_num: 0

在类下面添加的变量就是类变量,类变量的访问需要通过类直接进入。

假设我们将上述代码改成了如下形式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Student:
    student_num = 0

    def __init__(self, name, sex):
        self.name = name
        self.sex = sex
        # 执行该句时,python需要先取值再写入 
        self.student_num += 1 

s1 = Student("Caicai", "male")
print(f"Student.student_num: {Student.student_num}")
print(f"s1.student_num:{s1.student_num}")
# Student.student_num: 0
# s1.student_num:1

上述代码在执行self.student_num += 1时,python需要先取值再写入,而实例中并不存在student_num这个属性。因此在取值的时候,python实际上是在类里面获取了student_num的值,写入时给实例写入了这一属性,没有改变类内属性的值。因此我们需要统一通过类名进行访问类属性!!!

类方法

修正上述方法的表达后,我们增加一个添加学生数量的类方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Student:
    student_num = 0
    # 构造方法
    def __init__(self, name, sex):
        self.name = name
        self.sex = sex
        Student.student_num += 1
    # 类方法
    @classmethod
    def add_students(cls, add_num):
        cls.student_num += add_num
    # 类方法
    @classmethod
    def from_string(cls, info):
        name, sex = info.split(" ")
        return cls(name, sex) # 返回类方法


s1 = Student("Caicai", "male")
s2 = Student.from_string("Caicai male")
print(f"Student.student_num: {Student.student_num}")
# Student.student_num: 2

类方法需要使用装饰器@classmethod来装饰,类方法的第一个参数通常为类本身class的缩写cls,它可用于访问类变量。

上述的类通过from_string类方法解析不同的格式来返回构造函数,增加了不同的初始化方法。使用类方法来替代构造方法是一种很常见的方法。

静态方法

静态方法需要使用@staticmethod来装饰,静态方法不需要传入cls或者self,它同样不能访问类和实例里面的私有属性。静态方法的适用场景:该静态方法不需要类里面的内容,但在逻辑上这个静态方法需要在类的里面,同样也适用于一系列功能相关的函数封装在一起。

来看一个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Student:
    student_num = 0
    # 构造方法
    def __init__(self, name, sex):
        self.name = name
        self.sex = sex
        Student.student_num += 1
    # 类方法
    @classmethod
    def add_students(cls, add_num):
        cls.student_num += add_num
    # 类方法
    @classmethod
    def from_string(cls, info):
        name, sex = info.split(" ")
        return cls(name, sex) # 返回类方法

    # 静态方法
    @staticmethod
    def name_len(name):
        return len(name)


s1 = Student("Caicai", "male")
s2 = Student.from_string("Caicai male")
print(f"s2.name:{s2.name}, s2.name.len:{Student.name_len(s2.name)}")
# s2.name:Caicai, s2.name.len:6

静态方法在外部必须使用类本身来访问,在类内则可以通过self来访问。

推导式

推导式是一种高效创建列表、字典、集合或者其他可迭代的对象的方式,极大了压缩了代码的长度。

列表

先看一个列表复制的例子:

1
2
3
4
5
6
nums = [0, 1, 2, 3, 4, 5]

my_list = []
for i in nums:
    my_list.append(nums[i])
print(my_list)

上述为普通的写法,如果使用推导式,它的代码将简化为:

1
2
my_list = [i for i in nums]
print(my_list)

再来看一个略微复杂的例子:

1
2
3
4
5
6
nums = [0, 1, 2, 3, 4, 5]

my_list = []
for i in nums:
    my_list.append(i**2)
print(my_list)

它的推导式写法可变为:

1
2
my_list = [i**2 for i in nums]
print(my_list)

在循环中加入条件判断

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 1
nums = [0, 1, 2, 3, 4, 5]

my_list = []
for i in nums:
    if i % 2 == 0:
        my_list.append(i**2)
print(my_list)

# 2
nums = [0, 1, 2, 3, 4, 5]

my_list = [] 
for i in nums:
    if i % 2 == 0:
        my_list.append(i**2)
print(my_list)

注意只有if和有if-else的写法是不一样的

1
2
3
4
5
6
7
8
9
# 1
nums = [0, 1, 2, 3, 4, 5]
my_list = [i**2 for i in nums if i%2 == 0]
print(my_list)

# 2
nums = [0, 1, 2, 3, 4, 5]
my_list = [i**2 if i%2 == 0 else i**3 for i in nums]
print(my_list)

再加大一点难度,加入双层for循环:

1
2
3
4
5
6
7
8
letters = ["a", "b", "c"]
nums = [1, 2, 3]

my_list = []
for i in letters:
    for j in nums:
        my_list.append((i, j))
print(my_list)

推导式的写法为:

1
2
3
4
5
letters = ["a", "b", "c"]
nums = [1, 2, 3]

my_list = [(i, j) for i in letters for j in nums]
print(my_list)

字典

将列表中的元素一一配对:

1
2
3
4
5
6
7
letters = ["a", "b", "c"]
nums = [1, 2, 3]

my_dict = {}
for i, j in zip(letters, nums):
    my_dict[i] = j
print(my_dict)

推导式写法为:

1
2
# 注意切换为字典后写法的变化
my_dict = {i: j for i, j in zip(letters, nums)}

集合

集合的特点是无序,不重复。 举一个例子,使用频率没有前两个那么高:

1
2
3
4
5
l = [1,2,3,4,5,6,7,8,8]
my_set = set()
for i in l:
    my_set.add(i)
print(my_set)
1
2
3
4
l = [1,2,3,4,5,6,7,8,8]
my_set = set()
my_set = {i for i in l}
print(my_set)

总结

Python推导式非常适用于从零创建有序的一些迭代对象。但是推导式使用循环最多两层,再多不建议使用,因为比较容易造成代码可读性较差的问题,以及在代码调试上的一些难题。

生成器

生成器是一个在处理大量数据时较为有效的工具。它与列表是一种类似的工具,但两者的性能却完全不同。它们的主要区别:

Python中的生成器(Generator)和列表(List)是两种不同的数据结构,它们在内存使用、性能和使用场景上有显著的区别。以下是生成器和列表的主要区别:

  1. 内存使用

    • 列表:列表在内存中存储所有的元素。如果列表很大,会占用大量的内存。
    • 生成器:生成器是惰性计算的,它只在需要时生成下一个元素。这意味着生成器在内存中只存储生成下一个元素所需的状态,因此占用的内存非常少。
  2. 性能

    • 列表:列表在创建时会立即计算并存储所有元素,因此访问列表中的元素非常快。
    • 生成器:生成器在每次迭代时才计算下一个元素,因此生成器的创建和迭代速度可能比列表慢。
  3. 迭代行为

    • 列表:列表可以多次迭代,因为所有元素都存储在内存中。
    • 生成器:生成器只能迭代一次,因为元素是按需生成的,一旦生成并使用,生成器就无法再次生成相同的元素。
  4. 语法

    • 列表:列表使用方括号 [] 来定义,例如 [1, 2, 3, 4]
    • 生成器:生成器使用圆括号 () 来定义生成器表达式,或者使用 yield 关键字来定义生成器函数。例如:
      1
      2
      3
      4
      5
      6
      7
      
      # 生成器表达式
      gen = (x for x in range(10))
      
      # 生成器函数
      def gen_func():
          for i in range(10):
              yield i
      
  5. 使用场景

    • 列表:适用于需要多次访问、修改或查找元素的场景。
    • 生成器:适用于需要处理大量数据或无限序列,且不需要多次访问所有元素的场景。

生成器适用于超大数据场景的读取上,还有大语言模型的流式输出也使用到了生成器的思想。

来看一个例子来学习生成器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def square_numbers(nums):
    result=[]
    for i in nums:
        result.append(i * i)
    return result


def gen_numbers(nums):
    for i in nums:
        yield i * i

my_nums = square_numbers([1, 2, 3, 4, 5])
print(my_nums)
# [1, 4, 9, 16, 25]

my_gens = gen_numbers([1, 2, 3, 4, 5])
print("Running next function:")
print(next(my_gens))
print("Then running for loop:")
for gen in my_gens:
    print(gen)
print("Then running for loop:")
for gen in my_gens:
    print(gen)
# Running next function:
# 1
# Then running for loop:
# 4
# 9
# 16
# 25
# Then running for loop:

可以看到先使用next读取之后,for loop的读取便从4开始了,由此可见生成器生成的内容只允许读取一遍,所以再第二次遍历的时候已经变为了空值。

再举一个大语言模型的流式输出例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
def stream_output(data, chunk_size=10):
    """
    生成器函数,用于逐个生成数据块
    :param data: 要处理的数据
    :param chunk_size: 每个数据块的大小
    """
    start = 0
    end = chunk_size
    while start < len(data):
        yield data[start:end]
        # 使用时间停顿来模拟大模型处理过程
        time.sleep(0.1)
        start = end
        end += chunk_size

# 示例数据
data = "This is a long piece of text that we want to stream out in chunks."

# 使用生成器进行流式输出
for chunk in stream_output(data):
    print(chunk)

匿名函数

匿名函数的应用场景:当我们在使用一个简单的,只会使用一次的函数,就可以使用匿名函数。匿名函数的关键字是lambda,来看一个使用的例子:

1
2
3
4
5
6
7
def add(a, b):
    return a + b
print(add(3, 4))

# 匿名函数写法
add = lambda a, b: a + b
print(add(3, 4))

匿名函数的构造:函数名 = lambda + 入参 + 返回值,入参如果没有可以省略。让我们来看一个更常见的写法:

1
2
3
my_list = [1, 2, 3, 4, 5]
new_list = list(map(lambda x: x**2, my_list))
print(new_list)

首先先解释一下map函数的作用,map函数接受一个函数和一个可迭代对象作为参数,对可迭代对象每个元素都应用这个函数,最后返回一个新的迭代器。

再看一个好用的例子,将字典中的value作为匿名函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def user_logging(user):
    if user.level == 1:
        user.credits += 2
    elif user.level == 2:
        user.credits += 5
    elif user.level == 3:
        user.credits += 10

def user_logging_1(user):
    level_credit_map = {
        1: lambda x: x + 2,
        2: lambda x: x + 5,
        3: lambda x: x + 10
    }
    user.credits = level_credit_map[user.level](user.credits)

最后修改于 2024-06-23

知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。