zzzzou 阅读(14) 评论(0)

python学习-函数

标签: python 函数


目录

[toc]

一、文件处理

python的内置函数open提供了对文件的处理功能。open函数会调用os的系统接口,得到一个类文件对象f,此f对象将作为对文件操作的标识符。从行为动作划分,文件处理分为读取和写入。从操作的数据类型划分,文件处理分为操作字符串和操作二进制数据。

1、读取文件

with open(file_path, 'r', encoding='utf-8') as f:
    data = f.read()

注意:
: 1、使用with来妥善处理文件的关闭问题。
2、给定的file_path是文件的路径,python默认当前目录(相对路径),也可以给出绝对路径,不过在windows下的路径要注意 \ 的问题。
3、'r'表示了读取模式,如果使用'rb'则意味着使用读取二进制模式,两者的区别在于,'r'模式会使用encoding参数指定的字符编码格式对文件进行解码,并转换成unicode到内存中作为str类型存在,而'rb'则不会解码,而是直接将硬盘上的二进制数据读取到内存中作为bytes类型存在。
4、使用'r'更适用于需要对文件内容执行显式的字符串操作,一般有需要展示内容的需求。使用'rb'更适用于对数据进行非显式操作,如直接传输、压缩等等,不需要展示内容给用户。
5、readreadlinereadlines分别对应:读取整个文件作为一个字符串、读取一行作为一个字符串、读取所有行并返回一个行列表。

2、写入文件

with open(file_path, 'w', encoding='utf-8') as f:
    f.write(msg)

注意:
: 1、使用'w'模式将会首先寻找此文件是否存在,如果不存在则新建,如果存在,则会清空原有文件的所有内容。
2、使用write函数对文件执行写入操作,msg应该是str类型。

3、文件操作函数

file_path = 'text.py'

with open(file_path, 'w+', encoding='utf-8') as f:
    print(f.fileno())  # 获取文件对象在内核中的索引值

    f.write('hello, world')  # 写入数据
    f.flush()  # 在不关闭文件的情况下,强制将缓存中的内容刷入硬盘
    f.seek(0)  # 移动seek到开头
    print('这里有数据', f.read())  # 读取所有内容

    print(f.readable())  # 是否可读
    print(f.writable())  # 是否可写
    print(f.seekable())  # 是否可移动seek

    f.seek(0)
    f.truncate()  # 从seek处开始截取到文件末尾
    print('这里没数据', f.read())  # 读取内容

    print(f.tell())  # 获取当前seek位置

4、注意事项

  • [x] 使用+来扩展原有的文件处理模式,如w+r+
  • [x] 文件的操作要特别注意当前的seek位置,seek位置会随着读取和写入而后移。
  • [x] 文件对象f可以被迭代,即:for line in f,可以每次获取一行,类似readline
  • [x] 可以使用charsetdetect函数对二进制数据的编码格式进行推测。
  • [x] 使用aab模式来对文件追加内容,使用此模式时seek将会自动移到文件末尾。
  • [x] 使用seek(0)函数配合truncate()函数完成文件清空操作。
  • [x] os模块提供了很多函数用于处理文件和目录。
  • [x] 所有直接对硬盘数据的修改都会覆盖当前内容,无法直接移动位置,必须通过读取到内容、修改内容、写入硬盘的方式。

二、函数基础

函数在python中是第一类对象,即函数可以像变量一样被赋值。函数名是函数对象的引用,函数对象和普通变量没有太大的区别,普通对象比如数字对象可以四则运算,集合类型对象可以迭代,函数对象则可以执行。函数对象保存了执行代码和执行时的上下文环境,函数每次被执行的时候都要在内存中开辟一个新的函数栈用于保存函数的执行上下文,然后执行函数对象中的代码,一旦函数执行完毕就会通过return返回函数执行的结果,并销毁此函数栈,从而结束一个函数的运行。

1、函数体

函数体用于定义函数执行时的代码,函数体中的代码会被函数对象所保存并在函数被调用的时候被执行。函数被()所调用。

2、输入

函数可以被认为是一个小型程序或者子例程,是一个计算机的微型模型,可以接受调用者提供的输入。调用者的输入不同,函数执行的过程和结果也不同,所以虽然函数体中的代码已经预先定义好了,但是调用者可以通过不同的输入来实现不同的执行效果。而函数的形参就是用于接收可变化的输入值的变量。

3、输出

函数一定有输出,即使没有显式的声明return语句,也会在执行代码的最后一行添加return None。函数的输出用于表示此函数的执行结果,不论此函数是否需要输出结果给调用者,输出都被要求必须提供,哪怕提供的是None。一般调用者通过函数的输出来判断执行的结果,并可以通过输出来对外部函数的执行后续流程做一定的决策。

4、局部变量/变量作用域

因为函数的执行上下文存在于函数栈,而函数执行完毕后将会销毁此函数栈,故在函数运行过程中定义的所有变量都被称为局部变量,因为这些变量都存活在此函数栈中。可以在函数运行过程中通过locals()函数来获取当前函数的函数栈中的局部变量命名空间内容。正因为变量仅存活在函数栈中,故这些变量的作用域也就只在函数执行过程中生效。一旦函数执行完毕,将无法访问函数栈中的变量地址,当然,除非你显式的return这些变量地址

5、匿名函数

所谓匿名函数,即是一个没有函数名的函数,如下两个声明是完全等价的:

a = 2  # 声明对象2,名字是a
2  # 声明无名对象2
#===============
def f():  # 声明有名对象函数, 名字是f
    print('hello')

lambda x:print('hello')  # 声明无名对象函数  --> 匿名函数

匿名函数一般配合mapfilterreduce等高阶函数使用,此时匿名函数作为其他函数的输入。

6、函数调用

每一个函数的执行都依赖于对应的函数栈,即使是python程序没有定义任何函数,此程序之所以会运行,也是依赖于顶层函数栈提供了全局命名空间。在函数发生调用的时候,cpu代码执行视角将会从当前函数跳转到另一个函数所在的内存地址,一旦被调函数执行完毕,cpu代码执行视角将会回到主调函数的下一条代码地址处,主调函数获得被调函数的返回值。

7、命名空间LEGB

local 当前函数所在的命名空间
enclosing 嵌套函数所在的命名空间,从儿子到父亲到爷爷的寻找链
global 全局命名空间,即模块的命名空间
builtin 内置命名空间,即模块的上一级,定义了所有的内置函数和变量
检测顺序是:L --> E --> G --> B --> 报错

8、函数参数解包

函数中的*args和**kw提供了解包,args代表元组,kw代表字典,可以这样使用:

def show(a, b):
    print(a, b)

x = (1,2)
show(*x)

#=========

def show(a=1, b=2):
    print(a, b)

x = {'a': 10, 'b': 20}
show(**x)

9、递归

所谓的递归,就是函数调用过程中调用自己,这样会产生和无限循环类似的无限调用自己。函数每一次调用都需要创建一个函数栈,而因为主调函数并没有执行完毕,所以主调函数的函数栈不会销毁,而是会等待被调函数返回结果。无限递归,就会无限产生新的被调函数栈,保留主调函数栈,而导致函数栈溢出python函数栈设置为最大1000层。

使用递归一定需要设置递归停止条件,就像使用循环一定要设置循环退出条件一样。

使用递归可以方便的解决某些需要不断调用自身算法的问题,但是递归效率本身并不高,因为需要不停的 创建新的函数栈和销毁函数栈。

三、内置函数

print(dir(list))  # 查看对象的所有属性名称列表
print(sorted([233,34,54,56,56,67,67]))  # 对序列进行排序,返回新列表对象

print(eval('3 + 5'))  # 执行字符串代码,得到返回值,无法执行赋值等更改程序内容的语句
exec('a = 2')  # 执行多行字符串,返回值永远是None,但是可以执行赋值等更改程序内容的语句
print(a)

filter(lambda x: x%2==0, [34,34,54,54,56,6,2,2,3,34])  # 执行序列过滤
map(lambda x: str(x), [2334,34,4,5])  # 执行序列各元素统一处理

from functools import reduce
reduce(lambda x, y: x+y, [23,34,34,45,5,56])  # 执行序列回归累计操作

print('msg is here', 'another msg', sep='!', end='\n!!!!\n')  # print函数的参数使用

f = lambda x: 2
callable(f)  # 对象是否可调用判断

print(list(zip([23,23,32,43], [4,34,43,34])))  # 执行多个序列的打包操作,会截断序列以最短的为准,多个序列元素打包成元组

四、函数进阶

1、闭包

函数一旦定义完毕,就会将执行代码和上下文环境保存到函数对象中。上下文环境除了保留当前层次的环境之外,还会保留外层嵌套的上下文环境。一旦此函数被作为变量返回给外部函数,外部函数可以随时调用,被调用时,内层函数即可访问之前保留的嵌套函数中的上下文。此即:闭包,即,将执行代码和执行环境一起包裹,一旦被执行,就可以使用嵌套的上下文环境。闭包可以用于保留嵌套函数的变量状态。

def outer():
    a = 20
    def inner():
        print(a)

    return inner

f = outer()
f()  # 执行inner的时候,可以访问到a = 20

2、装饰器函数

正因为闭包的性质,可以对函数进行功能扩展,在不修改原有函数代码的前提下,增加额外的功能。装饰器也是一种高阶函数,将原函数作为输入,对原函数进行功能扩展并返回与原函数同名的新函数。新函数一旦被执行,将会把原函数功能和扩展功能一起执行。

func_map = {}

def register(key):
    def wrapper_outer(func):

        def wrapper_inner(*args, **kw):
            result = func(*args, **kw)
            print('扩展功能')
            return result

        func_map[key] = wrapper_inner

        return wrapper_outer
    return wrapper_outer

@register('show')
def show(msg):
    print('this is show msg:', msg)

msg = 'hello, world'
func_map['show'](msg)

3、生成器函数

生成器函数可以被设计成永远不会销毁函数栈,但是却可以和主调函数通过yield进行函数栈的切换。生成器函数最大的好处有如下几个:

1.延迟计算,生成器需要手工执行next或者send才会执行生成器函数代码

2.可以和主函数进行函数栈切换,效率高,串行无锁,无变量安全问题

3.生成器函数可以通过yield返回值,主调函数通过send发送值,两者可以交互

4.可以用于实现协程

log_path = 'xxx.log'

def logger_generator(log_path):
    log_count = 0
    with open(log_path, 'w', encoding='utf-8') as f:  # 准备log文件,预先清空内容
        pass
    msg = yield 'ok'

    while True:  # 永远执行
        if msg == 'stop':
            return 'stop'  # 引发stopiteration,主调函数要try
        else:
            log_count += 1
            msg = '# [{log_count}] --> {msg}\n'.format(log_count=log_count, msg=msg)  # 准备日志信息

            with open(log_path, 'a', encoding='utf-8') as f:  # 记录日志到文件
                f.write(msg)

            log_summary = '%d informations has been logged!' %log_count  # 准备日志汇总信息
            msg = yield log_summary  # 返回截止目前的日志汇总信息,同时等待主调发送新的日志消息

logger = logger_generator(log_path)
logger.send(None)

print(logger.send('用户登录'))
print(logger.send('用户密码修改'))

五、几个技术问题

1、解包

li = [1,2,3,4,5]
first, *mid, last = li
print(first, mid, last)

first, second, *_ = li
print(first, second, _)

a, b, *c = range(5)
print(a, b, c)

dic1 = {'a':1}
dic2 = {'b':2}
dic3 = {**dic1, **dic2}
print(dic3)

dic = {
    'name': 'hz',
    'age': 26,
}
msg = 'name is: {name}, age is: {age}'.format(**dic)
print(msg)

1.使用解包可以让代码更简洁

2.***分别用于解包序列和字典

3.使用*_来承载不需要的值,_变量也可以被使用

2、值交换

如下两者是等价的:

a = 2
b = 33

temp = a
a = b
b = temp
print(a, b)

#=========

a = 3
b = 44
a, b = b, a
print(a, b)

python的值交换会自动帮你处理temp临时变量

3、默认参数陷阱

a = 20

def show(x=a):
    print(x)

a = 30
show()
show(a)

print('#=========')

def show(x=[]):
    x.append(99)
    print(x)

show()
show()
show()

结果是:

20
30
#=========
[99]
[99, 99]
[99, 99, 99]

1.默认参数在函数被编译的时候就固定了引用对象

2.如果不显式的提供show(a)就会打印原来的固定对象20

3.如果默认参数指向可变对象,因为可变对象可以修改值,会导致非期望的结果如[99,99,99]

4.默认参数一定要指向不可变对象,而且调用函数的时候尽量显式的提供值

4、生成器深入理解

生成器的好处:

  • [x] 保存计算规则、算法、节省内存空间,随时计算。
  • [x] 高效的函数栈切换,生成器函数还可以嵌套多层,多个yield提供更灵活的切换控制
  • [x] 保存生成器函数中的上下文环境不会被销毁,只要切换进去就可恢复使用这些变量值
  • [x] 可以和主调函数或者其他子层生成器函数相互协作完成程序执行
  • [x] 使用sendnextyield来提供消息、信号交互
  • [x] 可以实现协程,高效串行执行,无变量安全问题,不需加锁