Java中有一类对象,非常轻量,主要用来归集一些属性方便访问,被称为POJO(plain old java object)。比如,你可以有一个Point类,然后通过Point.x, Point.y来访问其属性。在javascript当中,几乎每一个字典对象都可以看成POJO,因为你除了可以用Point["x"]来访问外,也可以直接用Point.x来访问。
在Python当中,则不存在这样的对象。本文讨论的就是如何在Python中实现这种对象。
1. 为什么我们需要Pojo?
在数据库访问中, 我们常常需要方便地表示一行数据,比起使用row['col1']这样的语法,我更喜欢row.col1,因为后者键入更方便(当然如果还有一种方法进行代码自动完成则更好)。另外,yaml的配置文件读入到程序中后,一般是以字典类来存放,所以要使用一个配置,我们可能会写下:
cfg["service"]["database"]["dsn"]显然可读性不如cfg.service.database.dns
2. namedtuple、munch和自定义类的实现
namedtuple
首先我们想到的实现方法很可能就是namedtuple。你可以定义一个新的简装类:
from collections import namedtuple
Point = namedtuple('Point', 'x, y')
point = Point(3, 5)使用namedtuple的好处是各种IDE都支持这种自定义类的intellisense!但除此之外,我并没有找到它的有效用途,而它的缺点则十分明显:
1. 经由namedtuple生成的对象,其属性是不可更改的。也就是一旦经构造函数生成,再也无法修改(即赋值)
2. 它只支持一级的字典键值到类属性值的转换:
Foo = namedtuple('Foo', 'a,b,c')
foo = Foo(a = 1, b = {'key': 'value'), c = 3)
foo.b.key #出错,属性不存在这样namedtuple就不适用于数据库访问的场合。因为我们从数据库读取一行数据,转换成为namedtuple之后,想要做一些计算(可能会改变这个namedtuple对象的值),再存入数据库则是不可能的;在配置文件表示方面,象yaml这样支持多级嵌套,甚至是支持数组的场合也是不堪使用了。
一个自定义类的实现
有时候我们不妨自己进行一些简单、粗糙的实现,只要我们明确知道这些代码会不会被超范围使用就好。比如在数据库访问频繁的场合,如果我们的应用有100多张表,要定义100多个pojo类,是一件很烦琐的事:
class User:
def __init__(self, name, age, gendre, password):
self.name = name
self.age = age
self.gendre = gendre
self.password = password这里的代码太多了!其实我们可以这样写:
class TableClass:
fields = ''
def __init__(self, **kwargs):
for _field in self.fields.split(','):
field = _field.strip()
setattr(self, field, kwargs.get(field, None))
def keys(self):
return self.__dict__.keys()
def values(self):
return self.__dict__.values()现在要定义100个pojo类就容易很多了,我们只需要指定每个类有哪些属性即可:
class User:
fields = 'name, age, gendre, password'
user = User(name='jack', age=13)
# 在构造时可以省略赋值,留待后面持续更新上面的方法的优点是与namedtuple相比,代码量并未增加,也同样受到IDE的属性自动提示协助。也不能支持多级嵌套的情况。但是,这样生成的类的属性是可以赋值的,你也不需要在构造时就传入全部了属性值。
有一个不那么明显的缺陷(对自定义类和namedtuple都是),每一个类也都是一个对象,都要占用内存空间。这是一个容易被python程序员忽略的事实,因为多数人的python只是一个简单的脚本工具,你通常不会去考虑它的内存特性。但如果你打算用python来写大型应用程序(这并不是不可能的),则我们要考虑的东西会多很多。
这里我们简单地提一下munch这个库。你可以认为它解决了上面两种方法所有的缺陷:太多类对象定义,不支持多层嵌套,或者属性无法赋值。
# pip install munch
from munch import Munch
cfg = {
"docker": {
"volume": "/home/docker"
}
}
config = Munch.fromDict(cfg)
print(config.docker.volume)
# /home/docker
config.docker.volume = '/tmp'
print(config.docker.volume)
#/tmp那么Munch是完美的解决方案吗?是的,除了性能问题。也就是在配置管理这样的场合,你只要使用Munch就可以了。但是如果在数据库访问,网络字节与对象的转换中,Munch则是一个不太优秀的选择,因为这种情况下,Munch被大量、频繁地使用,因此我们需要认真考察它CPU和内存使用情况。
又一个自定义实现
python的魔法往往都隐藏在它的那些内置方法中。这里getattr和getitem将派上用场。
# https://stackoverflow.com/a/31569634/1210352
class DictProxy(object):
def __init__(self, obj):
self.obj = obj
def __getitem__(self, key):
return wrap(self.obj[key])
def __getattr__(self, key):
try:
return wrap(getattr(self.obj, key))
except AttributeError:
try:
return self[key]
except KeyError:
raise AttributeError(key)
# you probably also want to proxy important list properties along like
# items(), iteritems() and __len__
class ListProxy(object):
def __init__(self, obj):
self.obj = obj
def __getitem__(self, key):
return wrap(self.obj[key])
# you probably also want to proxy important list properties along like
# __iter__ and __len__
def wrap(value):
if isinstance(value, dict):
return DictProxy(value)
if isinstance(value, (tuple, list)):
return ListProxy(value)
return valuegetitem用来提供下标访问,比如myarray[1],实际上被转换成了getitem(myarray, 1)。而getattr则提供对属性的访问。当我们使用python的内置函数getattr(obj, attr)时,实际上是在调用obj自己的getattr。这里要注意的是,当我们访问foo.x时,如果foo中有x这个属性,那么getattr是不会被调用的,而是直接返回foo.x的值。
class User:
def __init__(self, x, y):
self.x = x
self.y = y
def __getattr__(self, attr):
print("called with", attr)
user = User(1,2)
print(user.x)
print(user.z) # triggers __getattr__, will print "called with z"这种proxy的另一个使用的场合,可以见廖雪峰的文章定制类。
现在我们来测试一下上面的类能否支持多层嵌套:
def build_obj(cfg):
foo = DictProxy(cfg)
foo.docker.volume = ''
a = foo.docker.volume
foo = build_obj(cfg)与Munch相比,它并不真正地构建新的对象,而只是在对属性进行访问时,拦截这种请求并检索出正确的值,而后面的时间是无法省略的(即使是对象构建完成之后,访问其属性仍然需要花费几乎相同的时间)。让我们来跑一个对比测试:
我们在测试中只使用了结构相对简单的对象。如果对象结构复杂,两者之间的差异会更大。
结论:
在对性能有要求,对功能要求则相对简单的场合,应该使用这里的DictProxy,而不是Munch。而Munch则提供了十分丰富的功能。另外,DictProxy并不构建新的对象,因此在大量使用的情况下,它的内存开销也会远低于Munch。
即使你并不会遇到这里提到的使用场景,掌握getattr和getitem仍然是非常有用的。要写出高效、简洁的pythonic代码,就必须了解python的一些高级特性。
另外,本文提到了类对象也会占用内存空间这一事实,这可能是为多数python程序员所忽略的。