七个Python内存优化技巧,你用过几个?

上一篇 / 下一篇  2024-03-12 13:48:08

  当我们的项目变得越来越大时,高效管理计算资源是一个不可避免的要求。不幸的是,与低级语言如C或C++相比,Python在内存效率方面似乎不够。那么,现在应该更改编程语言吗?
  当然不是。事实上,有许多方法可以显著优化Python程序的内存使用,从优秀的模块和工具到先进的数据结构和算法。本文将聚焦于Python的内置机制,并介绍7个原始但有效的内存优化技巧。掌握这些技巧将显著提高我们的Python编程技能。
  1. 在类定义中使用__slots__
  Python作为一种动态类型语言,在面向对象编程方面更加灵活。一个很好的例子是在运行时向Python类中添加额外的属性和方法的能力。例如,下面的代码定义了一个名为Author的类。最初它有两个属性name和age。但是我们可以很容易地在后来添加一个额外的属性:
  class Author:
      def __init__(self, name, age):
          self.name = name
          self.age = age
  me = Author('Yang Zhou', 30)
  me.job = 'Software Engineer'
  print(me.job)
  然而,每个硬币都有两面。这种灵活性在底层浪费了更多的内存。因为Python类的每个实例都维护一个特殊的字典(__dict__)来存储实例变量。这个字典由于其基于哈希表的实现方式而固有地内存效率低下,占用大量内存。
  在大多数情况下,我们不需要在运行时更改实例的变量或方法,而且在类定义之后__dict__将不会改变。因此,如果我们能避免维护__dict__字典,那就更好了。Python为此提供了一个神奇的属性:slots。它通过指定类的所有有效属性的名称来充当白名单:
  class Author:
      __slots__ = ('name', 'age')
      def __init__(self, name, age):
          self.name = name
          self.age = age
  me = Author('Yang Zhou', 30)
  me.job = 'Software Engineer'
  print(me.job)
  #AttributeError: 'Author' object has no attribute 'job'
  如上所示,我们不能再在运行时添加job属性。因为__slots__白名单只定义了两个有效属性name和age。从理论上讲,由于属性现在是固定的,Python不需要为其维护一个字典。它只需为__slots__中定义的属性分配必要的内存空间。
  让我们编写一个简单的比较程序,看看它是否确实起作用:
  import sys
  class Author:
      def __init__(self, name, age):
          self.name = name
          self.age = age
  class AuthorWithSlots:
      __slots__ = ['name', 'age']
      def __init__(self, name, age):
          self.name = name
          self.age = age
  # Creating instances
  me = Author('Yang', 30)
  me_with_slots = AuthorWithSlots('Yang', 30)
  # Comparing memory usage
  memory_without_slots = sys.getsizeof(me) + sys.getsizeof(me.__dict__)
  memory_with_slots = sys.getsizeof(me_with_slots)  # __slots__ classes don't have __dict__
  print(memory_without_slots, memory_with_slots)
  # 152 48
  print(me.__dict__)
  # {'name': 'Yang', 'age': 30}
  print(me_with_slots.__dict__)
  # AttributeError: 'AuthorWithSlots' object has no attribute '__dict__'
  正如上面的代码所演示的,由于使用了__slots__,me_with_slots实例不具有__dict__字典。与必须保留额外字典的me实例相比,这有效地节省了内存资源。
  2. 使用生成器
  生成器是Python中的惰性求值版本的列表。它们就像元素生成工厂:仅在调用next()方法时生成一个项目,而不是一次计算所有项目。因此,当处理大型数据集时,它们非常内存高效。
  def number_generator():
      for i in range(100):
          yield i
  numbers = number_generator()
  print(numbers)
  print(next(numbers))
  #0
  print(next(numbers))
  #1
  上面的代码展示了编写和使用生成器的基本示例。关键字yield是生成器定义的核心。应用它意味着只有在调用next()方法时才会产生项目i。现在,让我们比较一下生成器和列表,看看哪个更内存高效:
  import sys
  numbers = []
  for i in range(100):
      numbers.append(i)
  def number_generator():
      for i in range(100):
          yield i
  numbers_generator = number_generator()
  print(sys.getsizeof(numbers_generator))
  #112
  print(sys.getsizeof(numbers))
  #920
  上述程序的结果证明了使用生成器可以显著节省内存使用。顺便说一下,如果我们将列表推导式的方括号改成括号,它将变成生成器表达式。这是在Python中定义生成器的更简便的方法:
  import sys
  numbers = [i for i in range(100)]
  numbers_generator = (i for i in range(100))
  print(sys.getsizeof(numbers_generator))
  #112
  print(sys.getsizeof(numbers))
  #920
  3. 利用内存映射文件支持大文件处理
  内存映射文件I/O,简称“mmap”,是一种操作系统级别的优化。
  它实现了需求分页,因为文件内容并不立即从磁盘读取,并且最初根本不使用物理RAM。实际从磁盘读取是在特定位置被访问时以懒惰的方式执行的。
  —— 维基百科
  简单来说,当使用mmap技术内存映射文件时,它在当前进程的虚拟内存空间中直接创建文件的映射,而不是将整个文件加载到内存中。映射而不是加载整个文件可以节省大量内存。
  听起来很复杂?幸运的是,Python已经提供了一个用于使用这种技术的内置模块,因此我们可以轻松利用它,而不必考虑操作系统级别的实现。例如,这是在Python中使用mmap进行文件处理的方法:
  import mmap
  with open('test.txt', "r+b") as f:
      # memory-map the file, size 0 means whole file
      with mmap.mmap(f.fileno(), 0) as mm:
          # read content via standard file methods
          print(mm.read())
          # read content via slice notation
          snippet = mm[0:10]
          print(snippet.decode('utf-8'))
  如上所演示的,Python使得内存映射文件I/O技术的使用变得方便。我们所需要做的就是简单地应用`mmap.mmap()`方法,然后使用标准文件方法或甚至切片表示法处理打开的对象。
  4. 减少全局变量的使用
  全局变量在程序运行期间始终驻留在内存中,因为它们具有全局范围。因此,如果一个全局变量保存一个大型数据结构,它将在整个程序生命周期中占用内存,可能导致内存使用效率低下。我们应该在Python代码中尽量减少全局变量的使用。
  5. 利用逻辑运算符的短路求值
  这个技巧似乎微妙,但巧妙地使用它将极大地节省程序的内存使用。例如,下面是一个简单的代码片段,根据两个函数返回的布尔值得到最终结果:
  result_a = expensive_function_a()
  result_b = expensive_function_b()
  result = result_a if result_a else result_b
  上面的代码能够工作,但实际上执行了两个内存效率低下的函数。获取相同结果的更聪明的方法如下:
  result = expensive_function1() or expensive_function2()
  由于逻辑运算符遵循短路求值规则,上述代码中的`expensive_function2()`将不会在`expensive_function1()`为True时执行。这将节省不必要的内存使用。
  6. 谨慎选择数据类型
  一位经验丰富的Python开发者会仔细而准确地选择数据类型。因为在某些场景中,使用一个数据类型比另一个更节省内存。
  元组比列表更节省内存
  由于元组是不可变的(在创建后不能更改),它允许Python在内存分配方面进行优化。然而,列表是可变的,因此需要额外的空间来容纳潜在的修改。
  import sys
  my_tuple = (1, 2, 3, 4, 5)
  my_list = [1, 2, 3, 4, 5]
  print(sys.getsizeof(my_tuple))
  #80
  print(sys.getsizeof(my_list))
  #120
  如上面的片段所示,即使它们包含相同的元素,元组`my_tuple`使用的内存比列表更少。因此,如果在创建后不需要更改数据,我们应该更喜欢使用元组而不是列表。
  (1) 数组比列表更节省内存
  Python中的数组要求元素是相同的数据类型(例如,全部整数或全部浮点数),但列表可以存储不同类型的对象,这必然需要更多的内存。因此,如果列表的元素都是相同类型,使用数组会更节省内存:
  import sys
  import array
  my_list = [i for i in range(1000)]
  my_array = array.array('i', [i for i in range(1000)])
  print(sys.getsizeof(my_list))
  #8856
  print(sys.getsizeof(my_array))
  #4064
  (2) 优秀的数据科学模块比内置数据类型更高效
  Python是数据科学的主导语言。有许多强大的第三方模块和工具提供了更多的数据类型,例如NumPy和Pandas。如果我们只需要一个简单的一维数字数组,并且不需要NumPy提供的广泛功能,那么Python内置的数组可能是一个不错的选择。
  但是,当涉及到复杂的矩阵操作时,对于所有数据科学家来说,使用NumPy提供的数组是第一选择,可能是最好的选择。
  7. 对相同的字符串应用字符串驻留技术
  下面的代码可能会使许多开发者感到困惑:
  >>> a = 'Y'*4096
  >>> b = 'Y'*4096
  >>> a is b
  True
  >>> c = 'Y'*4097
  >>> d = 'Y'*4097
  >>> c is d
  False
  正如我们所知,`is`运算符用于检查两个变量是否引用内存中的同一对象。它与`==`运算符不同,后者用于比较两个对象是否具有相同的值。那么为什么`a is b`返回True,而`c is d`返回False呢?
  这里有Python中的一个隐秘技巧 —— 字符串驻留技术。如果有几个值相同的小型字符串,它们将由Python隐式地进行驻留,并引用内存中的同一对象。定义小型字符串的神奇数字是4096。由于`c`和`d`的长度都是4097,它们是内存中的两个对象而不是一个。不再有隐式的字符串驻留。因此,在执行`c is d`时得到False。
  字符串驻留是一种优化内存使用的强大技术。如果我们想要显式地进行驻留,sys.intern()方法就派上用场了:
  >>> import sys
  >>> c = sys.intern('Y'*4097)
  >>> d = sys.intern('Y'*4097)
  >>> c is d
  True
  顺便说一下,除了字符串驻留,Python还对小整数应用驻留技巧。我们也可以利用它进行内存优化。

TAG: 软件开发 Python

 

评分:0

我来说两句

Open Toolbar