《Effective Python》笔记 第五章-类与接口
阅读Effective Python(第二版)的一些笔记
第37条 用组合起来的类来实现多层结构,不要用嵌套的内置类型
- 使用dict来做关系型数据库的层层关联关系,这个是可以的实现,但是嵌套次数多了之后,代码就会比较难理解,毕竟dict里面的key是啥,没有明确的标识,所以在进行此类场景的关联时,不要在字典里嵌套字典、长元组,以及用其他内置类型构造的复杂结构,尽量梳理后将为实体创建对应类。
- namedtuple能够实现出轻量级的容器,以存放不可变的数据,而且将来可以灵活地转化成普通的类。如果发现用字典来维护内部状态的那些代码已经越写越复杂了,那么就应该考虑改用多个类来实现。
一些简单的场景,可以使用namedtuple来快速创建简单类,使用格式如下namedtuple(className, [property_1, property_2...])
#coding:utf-8
from collections import namedtuple
# 使用namedtuple创建简单的类
UserClass = namedtuple("User", ["name", "gender", "age"])
user = UserClass(name="小明", gender="男", age=99)
print(user) # User(name='小明', gender='男', age=99)
print(user.name) # 小明
第38条 让简单的接口接受函数,而不是类的实例
Python类中的__call__()
方法,重写了这个方法后,就可以像调用函数一样调用对象实例,例子如下:
# coding:utf-8
class MyCallableClass(object):
def __init__(self, desc):
self.desc = desc
# 实现了__call__方法后可以想调用函数一样调用对象
def __call__(self, *args, **kwargs):
print("__call__()方法被调用,desc:{}".format(self.desc))
print("*args:{}, **kwargs:{}".format(args, kwargs))
demo = MyCallableClass("hello")
# 然后像调用方法一样进行调用
demo(1, 2, 3, name="小明", age=99)
# __call__()方法被调用,desc:hello
# *args:(1, 2, 3), **kwargs:{'name': '小明', 'age': 99}
hook(钩子函数)
在Python中,函数和接口都是一等公民,也就是可以像变量一样进行赋值操作。
Python有许多内置的API,都允许我们传入某个函数来定制它的行为(当做变量传入)。这种函数可以叫作挂钩(hook),API在执行过程中,会回调(call back)这些挂钩函数。
下面是一个示例:
# coding:utf-8
# 获取logger, 并调用传入的before_hander和after_handler,这俩handler就是hook
def get_logger(before_handler, after_handler):
def _logger(msg):
before_handler(msg)
print(msg)
after_handler()
return _logger
def before_h(m):
print("这是before输出的内容,msg:{}".format(m))
def after_h():
print("这是after输出的内容")
log = get_logger(before_h, after_h)
log("hello world")
# 这是before输出的内容,msg:hello world
# hello world
# 这是after输出的内容
上面传的before_hander和after_handler,这俩handler就是hook,也就是可以被调用的函数。前面说了实现了__call__()
方法的类实例也是可以被调用的,所以可以将对象传入,也ok,示例如下:
class MyBeforeHandelr(object):
def __call__(self, *args, **kwargs):
print("这是MyBeforeHandelr.__call__()输出的内容,msg:{}".format(args))
class MyAfterHandler(object):
def __call__(self, *args, **kwargs):
print("这是MyAfterHandler.__call__()输出的内容")
my_before_handler = MyBeforeHandelr()
my_after_handler = MyAfterHandler()
log = get_logger(my_before_handler, my_after_handler)
log("hello world")
# 这是MyBeforeHandelr.__call__()输出的内容,msg:('hello world',)
# hello world
# 这是MyAfterHandler.__call__()输出的内容
传入一个函数 与 传入一个可以被call的对象,有啥区别吗?
其实没啥区别,主要就是单独传入一个函数的话,这个是无状态的,也就是说处理过程完全一样;但是传入可以调用的对象的话,hook就可以是有状态的了,因为对象里面是可以保存状态的。
第39条 通过@classmethod多态来构造同一体系中的各类对象
多态机制使同一体系中的多个类可以按照各自独有的方式来实现同一个方法,这意味着这些类都可以满足同一套接口,或者都可以当作某个抽象类来使用,同时,它们又能在这个前提下,实现各自的功能;
比如下面的Book类(接口定义),定义了info接口,以及两个子类EBook、PaperBook,子类对info方法进行各自的实现。
# coding:utf-8
# 父类
class Book(object):
# 由子类实现方法
def info(self):
raise NotImplementedError
# 电子书
class EBook(Book):
def __init__(self, brand, size, price):
super().__init__() # 调用父类初始化方法
self.brand = brand
self.size = size
self.price = price
def info(self):
print("brand:{}, size:{}, price:{}".format(self.brand, self.size, self.price))
# 纸书
class PaperBook(Book):
def __init__(self, pages, press, edition):
super().__init__()
self.pages = pages
self.press = press
self.edition = edition
def info(self):
print("pages:{}, press:{}, edition:{}".format(self.pages, self.press, self.edition))
测试代码
ebook = EBook("xiaomi", 10.2, 1999)
ebook.info()
# brand:xiaomi, size:10.2, price:1999
paper_book = PaperBook(600, "人民邮电出版社", "the second")
paper_book.info()
# pages:600, press:人民邮电出版社, edition:the second
book_list = [ebook, paper_book]
for book in book_list:
# 直接调用方法,不需要关心是哪个子类,因为他们都实现了info方法
book.info()
# brand:xiaomi, size:10.2, price:1999
# pages:600, press:人民邮电出版社, edition:the second
上面代码就简单实现了多态;
现在需要有一个通用的功能,根据传入类的类型和参数,实现该类的实例化,传入的类可能是Ebook,也可能是PaperBook,此时可以采用类似工厂模式那种,如下:
# 仿照工厂模式
def build_book(clazz, param):
if clazz == EBook:
return EBook(param['brand'], param['size'], param['price'])
elif clazz == PaperBook:
return EBook(param['pages'], param['press'], param['edition'])
else:
raise TypeError
book1 = build_book(EBook, {"brand": "xiaomi", "size": 10.2, "price": 1000})
book1.info()
# brand:xiaomi, size:10.2, price:1000
book2 = build_book(PaperBook, {"pages": "400", "press": "asdf", "edition": "2ed"})
book2.info()
# brand:400, size:asdf, price:2ed
上面的代码是能实现需求的,但是一旦Book类又多了一个子类,那么就得修改build_book方法,否则创建新的子类对象,就会报TypeError;此时可以采用下面的方式:
- Book定义一个create_book_instance方法,方法接受一个dict字典,该字典保存了实例化子类的初始化数据;
- Book所有的子类实现该create_book_instance方法,从dict中获取参数进行实例的初始化;
- 修改前面build_book的逻辑:
# 父类
class BookV2(object):
# 由子类实现方法
def info(self):
raise NotImplementedError
# 留给子类实现,从config_data中获取数据来实例化子类对象
@classmethod
def create_book_instance(cls, config_data):
raise NotImplementedError
# 电子书
class EBookV2(BookV2):
def __init__(self, brand, size, price):
super().__init__() # 调用父类初始化方法
self.brand = brand
self.size = size
self.price = price
@classmethod
def create_book_instance(cls, config_data):
return EBookV2(config_data["brand"], config_data["size"], config_data["price"])
def info(self):
print("brand:{}, size:{}, price:{}".format(self.brand, self.size, self.price))
# 纸书
class PaperBookV2(BookV2):
def __init__(self, pages, press, edition):
super().__init__()
self.pages = pages
self.press = press
self.edition = edition
@classmethod
def create_book_instance(cls, config_data):
return PaperBookV2(config_data["pages"], config_data["press"], config_data["edition"])
def info(self):
print("pages:{}, press:{}, edition:{}".format(self.pages, self.press, self.edition))
# 仿照工厂模式
def build_book_v2(clazz, param):
# 直接返回
return clazz.create_book_instance(param)
# 测试
book3 = build_book_v2(EBookV2, {"brand": "xiaomi", "size": 10.2, "price": 999})
book3.info()
# brand:xiaomi, size:10.2, price:999
book4 = build_book_v2(PaperBookV2, {"pages": "500", "press": "gfd", "edition": "2ed"})
book4.info()
# pages:500, press:gfd, edition:2ed
第40条 通过super初始化超类
在Python中,如果类是支持多继承的,也就是说一个类可以继承多个类;
当在子类中要初始化父类时,目前有两种方式,这两种方式的区别比较大:
直接调用父类的``init`
这种方式比较简单粗暴
- 当子类继承了父类后,必须在需要在子类的初始化方法中手动调用某个父类初始化方法,父类初始化方法才会执行,否则不会自动执行;
- 当子类继承多个父类时,在
class subClass(parentClass1,parentClass2)
中,parentClass1,parentClass2
的顺序不重要,这个顺序不会影响父类的初始化,父类的初始化顺序取决于子类手动调用父类初始化方法的顺序,所以使用这种方式的话,不要单纯的看class的继承循序就以为是初始化数据。
这种方式
# coding:utf-8
class OneClass(object):
def __init__(self):
print("OneClass __init__()")
def say_one(self):
print("OneClass say one")
class TwoClass(object):
def __init__(self):
print("TwoClass __init__()")
def say_two(self):
print("TwoClass say two")
# class语句,OneClass和TwoClass的顺序不同
# class ThreeClass(OneClass, TwoClass):
class ThreeClass(TwoClass, OneClass):
def __init__(self):
# 如果不手动调用父类的初始化方法,那么父类的__init__不会自动调用
OneClass.__init__(self)
TwoClass.__init__(self)
print("ThreeClass __init__()")
def say_three(self):
super(ThreeClass, self).say_one()
super(ThreeClass, self).say_two()
print("ThreeClass say three")
three = ThreeClass()
three.say_three()
# OneClass __init__()
# TwoClass __init__()
# ThreeClass __init__()
# OneClass say one
# TwoClass say two
# ThreeClass say three
使用super方式类调用
- 一般来说,这种方式会按照
class subClass(parentClass1,parentClass2)
中,顺序初始化parentClass1,parentClass2
,而不是先初始化parentClass2,parentClass1
# 使用super().__init__()时,父类的初始化顺序就是class的参数顺序
class FourClass(TwoClass, OneClass):
def __init__(self):
# 如果不手动调用父类的初始化方法,那么父类的__init__不会自动调用
super().__init__()
print("FourClass __init__()")
def say_four(self):
super().say_one()
super().say_two()
print("FourClass say four")
four = FourClass()
four.say_four()
执行结果:
TwoClass __init__()
FourClass __init__()
OneClass say one
TwoClass say two
FourClass say four
关于使用哪种方式,可以根据自己的喜好觉得。
第41条 考虑用mix-in类来表示可组合的功能
关于python的mix-in,可以参考https://wiki.woodpecker.org.cn/moin/IntroMixin
假设A类想要使用B类的方法,那么A类可以直接继承B类,这样自然而然的拥有了B类的方法;这个时候使用继承,更多的考虑扩充自身功能;
比如ToJson类是一个通用的工具了,可以将对象的属性序列化为json,那么其他类有需要的话都可以继承这个类;继承该类后,如果有特殊的逻辑,那么可以重写响应的方法。
第42条 优先考虑用public属性表示应受保护的数据,不要用private属性表示
Python的属性和方法只有两种访问级别,public和private:
- 以两个下划线开头的属性和方法,都是private的,不能直接通过对象加属性名或者方法名调用;
- Python的private,只是将属性名和方法名做了替换:
_类名_属性名
或者_类名_方法名
- Python的private,只是将属性名和方法名做了替换:
- 以两个下划线开头的属性和方法,都是public的,可以直接通过对象加属性名或者方法名调用。
# coding:utf-8
class Person(object):
def __init__(self, name, addr, age):
self.name = name
self._addr = addr
self.__age = age
def get_age(self):
return self.__age
def _get_addr(self):
return self._addr
def __get_name(self):
return self.name
p = Person("abc", "beijing", 10)
print(p.name) # abc
print(p._addr) # beijing
# print(p.__age) # AttributeError: 'Person' object has no attribute '__age'
print(p._Person__age) # 10
print(p.get_age()) # 10
print(p._get_addr()) # beijing
# print(p.__get_name()) # AttributeError: 'Person' object has no attribute '__get_name'
print(p._Person__get_name()) # abc
print(dir(p))
# ['_Person__age', '_Person__get_name', '__class__', '__delattr__', '__dict__',
# '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',
# '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__',
# '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
# '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__',
# '_addr', '_get_addr', 'get_age', 'name']
第43条 自定义的容器类型应该从collections.abc继承
在定义类的时候,如果想使用某个类的功能,那么可以继承这个类;比如定义一个类,支持像list一样进行len、count、index,那么就可以继承list即可。
但是不使用继承,还想使用len()
,那么就需要重写类的__len__
方法;想使用下标形式[0]
形式访问指定位置的元素,就得重写__getitem__
方法,这个需要查资料或者记住;
有时候一个普通的操作可能会涉及到多个方法的重写,少重写一个方法就会报错,而且只有当运行的时候才会知道还需要重写哪个方法,一个一个解决;为了避免这个问题,collections.abc
模块,提供了很多接口,可以直接继承这些接口,就能知道哪些方法是必须要实现的了。
#coding:utf-8
from collections.abc import Set
# 继承collections.abc.Set, 自动带出哪些接口需要实现.
class MySet(Set):
def __iter__(self):
pass
def __contains__(self, x: object) -> bool:
pass
def __len__(self) -> int:
pass