主题
Python元编程
主要是针对类的魔法方法,像 __lt__
这种类似的就不说了,和运算符重载一个道理(所以说编程语言的思想都是差不多的)。
__dict__
属性字典:一个用于存储对象的(可写)属性的字典或其他映射对象。
- 只有通过
__init__
初始化的属性才能存在于实例的__dict__
(也可能不会,见描述器),类的方法和属性不存在于实例的__dict__
中,而在类的__dict__
中。 - 实例添加成员属性或方法时,会写入到实例的
__dict__
中,类的__dict__
不会写入。 - 魔法方法不应该添加到实例对象中(隐式调用会报错),而应该始终添加在实例的类中,以便始终一致地由解释器发起调用。
- 父类和子类都有各自的
__dict__
,子类不会包含父类的__dict__
。 - 类属性为可变对象时,实例通过可变对象的方法可以直接修改这个类的此属性。
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__
:
- 当实例访问属性或方法时会最先调用此方法,通过类来访问类属性不会调用。
- 如果类还定义了
__getattr__
方法,则后者不会被调用,除非__getattribute__
显式地调用它或是引发了 AttributeError。 - 应当返回基类同名函数的返回值或断言 AttributeError 异常,否则可能会无限递归。
- 魔法方法的隐式调用会跳过此方法。
- 基类(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__
呢?这就涉及到属性访问的查找顺序了:
__getattribute__
方法,无条件最先调用。- 数据描述器的
__get__
方法。 - 实例对象的属性字典
__dict__
,需注意:与数据描述器的对象同名时不会存在于属性字典中。 - 类的属性字典。
- 非数据描述器的
__get__
方法。 - 父类的属性字典。
__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)
注意两者区别:函数需要调用才是生成器对象,而生成器表达式生成的对象已经是生成器了,不需要再调用。