python高级编程学习——03—(getattr函数和map函数、python垃圾回收机制、调试内存泄漏、调试和性能分析、经典的参数错误)

1、通过实例方法名字的字符串调用方法
我们有三个图形类

Circle,Triangle,Rectangle
# 圆,三角,矩形

他们都有一个获取图形面积的方法,但是方法名字不同,我们可以实现一个统一的获取面积的函数,使用每种方法名进行尝试,调用相应类的接口。

In [40]: s='abc123'

In [41]: s.find('123')         # 返回值是‘1’的下标
Out[41]: 3

In [42]: getattr(s, 'find')                # str字符串中有find方法
Out[42]: <function str.find>

In [43]: getattr(s, 'find')('123')          # 直接返回‘1’的下标值
Out[43]: 3

In [44]: getattr(s, 'pop')                   # str没有pop方法,list中有
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-44-b6b4c4fb0030> in <module>
----> 1 getattr(s, 'pop')

AttributeError: 'str' object has no attribute 'pop'

In [45]: getattr(s, 'pop', None)             # 返回为None

In [46]: def demo(x):
    ...:     return x ** 2
    ...:

In [47]: demo(2)
Out[47]: 4

In [48]: map(demo, [1,2,3])
Out[48]: <map at 0x19480932ef0>

In [49]: list(map(demo, [1,2,3]))              # 强制转换成list,再分别调用demo方法
Out[49]: [1, 4, 9]                             # map函数是对传入的函数进行指定的映射

In [50]:

getattr函数和map函数的举例:

from 实例方法名字字符串调用_lab1 import Triangle,Rectangle,Circle


shape1 = Triangle(3, 4, 5)
shape2 = Rectangle(4, 6)
shape3 = Circle(1)


def get_area(shape):
    method_name = ['get_area', 'getArea', 'area']
    for name in method_name:
        f = getattr(shape, name, None)                   # 传入的shape对象中是否有name的方法
        # None是为了返回None,方便下面的if判断
        if f:            # 假如方法名称存在
            return f()
  
      
print(get_area(shape1))
print(get_area(shape2))
print(get_area(shape3))

shape_list = [shape1, shape2, shape3]
area = list(map(get_area, shape_list))                   # map函数的使用
print(area)

2、python垃圾回收机制
Python 中一切皆对象。你所看到的一切变量,本质上都是对象的一个指针。

那么,怎么知道一个对象,是否永远都不能被调用了呢?
就是当这个对象的引用计数(指针数)为 0 的时候,说明这个对象永不可达,自然它也就成为了垃圾,需要被回收。

import os
import psutil

# 显示当前 python 程序占用的内存大小
def show_memory_info(hint):
    pid = os.getpid()
    p = psutil.Process(pid)
    
    info = p.memory_full_info()
    memory = info.uss / 1024. / 1024
    print('{} memory used: {} MB'.format(hint, memory))      # 8M

def func():
    show_memory_info('initial')
    # global a                                                 # 若申明成全局变量,就不会触发垃圾回收机制
    a = [i for i in range(10000000)]                         # 函数内部的局部变量
    show_memory_info('after a created')                      # 396M
    return a                                                 # 只加上返回值,内存会释放

f = func()                                                   # 若加上变量接收返回值a,内存就不会被释放
show_memory_info('finished')                                 # 9M      内存释放了,所以内存恢复

'''输出内容
initial memory used: 8.8671875 MB
after a created memory used: 396.66796875 MB
finished memory used: 9.6015625 MB
'''

引用计数的使用:
注意:计数不是累加的,只算当前的函数引用

import sys

a = []

# 查看变量的引用计数
print(sys.getrefcount(a))          # 2     getrefcount函数也要计算一次,所以值为2


def fun(a):
    print(sys.getrefcount(a))

fun(a)                             # 4


b = a                              # a的计数不是累加的,只算当前的函数引用
print(sys.getrefcount(a))          # 3

手动释放内存
del方法

import gc         # 清除没有引用的对象模块

import os
import psutil

# 显示当前 python 程序占用的内存大小
def show_memory_info(hint):
    pid = os.getpid()
    p = psutil.Process(pid)
    
    info = p.memory_full_info()
    memory = info.uss / 1024. / 1024
    print('{} memory used: {} MB'.format(hint, memory))  # 8M


def func():
    show_memory_info('initial')
    # global a                                                 # 若申明成全局变量,就不会触发垃圾回收机制
    a = [i for i in range(10000000)]  # 函数内部的局部变量
    show_memory_info('after a created')  # 396M
    return a  # 只加上返回值,内存会释放


show_memory_info('init')                                        # 初始是40M
a = [i for i in range(10000000)]
show_memory_info('after a created')                             # 427M

del a                                     # 手动清除
gc.collect()                             # 清除没有引用的对象
show_memory_info('finish')                                      # 释放了内存   40M
'''
init memory used: 40.625 MB
after a created memory used: 427.81640625 MB
finish memory used: 40.921875 MB
'''

循环引用
如果有两个对象,它们互相引用,并且不再被别的对象所引用,那么它们应该被垃圾回收吗?

引用次数为0的时候,垃圾回收启动的充分必要条件?
答:不是,是充分非必要条件

# 引用次数为0的时候,垃圾回收启动的充分必要条件?    不是,充分非必要条件
# 循环引用
import os
import psutil
import gc         # 清除没有引用的对象模块

# 显示当前 python 程序占用的内存大小
def show_memory_info(hint):
    pid = os.getpid()
    p = psutil.Process(pid)
    
    info = p.memory_full_info()
    memory = info.uss / 1024. / 1024
    print('{} memory used: {} MB'.format(hint, memory))


def func():
    show_memory_info('initial')
    a = [i for i in range(10000000)]
    b = [i for i in range(10000000)]
    show_memory_info('after a, b created')
    a.append(b)
    b.append(a)                                            # 循环引用,互相引用

func()
gc.collect()                                              # 手动清除之后,内存被释放   9.79296875 MB 
show_memory_info('finished')                              # 没有被释放内存
'''
initial memory used: 8.9375 MB
after a, b created memory used: 784.41015625 MB
finished memory used: 784.4140625 MB
'''

调试内存泄漏
虽然有了自动回收机制,但这也不是万能的,难免还是会有漏网之鱼。内存泄漏是我们不想见到的,而且还会严重影响性能。有没有什么好的调试手段呢?

就是 objgraph,一个非常好用的可视化引用关系的包。主要推荐两个函数,第一个是 show_refs(),它可以生成清晰的引用关系图。

import objgraph

a = [1, 2, 3]
b = [4, 5, 6]

a.append(b)
b.append(a)

objgraph.show_refs([a])

在这里插入图片描述
dot转图片:dot转成图片格式
总结

  • 垃圾回收是 Python 自带的机制,用于自动释放不会再用到的内存空间;
  • 引用计数是其中最简单的实现,不过切记,这只是充分非必要条件,因为循环引用需要通过不可达判定,来确定是否可以回收;
  • Python的自动回收算法包括标记清除和分代收集,主要针对的是循环引用的垃圾收集;
  • 调试内存泄漏方面, objgraph 是很好的可视化分析工具。

3、调试和性能分析
在程序中相应的地方打印,的确是调试程序的一个常用手段,但这只适用于小型程序。如果程序不大,每次运行都非常快,那么使用 print(),的确是很方便的。

用 pdb 进行代码调试
首先,要启动 pdb 调试,我们只需要在程序中,加入import pdb和pdb.set_trace()这两行代码就行了

a = 1
b = 2
import pdb
pdb.set_trace()
c = 3
print(a + b + c)

在 IDE 断点调试器中可以执行的一切操作,比如打印,语法是"p ":

(pdb) p a
1
(pdb) p b
2

除了打印,常见的操作还有“n”,表示继续执行代码到下一行

(pdb) n
-> print(a + b + c)

而命令l,则表示列举出当前代码行上下的 11 行源代码,方便开发者熟悉当前断点周围的代码状态

(pdb) l
  1    a = 1
  2    b = 2
  3    import pdb
  4    pdb.set_trace()
  5  ->  c = 3
  6    print(a + b + c)

命令“s“,就是 step into 的意思,即进入相对应的代码内部。
在这里插入图片描述
执行完成之后继续输入n,会报错:如上图所示。
除了这些常用命令,还有许多其他的命令可以使用
参考对应的官方文档:pdb命令官方文档

用 cProfile 进行性能分析
日常工作中,我们遇到这样的问题:在线上,我发现产品的某个功能模块效率低下,延迟高,占用的资源多,但却不知道是哪里出了问题。

这时,对代码进行 profile 就显得异常重要了。
这里所谓的 profile,是指对代码的每个部分进行动态的分析,比如准确计算出每个模块消耗的时间等。

计算斐波拉契数列,运用递归思想

import cProfile

def fib(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fib(n-1) + fib(n-2)

def fib_seq(n):
    res = []
    if n > 0:
        res.extend(fib_seq(n-1))
    res.append(fib(n))
    return res


print(fib_seq(30))
cProfile.run('fib_seq(30)')               # 注意分号

输出如下显示
在这里插入图片描述
参数介绍:

  • ncalls,是指相应代码 / 函数被调用的次数
  • tottime,是指对应代码 / 函数总共执行所需要的时间(注意,并不包括它调用的其他代码 / 函数的执行时间)
  • tottime percall,就是上述两者相除的结果,也就是tottime / ncalls
  • cumtime,则是指对应代码 / 函数总共执行所需要的时间,这里包括了它调用的其他代码 / 函数的执行时间
  • cumtime percall,则是 cumtime 和 ncalls 相除的平均结果。

4、经典的参数错误
python的类型分为 可变 和 不可变 (指的是内存地址会不会改变)
可变: 列表和字典
不可变: 数组、字符串、元祖

def add(a, b):
    a += b
    return a


a = 1
b = 2
c = add(a, b)
print(c)              # 3
print(a, b)           # 1 2

a = [1, 2]
b = [3, 4]
c = add(a, b)
print(c)             # [1, 2, 3, 4]
print(a, b)          # a=[1, 2, 3, 4]  b=[3, 4]

a = (1, 2)
b = (3, 4)
c = add(a, b)
print(c)             # (1, 2, 3, 4)
print(a, b)          # a=(1, 2) b=(3, 4)        

不可变类型
以int类型为例:实际上 i += 1 并不是真的在原有的int对象上+1,而是重新创建一个value为6的int对象,i引用自这个新的对象。

可变类型
以list为例。list在append之后,还是指向同个内存地址,因为list是可变类型,可以在原处修改。

小整数对象池
-5——256
在-5到256之间的数,python是已经创建好的对象,超出这个范围就不是,需要单独创建。

# 这是交互环境,不同于pycharm的运行环境
In [1]: a= 1

In [2]: b = 1

In [3]: a is b
Out[3]: True

In [4]: a==b
Out[4]: True

In [5]: a==2222
Out[5]: False

In [6]: a=2222

In [7]: b=2222

In [8]: a is b
Out[8]: False

In [9]: a == b
Out[9]: True

发布了50 篇原创文章 · 获赞 9 · 访问量 2084

猜你喜欢

转载自blog.csdn.net/weixin_42118531/article/details/103805967
今日推荐