ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Python学习日记6

Python学习日记6 一抽象——函数详解6.1 为什么要抽象函数的意义6.1.1 懒惰是一种美德编程中懒惰不是贬义词而是指不做重复无谓的工作。如果你有一段代码需要在多个地方使用就应该把它封装成函数。没有抽象时每次需要计算斐波那契数列时都要重写循环fibs [0, 1] for i in range(8): fibs.append(fibs[-2] fibs[-1]) print(fibs) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]有抽象后定义一次函数随处调用。def fibs(num): 返回前 num 个斐波那契数组成的列表 result [0, 1] for i in range(num - 2): result.append(result[-2] result[-1]) return result print(fibs(10)) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] print(fibs(15)) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377]6.1.2 抽象让程序更易理解好的程序应该是分层的高层代码表达做什么低层函数表达怎么做。# 高层代码清晰表达程序意图 page download_page() freqs compute_frequencies(page) for word, freq in freqs: print(word, freq)你不需要知道download_page()和compute_frequencies()内部如何实现只需要知道它们完成什么任务。每个具体操作都在独立的函数中定义这种分层抽象让程序可读性大幅提升。6.2 定义和调用函数6.2.1 使用def定义函数def是 Python 中定义函数的关键字后面跟函数名、参数列表圆括号内和冒号函数体需要缩进。def hello(name): 向指定的名字打招呼 return Hello, name ! # 调用函数 print(hello(world)) # Hello, world! print(hello(Gumby)) # Hello, Gumby!6.2.2 判断对象是否可调用callable()有些对象是函数可调用有些不是。callable()可以帮我们判断。import math x 1 y math.sqrt print(callable(x)) # False数字不可调用 print(callable(y)) # True函数可调用6.2.3 返回值return语句return用于从函数返回一个值函数执行到return时立即结束。def test(): print(这条会被打印) return print(这条不会被执行) # 永远不会执行 result test() # 打印这条会被打印 print(result) # None关键点1所有函数都有返回值2如果没有显式return或者return后面没有值函数返回None3return可以返回任何类型的值数字、字符串、列表、字典等6.3 函数的文档字符串docstring6.3.1 什么是文档字符串文档字符串是写在函数开头的字符串用于说明函数的用途、参数和返回值。它会被 Python 保存为函数的属性可以通过.__doc__或help()查看。def square(x): 计算一个数的平方并返回结果 return x * x # 查看文档字符串 print(square.__doc__) # 计算一个数的平方并返回结果 # 使用 help 查看完整信息 help(square)输出Help on function square in module __main__: square(x) 计算一个数的平方并返回结果6.3.2 文档字符串的规范好的文档字符串应包含函数的作用简述参数说明可选返回值说明可选def fibs(num): 返回前 num 个斐波那契数组成的列表。 参数: num: 要生成的斐波那契数的个数必须 2 返回: 包含前 num 个斐波那契数的列表 result [0, 1] for i in range(num - 2): result.append(result[-2] result[-1]) return result6.4 函数的参数核心知识6.4.1 位置参数位置参数是最基本的参数形式调用时按位置依次匹配。def greet(greeting, name): print(greeting , name !) greet(Hello, Alice) # Hello, Alice! greet(Hi, Bob) # Hi, Bob!如果参数顺序错了结果就会出错greet(Alice, Hello) # Alice, Hello!逻辑错误6.4.2 关键字参数关键字参数允许你按名称指定参数值从而忽略参数的顺序def greet(greeting, name): print(greeting , name !) greet(nameAlice, greetingHello) # Hello, Alice! greet(greetingHi, nameBob) # Hi, Bob!优点调用时参数顺序不再重要代码可读性更高参数的含义一目了然6.4.3 默认参数值给参数指定默认值后调用时如果不提供该参数将使用默认值。这在定义可选参数时非常有用。def greet(greetingHello, nameworld): print(greeting , name !) greet() # Hello, world! greet(Hi) # Hi, world! greet(Hi, Alice) # Hi, Alice! greet(nameGumby) # Hello, Gumby!重要提示默认参数在函数定义时被计算一次。如果默认值是可变对象如列表可能会产生意料之外的行为。def add_item(item, lst[]): lst.append(item) return lst print(add_item(1)) # [1] print(add_item(2)) # [1, 2] ← 注意不是 [2]正确的做法用None作为默认值在函数内部创建新列表。def add_item(item, lstNone): if lst is None: lst [] lst.append(item) return lst print(add_item(1)) # [1] print(add_item(2)) # [2]6.4.4 收集参数可变参数有时候你希望函数能接受任意数量的参数。Python 提供了*和**两种收集方式。*收集位置参数 → 元组def print_all(*args): print(我收到了:, args) print_all(1, 2, 3) # 我收到了: (1, 2, 3) print_all(a, b, c) # 我收到了: (a, b, c) print_all() # 我收到了: ()args是一个元组包含了所有传入的位置参数。即使只传一个参数也会被包装成元组。**收集关键字参数 → 字典def print_kwargs(**kwargs): print(我收到了:, kwargs) print_kwargs(x1, y2) # 我收到了: {x: 1, y: 2} print_kwargs(nameAlice, age25) # 我收到了: {name: Alice, age: 25}混合使用def mixed(a, b, *args, **kwargs): print(a , a) # 1 print(b , b) # 2 print(args , args) # (3, 4, 5) print(kwargs , kwargs) # {x: 10, y: 20} mixed(1, 2, 3, 4, 5, x10, y20)工作流程普通位置参数a、b先取值剩余的位置参数被*args收集所有关键字参数被**kwargs收集6.4.5 分配参数拆包与收集参数相反拆包是把序列或字典拆开作为单独的参数传递给函数。def add(x, y): return x y # 元组拆包 params (1, 2) print(add(*params)) # 3 # 字典拆包 params {x: 3, y: 4} print(add(**params)) # 7拆包的好处当参数已经存储在列表/字典中时无需手动一个个取出直接拆包传递即可。def calculate(a, b, c): return a * b c values [2, 3, 4] print(calculate(*values)) # 2*34 106.4.6 参数的完整顺序如果一个函数同时包含多种参数类型顺序应为def func(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2): pass但更常见的顺序是def func(普通位置参数, *args, 默认参数, **kwargs): pass实际使用时我们通常只需要掌握位置参数、默认参数、*args、**kwargs这四种就足够了。6.5 作用域命名空间6.5.1 什么是作用域作用域可以理解为变量的可见范围。Python 中每个函数调用都会创建一个新的作用域局部命名空间函数内部定义的变量只在该函数内可见。def foo(): x 42 # 局部变量只在 foo 函数内有效 x 1 # 全局变量 foo() print(x) # 1全局变量未被修改6.5.2 访问全局变量在函数内部可以读取全局变量的值但如果要修改它必须使用global关键字声明。x 1 def read_global(): print(x) # 读取全局变量OK def modify_global(): global x x x 1 # 修改全局变量必须用 global read_global() # 1 modify_global() print(x) # 2为什么需要global因为如果函数内部对变量赋值Python 默认会创建一个新的局部变量而不是修改全局变量。没有global时x x 1会报错右边的x还未定义因为左边声明了局部xPython 不会去全局查找。6.5.3 嵌套作用域和闭包函数可以嵌套定义。内部函数可以访问外部函数的变量这种机制称为闭包closure。def multiplier(factor): 根据指定的因子创建乘法函数 def multiply(number): return number * factor # 内部函数访问外部变量 factor return multiply # 返回内部函数 double multiplier(2) # 创建乘以2的函数 triple multiplier(3) # 创建乘以3的函数 print(double(5)) # 10 print(triple(5)) # 15工作原理解读调用multiplier(2)时创建了局部变量factor 2内部定义了函数multiply它捕获了factor的值multiplier返回multiply函数即使multiplier执行完毕double函数仍然记住了factor 2修改外部变量nonlocal如果内部函数需要修改外部变量要用nonlocal声明。def make_counter(): count 0 def increment(): nonlocal count # 声明要修改外部变量 count 1 return count return increment counter make_counter() print(counter()) # 1 print(counter()) # 2 print(counter()) # 36.6 递归6.6.1 什么是递归递归是函数调用自身的编程方式。一个正确的递归函数必须包含两个部分基线条件Base Case最简单的情况直接返回结果不再递归递归条件Recursive Case将问题分解为更小的子问题调用自身6.6.2 经典示例阶乘数学定义1! 1基线条件n! n × (n-1)!递归条件代码实现def factorial(n): if n 1: # 基线条件 return 1 else: # 递归条件 return n * factorial(n - 1) print(factorial(5)) # 120 print(factorial(10)) # 3628800执行过程追踪以factorial(5)为例factorial(5) → 5 * factorial(4) → 5 * (4 * factorial(3)) → 5 * (4 * (3 * factorial(2))) → 5 * (4 * (3 * (2 * factorial(1)))) → 5 * (4 * (3 * (2 * 1))) → 1206.6.3 经典示例幂运算数学定义x^0 1基线条件x^n x × x^(n-1)递归条件def power(x, n): if n 0: # 基线条件 return 1 else: # 递归条件 return x * power(x, n - 1) print(power(2, 10)) # 1024 print(power(3, 4)) # 816.6.4 经典示例二分查找二分查找是在已排序的序列中查找指定元素的高效算法它完美体现了递归思想。算法思想找到序列的中间位置如果目标值等于中间元素找到如果目标值小于中间元素在左半部分继续查找如果目标值大于中间元素在右半部分继续查找重复上述过程直到找到或区间为空def search(sequence, number, lower0, upperNone): 在已排序序列中二分查找目标值的索引。 参数: sequence: 已排序的序列 number: 要查找的目标值 lower: 查找区间的下界包含 upper: 查找区间的上界包含默认为最后一个元素 # 首次调用时设置 upper 为最后一个索引 if upper is None: upper len(sequence) - 1 # 基线条件区间只剩一个元素 if lower upper: assert number sequence[upper], 目标值不在序列中 return upper # 递归条件计算中间位置 middle (lower upper) // 2 if number sequence[middle]: # 目标在右半部分更新下界为 middle 1 return search(sequence, number, middle 1, upper) else: # 目标在左半部分更新上界为 middle return search(sequence, number, lower, middle) # 测试 arr [1, 3, 5, 7, 9, 11, 13, 15] print(search(arr, 7)) # 3 print(search(arr, 13)) # 6执行过程查找 7search([1,3,5,7,9,11,13,15], 7, 0, 7) → middle 3, sequence[3] 7 → 7 7返回 36.7 函数式编程工具Python 虽然以面向对象为主但也支持一些函数式编程风格的工具。6.7.1map—— 对序列每个元素应用函数map将一个函数应用到序列的每个元素返回一个迭代器。def square(x): return x * x nums [1, 2, 3, 4, 5] result map(square, nums) print(list(result)) # [1, 4, 9, 16, 25]结合lambda使用更简洁nums [1, 2, 3, 4, 5] print(list(map(lambda x: x * x, nums))) # [1, 4, 9, 16, 25]6.7.2filter—— 过滤序列元素# 保留所有非空字符串 words [hello, , world, , python] print(list(filter(lambda s: s.strip() ! , words))) # [hello, world, python]filter根据函数返回值True保留False过滤来处理序列。def is_even(x): return x % 2 0 nums [1, 2, 3, 4, 5, 6] print(list(filter(is_even, nums))) # [2, 4, 6]6.7.3reduce—— 累积计算reduce反复将函数应用到序列元素上最终归约为一个值。需要从functools导入。from functools import reduce def add(x, y): return x y nums [1, 2, 3, 4, 5] total reduce(add, nums) print(total) # 15 # 用 lambda 更简洁 total reduce(lambda x, y: x y, nums) print(total) # 15工作过程reduce(add, [1,2,3,4,5]) → add(1, 2) 3 → add(3, 3) 6 → add(6, 4) 10 → add(10, 5) 156.7.4lambda表达式lambda用于创建匿名函数没有函数名的短函数适合简单的一次性操作。# 普通函数 def double(x): return x * 2 # lambda 等效 double lambda x: x * 2 print(double(5)) # 10lambda 语法lambda 参数列表: 返回值表达式注意lambda 体内只能写一个表达式不能写多条语句或赋值操作。使用场景作为map、filter、reduce的参数用于排序时的key参数# 按字符串长度排序 words [apple, kiwi, banana, pear] words.sort(keylambda s: len(s)) print(words) # [pear, kiwi, apple, banana]二再谈抽象——类与面向对象7.1 面向对象编程概述7.1.1 什么是面向对象编程面向对象编程Object-Oriented ProgrammingOOP是一种编程范式它将数据和操作数据的函数组织在一起形成对象。三个核心概念7.1.2 为什么要使用对象1. 多态代码更灵活你不需要知道对象的具体类型只需要知道它能做什么。# 不管 x 是字符串还是列表都能调用 count 方法 x hello world print(x.count(l)) # 3 x [a, b, c, a] print(x.count(a)) # 22. 封装代码更安全对象的内部状态对外部隐藏外部只能通过规定的方法访问避免误操作破坏数据3. 继承代码可复用子类自动拥有父类的所有方法只需添加或修改需要变化的部分避免重复编写代码。7.2 类Class的基本概念7.2.1 类和实例类是一类对象的模板或蓝图如人类实例是根据类创建的具体对象如具体的某个人张三属性属于对象的数据如人的名字、年龄方法属于对象的函数如人能说话、走路class Person: 定义一个人类 def set_name(self, name): self.name name def get_name(self): return self.name def greet(self): print(Hello, world! Im {}..format(self.name)) # 创建实例 foo Person() bar Person() foo.set_name(Luke Skywalker) bar.set_name(Anakin Skywalker) foo.greet() # Hello, world! Im Luke Skywalker. bar.greet() # Hello, world! Im Anakin Skywalker..2.2self参数的含义self指向当前实例本身是所有实例方法的第一个参数。foo Person() foo.set_name(Luke)执行foo.set_name(Luke)时Python 自动做两件事将foo作为self传入将Luke作为name传入所以set_name(Luke)等效于Person.set_name(foo, Luke)。self让每个实例拥有独立的属性空间foo.name和bar.name互不影响。7.2.3 属性、函数和方法方法是定义在类内部的函数它自动接收self作为第一个参数。但方法也可以被替换为普通函数class Class: def method(self): print(I have a self!) def function(): print(I dont...) instance Class() instance.method() # I have a self! instance.method function # 将方法替换为普通函数 instance.method() # I dont...关联方法当通过实例调用方法时self被自动绑定到该实例这种预绑定的方法称为关联方法。class Bird: song Squawk! def sing(self): print(self.song) bird Bird() bird.sing() # Squawk! birdsong bird.sing # 将关联方法赋给变量 birdsong() # Squawk!self 仍然指向 bird7.3 封装Encapsulation7.3.1 公开属性 vs 私有属性在 Python 中默认所有属性和方法都是公开的可以直接从外部访问和修改。class Person: def __init__(self, name): self.name name p Person(Alice) print(p.name) # Alice可以直接访问 p.name Bob # 可以直接修改7.3.2 名称改写Name Mangling——模拟私有要让属性或方法变成私有的外部无法直接访问可以在名称前加两个下划线__。class Secretive: def __inaccessible(self): # 私有方法 print(Bet you cant see me...) def accessible(self): print(The secret message is:) self.__inaccessible() # 内部可以调用 s Secretive() s.accessible() # The secret message is: # Bet you cant see me... # s.__inaccessible() # 报错AttributeError原理Python 会把__inaccessible改名为_Secretive__inaccessible在名称前加_类名。# 虽然不推荐但实际上仍可访问 s._Secretive__inaccessible() # Bet you cant see me...7.3.3 单下划线约定如果只想提醒外部不要访问而不是强制禁止可以用一个下划线_开头class Person: def __init__(self, name): self._name name # 约定这是内部使用的属性from module import *不会导入以单下划线开头的名称但直接访问仍然可以。两种方式的对比7.4 类的命名空间7.4.1 类变量 vs 实例变量类定义中的代码在类的命名空间中执行这里定义的变量是类变量被所有实例共享。class MemberCounter: members 0 # 类变量所有实例共享 def init(self): MemberCounter.members 1 # 每初始化一次计数加1 m1 MemberCounter() m1.init() print(MemberCounter.members) # 1 m2 MemberCounter() m2.init() print(MemberCounter.members) # 2 # 实例也可以访问类变量 print(m1.members) # 2 print(m2.members) # 27.4.2 实例变量遮蔽类变量如果给实例的同名属性赋值会在实例中创建新的实例变量它会遮蔽类变量。class MemberCounter: members 0 m1 MemberCounter() m1.members Two # 在 m1 实例中创建新属性 print(m1.members) # Two实例变量 print(MemberCounter.members) # 0类变量未变 m2 MemberCounter() print(m2.members) # 0m2 没有实例变量使用类变量7.5 继承Inheritance7.5.1 指定超类在类名后的括号中指定超类父类子类自动获得父类的所有方法。class Filter: 通用过滤器默认不过滤任何内容 def init(self): self.blocked [] def filter(self, sequence): return [x for x in sequence if x not in self.blocked] class SPAMFilter(Filter): # SPAMFilter 继承自 Filter 将 SPAM 过滤掉的过滤器 def init(self): self.blocked [SPAM] # 重写 init 方法 f Filter() f.init() print(f.filter([1, 2, 3])) # [1, 2, 3] s SPAMFilter() s.init() print(s.filter([SPAM, eggs, bacon, SPAM])) # [eggs, bacon]代码解读SPAMFilter没有定义filter方法但继承自Filter所以可以直接使用SPAMFilter重写了init方法将blocked设置为[SPAM]这种设计让你能创建大量不同的过滤器都复用同一个filter方法7.5.2 判断继承关系issubclass判断是否是子类print(issubclass(SPAMFilter, Filter)) # True print(issubclass(Filter, SPAMFilter)) # False__bases__查看父类print(SPAMFilter.__bases__) # (class __main__.Filter,) print(Filter.__bases__) # (class object,)7.5.3 判断实例类型isinstance判断对象是否属于某个类包括父类s SPAMFilter() print(isinstance(s, SPAMFilter)) # True print(isinstance(s, Filter)) # True因为 SPAMFilter 是 Filter 的子类 print(isinstance(s, str)) # False__class__查看对象所属的类print(s.__class__) # class __main__.SPAMFilter7.5.4 多重继承Python 支持多重继承一个类可以继承多个父类。class Calculator: def calculate(self, expression): self.value eval(expression) class Talker: def talk(self): print(Hi, my value is, self.value) class TalkingCalculator(Calculator, Talker): 同时继承 Calculator 和 Talker pass tc TalkingCalculator() tc.calculate(1 2 * 3) # 来自 Calculator tc.talk() # Hi, my value is 7来自 Talker方法解析顺序MRO当多个父类有同名方法时排在前面的类的方法会覆盖后面的。class A: def hello(self): print(Hello from A) class B: def hello(self): print(Hello from B) class C(A, B): # A 在前所以 A.hello 覆盖 B.hello pass c C() c.hello() # Hello from A多重继承的注意事项功能强大但容易增加复杂性除非必要否则应优先考虑单继承 组合7.6 接口和内省Introspection7.6.1 什么是接口接口协议是指对象对外暴露的方法和属性。在 Python 中我们不显式声明接口而是假定对象能完成我们需要它完成的任务——这就是鸭子类型Duck Typing如果走起来像鸭子叫起来像鸭子那么它就是鸭子。7.6.2 检查对象是否包含某属性hasattr检查对象是否有指定属性class TalkingCalculator(Calculator, Talker): pass tc TalkingCalculator() print(hasattr(tc, talk)) # True print(hasattr(tc, fly)) # Falsegetattr获取属性值可指定默认值print(getattr(tc, talk, None)) # bound method Talker.talk of ... print(getattr(tc, fly, None)) # Nonesetattr设置属性值setattr(tc, name, Mr. Gumby) print(tc.name) # Mr. Gumby检查属性是否可调用print(callable(getattr(tc, talk, None))) # True print(callable(getattr(tc, fly, None))) # False7.7 抽象基类Abstract Base Class7.7.1 为什么需要抽象基类抽象基类用于定义接口规范强制子类实现特定的方法。它本身不能被实例化。7.7.2 定义抽象基类使用abc模块Abstract Base Classes来定义。from abc import ABC, abstractmethod class Talker(ABC): 抽象基类定义 talk 方法必须被实现 abstractmethod def talk(self): passabstractmethod是一个装饰器标记该方法为抽象方法子类必须实现它。7.7.3 使用抽象基类如果子类没有实现抽象方法就无法实例化。class Knight(Talker): pass # k Knight() # TypeError: Cant instantiate abstract class Knight with abstract methods talk正确做法实现抽象方法。class Knight(Talker): def talk(self): print(Ni!) k Knight() k.talk() # Ni! print(isinstance(k, Talker)) # True7.7.4 注册虚拟子类你可以在不继承的情况下让一个类注册为抽象基类的子类。class Herring: def talk(self): print(Blub.) # 注册 Herring 为 Talker 的虚拟子类 Talker.register(Herring) h Herring() print(isinstance(h, Talker)) # True虽然 Herring 没有继承 Talker注意事项注册后isinstance返回True但 Python不保证该类真的实现了所有方法这是一种信任机制需要程序员自己确保实现了接口class Clam: pass Talker.register(Clam) c Clam() print(isinstance(c, Talker)) # True # c.talk() # 报错AttributeError没有实现 talk 方法7.8 面向对象设计的思考指南7.8.1 设计原则将相关的东西放在一起如果一个函数操作一个全局变量应该把它们封装成类的属性和方法对象间保持低耦合方法只关心自己的实例让其他对象自己管理自己慎用继承尤其是多重继承继承容易增加复杂性组合有时是更好的选择保持简单方法应短小精悍最好 30 秒内能读懂7.8.2 如何确定需要哪些类和方法步骤写下问题描述标记出所有名词、动词、形容词名词→ 可能的类动词→ 可能的方法形容词→ 可能的属性将方法和属性分配给对应的类7.8.3 通过用例验证设计记录一系列使用场景用例检查模型是否覆盖所有需求逐个推演每个场景发现遗漏就补充发现不合理就修改反复迭代直到满意
返回列表