《Effective Python》笔记 第六章-元类与属性
阅读Effective Python(第二版)的一些笔记
第44条 用纯属性与修饰器取代旧式的setter与getter方法
有一些编程语法,会为每个属性定义对应的getter和setter,用来获取属性和设置属性值,但是在Python中,不用这么复杂,直接使用对象.属性名
来访问即可;
如果在访问属性时需要做特殊的处理,那么可以使用@property
实现,并且在使用@property
时,不要引发奇怪的副作用,比如进行io操作或者其他耗时的操作。
# coding:utf-8
class Person(object):
def __init__(self):
self.name = None
self._age = None
@property
def age(self):
print("执行@property装饰的age()")
return self._age
@age.setter
def age(self, new_age):
print("执行@age.setter装饰的age(), new_age:%s" % new_age)
if new_age < 0:
raise Exception("age必须大于等0")
self._age = new_age
p = Person()
p.name = "abc"
p.age = 10 # 执行@age.setter装饰的age(), new_age:10
print(p.name) # abc
print(p.age)
# 执行@property装饰的age()
# 10
第45条 考虑用@property实现新的属性访问逻辑,不要急着重构原有的代码
如果一个类里面属性访问和设置的逻辑比较复杂,现在需要修改这些逻辑,建议不要重构已有的代码,而是使用@property
来改已有的实例属性增加功能(将已有的逻辑迁移到@property
中);
另外如果类中的@property
比较多了,这时候应该考虑这种重构这个类了,因为每个属性的访问和设置时,都有逻辑,那么这个类就特别复杂了。
第46条 用描述符来改写需要复用的@property方法
@property
是很方便,但是还是有一些无法处理的问题,我们需要明确的为某个属性定义加上@property
和@property.setter``,如果我们给A属性加了,但是没有给B属性加,那么B就不能像A那样使用`@property的便捷。
那么如何做,才能实现B不加@property
也能使用@property
的便捷呢?可以使用描述符;
什么是描述符,可以先看下面这个例子:
class Person(object):
def __init__(self):
self.name = None
person = Person()
当为person的name属性复制时,person.name = "abc"
,翻译为Person.__dict__['name'].__set__(person, "abc")
;
当获取person的name属性值时,print(person.name)
,翻译为print(Person.__dict__['name'].__get__(person, "abc"))
;
也就是说,当访问对象的属性时,Python会转为在类的层面查找,查询Person类里面有没有这样一个属性。如果有,而且还是个实现了__get__
与__set__
方法的对象,那么系统就认定你想通过描述符协议定义这个属性的访问行为。
将上面的翻译拆分一下Person.__dict__['name'].__set__(person, "abc")
,首先是Person.__dict__['name']
,这个结果肯定是个对象,然后调用这个对象的__set__(person, "abc")
和__get__(person, "abc"))
;前面我们提到类中的属性不加@property
就不能使用@property
的便捷,那么这里看来,只要Person.__dict__['xxx']
都支持__set__
和__get__
即可。
第47条 针对惰性属性使用__getattr__、getattribute__及__setattr
- 如果访问一个类中未定义的属性(注意不是实例属性),就会调用该类的
__getattr__
方法 - 当要给对象的属性设置值时候,就会调用
__setattr__
方法。 - 访问一下类中的属性,不论该属性是否存在,都会调用
__getattr__
方法
一般情况下,对象可能会包含很多数据,这些数据可以在类加载的时候被初始化,这种情况可能不是最优的,也许加载的数据很久用不到,甚至运行过程不会用到,此时就可以使用懒加载,实现的时候,可以自己定义懒加载方法,另外一种方式是使用__getattr__
,这种方式动态的加载数据并动态为类增加一个属性。
第48条 用__init_subclass__验证子类写得是否正确
当子类继承了父类后,父类的一些接口如果要子类保证数据的某些规则,这个时候父类怎么对子类进行限制呢?
一种方式就是父类中定义check方法,子类继承后,由子类主动调用check方法来校验参数是否符合条件,如下所示
#coding:utf-8
class BaseClass(object):
DATA = None
@classmethod
def check_data(cls):
if cls.DATA is None:
raise RuntimeError
elif not isinstance(cls.DATA, dict):
raise TypeError
class ChildClass(BaseClass):
# 设置属性值
DATA = dict()
child = ChildClass()
# 手动调用check
child.check_data()
还有一种方式,就是使用__init_subclass__
,简单的示例如下
#coding:utf-8
class SuperClass(object):
def __init_subclass__(cls, **kwargs):
print("run SuperClass.__init_subclass__")
print("cls:%s" % cls)
print("**kwargs:%s" % kwargs)
# 类属性demo_value设置为abc-xyz
cls.demo_value = "abc-xyz"
# 定义子类
class SubClass(SuperClass):
# 子类也定义了demo_value
demo_value = "qaq"
sub = SubClass()
print(sub.demo_value)
# run SuperClass.__init_subclass__
# cls:<class '__main__.SubClass'>
# **kwargs:{}
# abc-xyz
# 定义另外一个子类
class SecondClass(SubClass):
demo_value = "second"
second = SecondClass()
print(second.demo_value)
# run SuperClass.__init_subclass__
# cls:<class '__main__.SecondClass'>
# **kwargs:{}
# abc-xyz
看了上面__init_subclass__
的例子,就可以看出,父类是可以修改子类的数据;
现在需要的是父类中增加子类参数的校验,肯定是办不到的,因为父类始终覆盖了子类的数据,其实在继承的时候,还可以指定参数,传给__init_subclass_
的kwargs。示例如下:
class SuperClass(object):
def __init_subclass__(cls, **kwargs):
print("run SuperClass.__init_subclass__")
print("cls:%s" % cls)
print("**kwargs:%s" % kwargs)
# 进行校验
if "demo_value" not in kwargs:
raise RuntimeError
if len(kwargs['demo_value']) < 0:
raise RuntimeError
cls.demo_value = kwargs['demo_value']
# 在继承的时候,还可以指定参数,传给`__init_subclass_`的kwargs
class ThirdClass(SuperClass, demo_value="abc", other_value="dfadfasf"):
pass
third = ThirdClass()
print(third.demo_value)
# run SuperClass.__init_subclass__
# cls:<class '__main__.ThirdClass'>
# **kwargs:{'demo_value': 'abc', 'other_value': 'dfadfasf'}
# abc
于是,这样就在父类中定义了子类数据的check。
第49条 用__init_subclass__记录现有的子类
如果定义了一个类后,怎么知道这个类有哪些子类呢?比较简单的大概有以下几种思路,首先肯定会有一个几个来保存有哪些子类,每当有新的子类时,就加入到该集合:
方式1:子类初始化方法中,手动将自己加入到集合中
#coding:utf-8
class SuperClass(object):
# 保存继承该类的子类,key为名称,value为type
SUB_CLASS_DICT = {}
class OneClass(SuperClass):
def __init__(self):
SuperClass.SUB_CLASS_DICT.setdefault(self.__class__.__name__, self.__class__)
class TwoClass(SuperClass):
def __init__(self):
SuperClass.SUB_CLASS_DICT.setdefault(self.__class__.__name__, self.__class__)
one = OneClass()
two = TwoClass()
print(SuperClass.SUB_CLASS_DICT)
# {'OneClass': <class '__main__.OneClass'>, 'TwoClass': <class '__main__.TwoClass'>}
方式2:父类扩展__new__
方法,那么子类就不需要手动将自己加入到集合中;
class SuperClassV2(object):
# key为名称,value为type
SUB_CLASS_DICT = {}
def __new__(cls, *args, **kwargs):
cls.SUB_CLASS_DICT.setdefault(cls.__name__, cls)
class OneClassV2(SuperClassV2):
pass
class TwoClassV2(SuperClassV2):
pass
one = OneClassV2()
two = TwoClassV2()
print(SuperClassV2.SUB_CLASS_DICT)
# {'OneClassV2': <class '__main__.OneClassV2'>, 'TwoClassV2': <class '__main__.TwoClassV2'>}
方式3:使用__init_subclass__
来实现,功能就是创建对象后会回调该方法,该方法定义在父类中,这样的话,子类也不需要手动将自己加入到集合中;
class SuperClassV3(object):
# key为名称,value为type
SUB_CLASS_DICT = {}
def __init_subclass__(cls, **kwargs):
cls.SUB_CLASS_DICT.setdefault(cls.__name__, cls)
class OneClassV3(SuperClassV3):
pass
class TwoClassV3(SuperClassV3):
pass
one = OneClassV3()
two = TwoClassV3()
print(SuperClassV3.SUB_CLASS_DICT)
# {'OneClassV3': <class '__main__.OneClassV3'>, 'TwoClassV3': <class '__main__.TwoClassV3'>}
第50条 用__set_name__给类属性加注解
todo
第51条 优先考虑通过类修饰器来提供可组合的扩充功能,不要使用元类
假设有一个装饰器,可以记录方法执行的入参和返回值,比如下面这样:
# coding:utf-8
import time
def log(func):
def wraper(*args, **kwargs):
print("func_name:%s, args:%s, kwargs:%s" % (func.__name__, args, kwargs))
result = func(*args, **kwargs)
print("func_name:%s, result:%s" % (func.__name__, result))
return result
return wraper
@log
def do_repeat(msg, repeat_times):
time.sleep(1)
return msg * repeat_times
do_repeat("abc", 10)
# func_name:do_repeat, args:('abc', 10), kwargs:{}
# func_name:do_repeat, result:abcabcabcabcabcabcabcabcabcabc
如果一个类中有10个方法,每个方法都需要有这个功能,怎么做呢?只需要给每个方法都加上@log
装饰器即,如果有20个呢?30个呢?
这个时候如果还手动添加,就不太好了,怎么做可以只加一次呢?
方式1:可以使用元类,在__new__
中统一为所有方法添加该装饰器(相当于装饰后覆盖)
# 类中需要装饰的类型
handle_types = (
types.MethodType,
types.FunctionType,
types.BuiltinMethodType,
types.BuiltinFunctionType,
types.MethodDescriptorType,
types.ClassMethodDescriptorType
)
class SuperClass(object):
def __new__(cls, *args, **kwargs):
clazz = super().__new__(cls, *args, **kwargs)
# 遍历类的方法,
for key in dir(clazz):
value = getattr(clazz, key)
# 如果是function上面的哪几种类型,就装饰后进行覆盖
if isinstance(value, handle_types):
wraped_func = log(value)
setattr(clazz, key, wraped_func)
# 返回方法被装饰后的类
return clazz
class SubClass(SuperClass):
def say(self, msg):
return "say msg----------" + msg
def show(self, msg):
return "show msg________" + msg
sub = SubClass()
sub.say("hello")
# func_name:say, args:('hello',), kwargs:{}
# func_name:say, result:say msg----------hello
sub.show("yes")
# func_name:show, args:('yes',), kwargs:{}
# func_name:show, result:show msg________yes
方式2:使用类装饰器,前面都是介绍的函数装饰器,其实也有类装饰器,是对类进行增强,返回一个新的类,这样的话,就可以将上面__new__
中的逻辑提到类装饰器中,示例如下:
def log_class(clazz):
# 遍历类的方法
for key in dir(clazz):
value = getattr(clazz, key)
# 如果是function上面的哪几种类型,就装饰后进行覆盖
if isinstance(value, handle_types):
wraped_func = log(value)
setattr(clazz, key, wraped_func)
return clazz
@log_class
class TwoClass(object):
def say(self, msg):
return "say msg----------" + msg
def show(self, msg):
return "show msg________" + msg
two = TwoClass()
# func_name:__new__, args:(<class '__main__.TwoClass'>,), kwargs:{}
# func_name:__new__, result:<__main__.TwoClass object at 0x108962a00>
two.say("dadfd")
# func_name:say, args:(<__main__.TwoClass object at 0x108962a00>, 'dadfd'), kwargs:{}
# func_name:say, result:say msg----------dadfd
two.show("erwer")
# func_name:show, args:(<__main__.TwoClass object at 0x108962a00>, 'erwer'), kwargs:{}
# func_name:show, result:show msg________erwer
推荐使用类装饰器,这样并不会修改需要使用该装饰器的代码;因为前两种方式,要么需要修改类中的每个方法,也不需要修改类的继承,对类的侵入性比较小。