Skip to content
0

Python元编程

主要是针对类的魔法方法,像 __lt__ 这种类似的就不说了,和运算符重载一个道理(所以说编程语言的思想都是差不多的)。

__dict__

属性字典:一个用于存储对象的(可写)属性的字典或其他映射对象。

  1. 只有通过 __init__ 初始化的属性才能存在于实例的 __dict__(也可能不会,见描述器),类的方法和属性不存在于实例的 __dict__ 中,而在类的 __dict__ 中。
  2. 实例添加成员属性或方法时,会写入到实例的 __dict__ 中,类的 __dict__ 不会写入。
  3. 魔法方法不应该添加到实例对象中(隐式调用会报错),而应该始终添加在实例的类中,以便始终一致地由解释器发起调用。
  4. 父类和子类都有各自的 __dict__ ,子类不会包含父类的 __dict__
  5. 类属性为可变对象时,实例通过可变对象的方法可以直接修改这个类的此属性。
python
class Person:
   ADDRESS = []  # 类属性为可变对象

   def __init__(self, name)
      self.name = name

   def __str__(self):
      return f"My name is {self.name}"

   __repr__ = __str__

p1 = Person("Tom")
print(p1.__dict__)  # {'name': 'Tom'}
p2 = Person("Jeck")
print(p2.ADDRESS)  # []
p1.ADDRESS.append("湖北")
# p1.ADDRESS = ["湖北"]  # 此方法不会改变类的属性,这属于给实例添加属性
print(p2.ADDRESS)  # ["湖北"]

p1.__len__ = len  # 不要把魔法方法添加到实例对象中

__all__

控制模糊导入时,哪些函数或类等能被其他模块导入。注意:对精准导入无效。

python
__all__ = [
   "MyClass"
]

class MyClass:
   pass

class TestClass:
   pass
python
from a import *  # 模糊导入,此模块中只能使用 __all__ 中定义的 MyClass
from a import TestClass  # 精准导入,不受 __all__ 的影响

__solts__

__slots__ 主要用于程序需要创建大量(可能百万级别)对象,可能导致内存占用过大的场景。

  • 在Python中,每个类都有实例属性。默认情况下Python用一个字典(__dict__)来保存一个对象的实例属性。这非常有用,因为它允许我们在运行时去设置任意的新属性,但会占用多余的内存。

  • 限定一个类创建的实例只能有固定的属性(实例变量),不允许对象添加列表以外的属性。注意是实例的变量,不是类变量。含有 __solts__ 属性的类所创建的实例没有 __dict__ 属性,即此实例不用字典来存储对象的属性。

  • 当一个类的父类没有定义 __solts__ 属性,父类中的 __dict__ 属性总是可以访问到的,所以只在子类中定义 __solts__ 属性而不在父类中定义是没有意义的。

  • 如果定义了 __solts__ 属性,还是想在之后添加新的变量,就需要把 __dict__ 字符串添加到 __solts__ 的元组里。

  • 定义了 __solts__ 属性,还会消失的一个属性是 __weakref__,这样就不支持实例的 weak reference,如果还是想用这个功能,同样,可以把__weakref__ 字符串添加到元组里。

  • __solts__功能是通过 descriptor 实现的,会为每一个变量创建一个 descriptor。

  • __solts__ 的功能只影响定义它的类,因此,子类需要重新定义 __slots__ 才能有它的功能。

python
class Person:

   __slots__ = ('name', 'age')

   def __init__(self, name: str, age: int):
      self.name = name
      self.age = age

p = Person("tom", 18)
print(p.__dict__)  # 异常
p.address = ""  # 异常

上下文管理器

  • 在类中实现上下文管理器比较简单,实现 __enter____exit__ 即可。
  • 函数要实现上下文管理器,需要借助内建库 contextlib
python
import traceback

class Resource:
   def __init__(self, name):
      self.name = name

   def __enter__(self):
      print('===connect to resource===')
      return self

   def __exit__(self, exc_type, exc_val, exc_tb):
      """异常可以在此捕获"""
      print(exc_type)
      print(exc_val)
      print(''.join(traceback.format_exception(exc_type, exc_val, exc_tb)))

      print('===close resource connection===')
      return True  # 返回 True 表示不将异常继续抛出

   def operate(self):
      print(f'==={self.name} in operation===')

   @staticmethod
   def error():
      print(1 / 0)


with Resource('wgj') as res:
   res.operate()
   res.error()
python
import contextlib

@contextlib.contextmanager
def open_func(file_name):
   # 此部分相当于 __enter__方法:打开资源
   file_handler = open(file_name, 'w')

   try:
      yield file_handler
   except Exception as e:
      # 此部分相当于 __exit__ 方法:处理异常
      print(f'the exception was thrown: {e}')
   finally:
      # 此部分相当于 __exit__ 方法:关闭资源
      print('close file: ', file_name)
      file_handler.close()

with open_func('mytest.txt') as f:
   f.read()

属性代理

熟悉 Vue 的应该会知道,它的核心就是 javascript 中的数据代理 Proxy。同样作为动态语言的 Python 当然也可以:主要涉及魔法方法 __getattribute____getattr____setattr__ 以及 __delattr__

__getattribute__

  1. 当实例访问属性或方法时会最先调用此方法,通过类来访问类属性不会调用。
  2. 如果类还定义了 __getattr__ 方法,则后者不会被调用,除非 __getattribute__ 显式地调用它或是引发了 AttributeError。
  3. 应当返回基类同名函数的返回值或断言 AttributeError 异常,否则可能会无限递归。
  4. 魔法方法的隐式调用会跳过此方法
  5. 基类(object)的此方法是对不存在的属性访问抛出 AttributeError 异常。
Python
class Person:

   def __init__(self, name, age):
      print("__init__ start")
      self.name = name
      self.age = age
      print("__init__ end")

   def __getattribute__(self, item):
      """
      访问属性或方法时,无条件最先调用!
      :param item: 访问的属性名
      :return: 
      """
      print(f"__getattribute__ called... item: {item}")
      if item == "names":
         return "may be name"  # 将不会在调用 __getattr__
      # elif item == "age":
      #     return getattr(self, item)  # 无限递归
      # print(self.__dict__)  # 无限递归
      # 基类方法默认对于不存在的实例变量引发 AttributeError 异常。
      return super().__getattribute__(item)
      # 因为 Person 的父类就是 object ,所以可以用 super 方法,如果父类不是 object 也可直接调用。
      # return object.__getattribute__(self, item)
   
   def __getattr__(self, item):
      """
      何时被调用:
         1. 当定义了 __getattribute__ 方法时,在该方法中显示调用或抛出 AttributeError
         2. 没有定义 __getattribute__ 方法时,获取不存在的属性
      :param item: 访问的属性名
      :return: 任何类型,类、实例、函数等
      """
      print(f"__getattr__ called... item: {item}")
      if item == "names":
         return "names 不存在"
      elif item == "age":
         return "年龄可不能随便告诉你哟!"
      elif item == "run":
         return lambda: print("开始跑起来吧")

      return f"实例没有这个属性:{item}"

   def __setattr__(self, key, value):
      """
      一个属性被尝试赋值时被调用(如p.x = a)
      注意:一定不能像 self.key = value 这样写,会进入死循环

      :param key: 属性名
      :param value: 属性值
      :return: 
      """
      print(f"__setattr__ called... {key}{value}")
      if key == "name":
         if value == "":
               raise Exception("name is not empty str")
         # 如:只要是给name赋值,全部为 XX
         super().__setattr__(key, "XX")

      # 基类的此方法默认是将键值对设置到实例的 __dict__ 字典中。
      object.__setattr__(self, key, value)

   def __delattr__(self, item):
      print(f"__delattr__ called... item: {item}")
      if item == "name":  
         return  # 删不掉 name 属性
      super().__delattr__(item)

   def __len__(self):
      return 10

   def eat(self, fruit):
      print(f"{self.name} is eating {fruit}")

有时候,你要验证多个问题,分别写在多个函数里面,要运行哪个就改代码调用哪个函数,这样比较麻烦。下面教大家一种方法,不用改代码直接运行。

那就是 Pycharm + Python 的单元测试库。Pycharm 会在以 test 开头的方法处生成一个运行按钮,点击即可运行此方法。

Python
from unittest import TestCase

class TestPerson(TestCase):
   def setUp(self):
      # 在 __init__ 中的初始化也是赋值,所以会调用 __set__ 
      self.p = Person("Tom", 18)

   # 以test开头的方法
   def test_get(self):
      """测试获取属性"""
      print(self.p.__dict__)
      # 实际的单元测试应该类似下面这种写法,我这里是为了打印输出用了print
      assert self.p.name == "XX"  
      print("p.names:", self.p.names)
      print("p.age:", self.p.age)
      getattr(self.p, "name")
      # 函数调用也属于属性获取,会先调用__getattribute__
      self.p.eat("apple")
      self.p.run()  # 先调用__getattribute__,没找到再调用了__getattr__

      # 调用__getattribute__后由于没找到引发异常调用__getattr__
      print(self.p.love)

   def test_set(self):
      self.p.address = "www"
      print(self.p.address)

   def test_magic_function(self):
      print(self.p.__len__())  # 魔法方法的显示调用,这将调用__getattribute__
      print(len(self.p))       # 魔法方法的隐式调用,这将跳过__getattribute__

   def test_del(self):
      print(self.p.__dict__)
      delattr(self.p, "name")
      delattr(self.p, "age")
      print(self.p.__dict__)
      print(self.p.name)
      print(self.p.age)

下面是输出结果:

console
__init__ start
__setattr__ called... name:Tom
__setattr__ called... age:18
__init__ end
__setattr__ called... address:www
__getattribute__ called... item: address
www
console
# 省略了 __init__ 里面的 __setattr__ 输出,下同
__getattribute__ called... item: __dict__
{'name': 'XX', 'age': 18}
__getattribute__ called... item: name
__getattribute__ called... item: names
p.names: may be name
__getattribute__ called... item: age
p.age: 18
__getattribute__ called... item: name
__getattribute__ called... item: eat
__getattribute__ called... item: name
XX is eating apple
__getattribute__ called... item: run
__getattr__ called... item: run
开始跑起来吧
__getattribute__ called... item: love
__getattr__ called... item: love
实例没有这个属性:love
console
__getattribute__ called... item: __len__
10
10
console
__getattribute__ called... item: __dict__
{'name': 'XX', 'age': 18}
__delattr__ called... item: name
__delattr__ called... item: age
__getattribute__ called... item: __dict__
{'name': 'XX'}  # 删不掉
__getattribute__ called... item: name
XX
__getattribute__ called... item: age
__getattr__ called... item: age
年龄可不能随便告诉你哟!

描述器

定义:含有__set____get____delete__ 任一个方法的类称为描述器类,其实例作为另一个类(称为所有者)的属性时就会起作用,赋值时自动调用 __set__,访问时自动调用 __get__,删除属性时自动调用 __delete__。定义了 __set____delete__ 的称为数据描述器;只有 __get__ 的称为非数据描述器

python
class DataDesc:
   def __init__(self, default=0):
      self._score = default

   def __set__(self, instance, value):
      print("数据描述器__set__被调用了")
      if not isinstance(value, int):
         raise TypeError(f'Score must be integer, but value is {type(value)} {value}')
      if not 0 <= value <= 100:
         raise ValueError('Valid value must be in [0, 100]')

      self._score = value

   def __get__(self, instance, owner):
      print(f"数据描述器__get__被调用了(ins={instance}, owner={owner})")
      return self._score

   def __delete__(self, instance):
      print("__delete__被调用了")
      del self._score
python
class NoDataDesc:
   def __init__(self, default=0):
      self._score = default

   def __get__(self, instance, owner=None):
      """
      :param instance: 所有者的实例对象(当通过类直接访问属性时,此为None)
      :param owner:    所有者的类
      :return:
      """
      print(f"非数据描述器__get__被调用了(ins={instance}, owner={owner})")
      return self._score

使用方式如下:

python
class Student:
    math = DataDesc(0)
    english = NoDataDesc(0)
    fake_score = NoDataDesc(0)

    def __init__(self, name, math, english):
        self.name = name
        # self.math = DataDesc(math)  # 必须作为类的属性时才起作用
        self.math = math  # 调用 __set__
        self.english = english

    def __getattribute__(self, item):
        print(f"__getattribute__被调用了({item=})")
        return super(Student, self).__getattribute__(item)


s = Student('wgj', 89, 60)
print(s.__dict__)  # {'english': 60, 'name': 'wgj'} math已经被数据描述器实例math覆盖了
print(Student.__dict__)
print(s.name)

print(f"{s.math=}")

print(f"{s.english=}")

print(f"{s.fake_score=}", )

delattr(s, 'math')

输出结果如下:

console
数据描述器__set__被调用了(instance=<__main__.Student object at 0x000002577CF03860>, value=89)
__getattribute__被调用了(item='__dict__')
{'name': 'wgj', 'english': 60}
{'__module__': '__main__', 'math': <__main__.DataDesc object at 0x000002577CF03650>, 'english': <__main__.NoDataDesc object at 0x000002577CF03560>, 'fake_score': <__main__.NoDataDesc object at 0x000002577CF03830>, '__init__': <function Student.__init__ at 0x000002577CF240E0>, '__getattribute__': <function Student.__getattribute__ at 0x000002577CF24180>, '__dict__': <attribute '__dict__' of 'Student' objects>, '__weakref__': <attribute '__weakref__' of 'Student' objects>, '__doc__': None}
__getattribute__被调用了(item='name')
wgj

__getattribute__被调用了(item='math')
数据描述器__get__被调用了(instance=<__main__.Student object at 0x000002577CF03860>, owner=<class '__main__.Student'>)
s.math=89

__getattribute__被调用了(item='english')
s.english=60

__getattribute__被调用了(item='fake_score')
非数据描述器__get__被调用了(instance=<__main__.Student object at 0x000002577CF03860>, owner=<class '__main__.Student'>)
s.fake_score=0

__delete__被调用了(instance=<__main__.Student object at 0x000002577CF03860>)

属性 english 是非数据描述器,为何没调用 __get__ 呢?这就涉及到属性访问的查找顺序了:

  1. __getattribute__ 方法,无条件最先调用。
  2. 数据描述器的 __get__ 方法。
  3. 实例对象的属性字典 __dict__ ,需注意:与数据描述器的对象同名时不会存在于属性字典中。
  4. 类的属性字典。
  5. 非数据描述器的 __get__ 方法。
  6. 父类的属性字典。
  7. __getattr__ 方法。

因为实例对象的属性字典在非数据描述器前面,所以就不会调用 __get__ 方法了。

描述器把数据和使用它的类进行了分离,数据相关的操作在描述器类中,代码模块化更利于复用,提高可扩展性。描述器的应用场景有很多,比如数据验证、类型检查、计算缓存、动态计算等等。

python
import os

class DirectorySize:
   """非数据描述器"""

   def __get__(self, instance, owner=None):
      return len(os.listdir(instance.dirname))

class Directory:
   size = DirectorySize()

   def __init__(self, dirname):
      self.dirname = dirname
      # self.size = self.get_size()  # 传统方式的写法

   def get_size(self):
      return len(os.listdir(self.dirname))

if __name__ == '__main__':
   d1 = Directory('../')
   d2 = Directory('../../')
   print(d1.size, d2.size)
python
class Typed:
   def __init__(self, name, expected_type):
      self.name = name
      self.expected_type = expected_type

   def __get__(self, obj, obj_type=None):
      return obj.__dict__.get(self.name)

   def __set__(self, obj, value):
      if not isinstance(value, self.expected_type):
         raise TypeError(f"期望类型 {self.expected_type}")
      obj.__dict__[self.name] = value

class Person:
   name = Typed('name', str)
   age = Typed('age', int)

   def __init__(self, name, age):
      self.name = name
      self.age = age

if __name__ == '__main__':
   p = Person('Tom', "18")
python
# Python 内置库中已经实现了计算缓存的装饰器:@functools.lru_cache
class lazyproperty:
   def __init__(self, func):
      self.func = func

   def __get__(self, instance, cls):
      if instance is None:
         return self
      else:
         value = self.func(instance)
         setattr(instance, self.func.__name__, value)
         return value

class Circle:
   def __init__(self, radius):
      self.radius = radius

   @lazyproperty
   def area(self):
      print('Computing area')
      return 3.1415 * self.radius ** 2

c = Circle(5)
print(c.area)
# 需注意计算出来的值是可以被修改的,因为已经在实例的__dict__属性字典中了
print(c.__dict__)
print(c.area)  # 第二次之后的调用不会在打印 Computing area

迭代器、生成器、可迭代

  • 类中实现了 __iter__ 的是可迭代对象 Iterable,可迭代对象可以使用 for 循环遍历,可以使用 iter() 函数得到其对应的迭代器。
  • 类中实现了 __iter____next__ 是迭代器 Iterator,迭代器可以使用 next() 方法调用得到下一个值,如果没有值了,调用 next() 将引发 StopIteration 异常。工作中 next() 方法用的不多,因为还要处理异常,直接使用 for 循环遍历即可,但有些场景有妙用。
  • 在函数中使用 yield 关键字,函数将变成生成器,调用函数返回的是生成器对象;此外还有生成器表达式也是生成器。
  • 生成器和迭代器都是可迭代对象,所以都能用 for 循环直接遍历。

生成器是一种流式处理的思想,即把数据一条一条地流到下游处理,而不是先全部放到一个容器中,再将这个容器传到下游处理。如果数据量很大,那将占用大量内存,而流式处理就不会有这种问题。

python
def gen_data():
   for i in range(1000):
      if i > 10:
         return -1  # 在生成器中使用 return 可提前终止生成器
      yield i

print(gen_data())
for data in gen_data():
   print(data)

# 生成器表达式
gen_data2 = (i*10 for i in range(1000))
print(gen_data2)

注意两者区别:函数需要调用才是生成器对象,而生成器表达式生成的对象已经是生成器了,不需要再调用。