之前的文章提到,使用type关键字可以用来动态地生成一个类,但是这样写实在是太麻烦了,需要创建一大堆的参数。大部分的动态创建,都可以简单地使用以下方法来实现。
# Python 3
def gen_class(type_):
if type_ == 'Person':
class Foo():
def __init__(self):
print('I am a Person object')
else:
class Foo():
def __init__(self):
print('I am not a Person object')
return Foo
Person = gen_class('person')
p = Person()
但在一般的实践场景下,上面的代码都应该被尽量的避免,因为就单单地创建对象而言,不如直接定义两个类方便。(但也有意外,例如上层程序希望传递不同的值而获得相同类但不同的属性时)
更多的情况下,我们希望类实例创建时准守某个准则,比如对类的属性做检查,并自动将驼峰、大写命名的属性全部转换为小写。先来看看一般地做法是怎样的吧:
# Python 3
import re
class Foo():
def __init__(self):
self.__get_standard_attr()
def __get_standard_attr(self):
attrs:list = dir(self)
for attr in attrs:
if hasattr(self, attr):
# 大写属性转小写
if attr.startswith('_'):
continue
attr_char_list = re.split(r'([A-Z])', attr)
setattr(self, self.__camel_to_underline(attr_char_list), getattr(self, attr))
def __camel_to_underline(self, char_list = [], str_=''):
if not char_list:
return str_
if not char_list[0]:
char_list.pop(0)
return self.__camel_to_underline(char_list, str_)
str_ += char_list[0].lower()
if len(char_list) > 1 and not char_list[1]:
str_ += '_'
elif len(char_list) > 1 and char_list[1].isupper():
str_ += '_'
else:
pass
char_list.pop(0)
return self.__camel_to_underline(char_list, str_)
class Test(Foo):
camelAttr = 1
t = Test()
print(t.__dict__)
通过上述方法,我们悄悄地将类变量<e>camelAttr</e>悄悄地变成了camel_attr.(实际上是更改了对象<e>t</e>的成员变量名)。但是上述的方法有个显而易见的弊端,一旦我们给子类(<e>Test</e>)定义了初始函数(<e>__ init __()</e>), 其父类的属性转换就不会对子类起作用。(没错,Python的子类的构造方法默认不会执行父类的构造方法。子类的构造相当于重写父类的构造。)除非你每次都手动将父类的构造执行一次:
class Test(Foo):
def __init__(self):
...
Foo.__init__(self)
但是,有没有更加简单的方案呢?
Python世界里的ORM映射
如果你使用Python做过Web应用开发,你应该对ORM比较熟悉,它将一个Model映射到数据库中的表中,并且封装了大量的SQL语句。有经验的你一定发现了,在一个Model中定义的属性,居然和最后取出来的不一样。例如:
# Python3 flask-sqlalchemy
db = SQLAlchemy(app)
class Person(db.Model):
name = db.Column(db.String(32))
gender = db.Column(db.Enum(
'male',
'female'
))
tel = db.Column(db.String(16)
evink = Person(
name='EvinK',
gender='male',
tel='131****2503'
)
...
# db.Column指向到这里
class Column(SchemaItem, ColumnClause):
"""Represents a column in a database table."""
pass
evink.name # 这里居然是一个str,而不是一个db.Colum对象
SQLAlchemy中的这种实现,就得益于<e>metaclass</e>这个东西。在我的理解里,它可以被叫做类的模板,让实例的对象用程序员的构造模式被生成出来。一般在我们的实践中,很少会使用到这个东西,但是又由于它强大的特性,所以能帮助我们创造出很强大的框架类工具。又或者说,这个东西,是被程序员来使用并创造出可以让程序员来提高效率的东西,这样介绍,似乎真的有了'元'的味道。
再回到我们之前的问题,利用<e>metaclass</e>,似乎这道题有了一个完美的解决方案。
# Python3
def camel_to_underline(char_list = [], str_=''):
if not char_list:
return str_
if not char_list[0]:
char_list.pop(0)
return camel_to_underline(char_list, str_)
str_ += char_list[0].lower()
if len(char_list) > 1 and not char_list[1]:
str_ += '_'
elif len(char_list) > 1 and char_list[1].isupper():
str_ += '_'
else:
pass
char_list.pop(0)
return camel_to_underline(char_list, str_)
def get_standard_attr(class_name, class_parent, class_attrs):
new_attrs = {}
for name,value in class_attrs.items():
if not name.startswith('__'):
attr_char_list = re.split(r'([A-Z])', name)
name = camel_to_underline(attr_char_list)
new_attrs[name] = value
# 使用type创建一个新的对象
return type(class_name, class_parent, new_attrs)
class Foo(metaclass=get_standard_attr):
camelAttr = []
f = Foo()
f.camelAttr # 'Foo' object has no attribute 'camelAttr'
f.camel_attr # []
值得注意的是,这里继承的metaclass应该真的是一个类,而这里使用了一个type来模拟它。(可以看出Python中对象之间的转换是多么灵活……)
# Python3
# 一个真正的元类
class StandardAttr(type):
# 这里我们希望返回创建的对象,也就是类的模板
# 所以使用了__new__方法
def __new__(cls, class_name, class_parent, class_attrs):
...
return cls
未完待续……
原文地址:>> https://code.evink.me/2018/11/post/Meta-Programming-Part-2/
网友评论