类工厂
类在Python中为一等公民的事实使得采用其他强大的设计模式成为可能。类工厂就是这类模式中的一种。本质上,类工厂就是一个在运行时创建类的函数。该概念允许创建类时根据情况决定其属性,不如说,根据用户输入创建属性。
一、类型回顾
正如Python中的其他对象一样,类也是由一个类实例化。例如,假如创建一个名为Animal类,如下所示:
class Animal(object):
'''A class representing an arbitrary animal.'''
def __init__(self, name):
self.name = name
def eat(self):
pass
def go_to_vet(self):
pass
Animal类在其构造函数被调用时负责创建Animal对象。与Animal类创建对象的方式相同的是,Animal本身也是一个对象,它的类是type-----Python中一个用于创建其他类的内置类。
type是基本元类,自定义元类继承type类。
也可以直接调用type类代替class关键字创建一个类。type接收3个位置参数:name、bases和attrs,分别对应于类名称、类的基类(以元组的形式)以及类的属性(字典类型)。
二、理解类工厂函数
类工厂函数是一个用于创建并返回类的函数。
考虑之前的Animal示例,可以使用type而不是class关键字创建一个与该类等价的类,如下所示:
def init(self, name):
self.name = name
def eat(self):
pass
def go_to_vet(self):
pass
Animal = type('Animal', (object,) {
'__doc__': 'A class representing an arbitrary animal.',
'__init__': init,
'eat': eat,
'go_to_vet': go_to_vet,
})
这种方式并不理想。其中一个原因是这种写法会将函数置于和Animal同一层命名空间下。通常,不使用class关键字而直接使用type并不是理想的办法,除非真需要这么做。在这类情况下,可以通过将代码放入一个函数中从而减少混乱,并将函数分发使用。如下所示:
def create_animal_class():
'''Return an Animal class,built by invoking the type conctruct.'''
def init(self, name):
self.name = name
def eat(self):
pass
def go_to_vet(self):
pass
return type('Animal', (object,) ,{
'__doc__': 'A class representing an arbitrary animal.',
'__init__': init,
'eat': eat,
'go_to_vet': go_to_vet,
})
现在,就可以通过调用上述函数获得一个自定义创建的Animal类,如下所示:
Animal = create_animal_class()
这里需要注意的重点是,如果多次调用create_animal_class()函数会返回不同的类。也就是说,尽管所返回的类有相同的名称与属性,其实它们并不是同一个类。这些类之间的相似性是基于每次运行函数时都会赋值相同的字典键与相似的函数。
换句话说,所返回的类之间的相似性并不确定。函数不能接受一个或多个参数并返回不同类并没有具体的原因。实际上,这也是类工厂函数的整个目的。
考虑下面多次调用类create_animal_class()返回的不同类:
>>> Animal1 = create_animal_class()
>>> Animal2 = create_animal_class()
>>> Animal1
<class '__main__.Animal'>
>>> Animal2
<class '__main__.Animal'>
>>> Animal1 == Animal2
False
类似的,考虑下面的实例:
>>> animal1 = Animal1('louisoix')
>>> animal2 = Animal2('louisoix')
>>> isinstance(animal1,Animal1)
True
>>> isinstance(animal1,Animal2)
False
尽管这两个类内部都是调用Animal,但并不是同一个类。它们是函数两次执行所返回的不同结果。
本例通过调用type创建Animal类,但这并不是必须的。使用class关键字创建类更加简单。即使在函数中使用class关键字并在函数结尾部分返回类也同样生效:
def create_animal_class():
'''Return an Animal class,built using the class keyword and return afterwords.'''
class Animal(object):
'''A class representing an arbitrary animal.'''
def __init__(self, name):
self.name = name
def eat(self):
pass
def go_to_vet(self):
pass
return Animal
大多数情况下,使用class关键字而不是直接调用type 创建类更加可行。然而,并非在所有情况下都可以这么做。
三、决定何时应该编写类工厂
编写类工厂的主要原因是在需要基于运行时的信息(如用户输入)创建类时。而class关键字假定你已经在编码时知道需要赋值给类的属性(虽然这并不是必要的)。
如果在编码时并不知道需要赋值给类的属性,类工厂函数将会是一个方便的替代办法。
(1)运行时属性
考虑下面创建类的函数,但这次,该类的属性可以基于传递给函数的参数而变化:
def get_credential_class(use_proxy = False, tfa = False):
if use_proxy:
keys = ['service_name', 'email_address']
else:
keys = ['username', 'password']
if tfa:
keys.append('tfa_token')
class Credential(object):
expected_keys = set(keys)
def __init__(self, **kwargs):
if self.expected_keys != set(kwargs.keys()):
raise ValueError('Keys do not match.')
for k, v in kwargs.items():
setattr(self, k, v)
return Credential
get_credential_class函数请求获得所发生的登录类型的信息-----是传统登录方式(使用用户名与密码)还是使用OpenID服务登录。如果是传统登录方式,或许还需要双因素认证,也就是额外需要验证令牌。
该函数返回一个类, 用于表示合适的凭据类型。例如,如果将use_proxy变量设置为True,则返回的类会包含设置为 [‘service_name’, ‘email_address’]的expected_keys 属性,代表通过代理身份验证所要的密钥。而向该函数传入的参数use_proxy为False时,将会返回带有不同expected_keys 属性的类。
然后,类的__init__方法检查从expected_keys 属性中获得的关键字参数。如果不匹配,则构造函数引发异常。如果匹配,将值写入实例。
可以使用class关键字而不是调用type在函数中创建该类。这是由于class代码块在def代码块中,该类是在函数作用域内创建的。
1.理解应该这样做的原因
假如是一个为大量不同的第三方网站提供凭据的服务就不同了。这类网站更倾向将所需的键与值类型存储在数据库中。
现在,你拥有了一个能够根据数据库查询结果生成属性的类。这很重要,因为数据库查询在运行时而不是在编码时发生。现在突然就有了一个类,这个类可以拥有无限种expected_keys 属性,而完全靠手动编码实现这点就变得不再现实。
将这类数据存入数据库同时也意味着,随着数据改变,代码却无需变更。一个网站或许需要修改或增加它支持的凭据类型,现在只需要在数据库中添加或删除行即可,而Credential类却无须修改即可继续使用。
2.属性字典
仅仅是某些属性只有在执行时可知并不是使用类工厂的必要条件。常常,可以即席将属性写入类,或是类仅仅存储一个包含任意属性集合的字典。
如果该方案可行,那么该方案就更加简单、更加直接。
class Credential(object):
attrs = {}
属性字典的最常见的缺点在于,当写一个子类继承现有类,而现有类无法直接控制时,需要现有的功能可以对修改过的属性进行操作。
(2)避免类属性一致性问题
另一个编写类工厂函数的原因是处理类与实例之间属性不同的问题。
1.类属性与实例属性
考虑下面两段代码没有产生等价的类或实例:
### CLASS ATTRIBUTE ###
class C(object):
foo = 'bar'
### INSTANCE ATTRIBUTE ###
class I(object):
def __init__(self):
self.foo = 'bar'
对于这两个类,首先也是最显著的区别是能够从什么位置访问foo属性。C.foo属性返回字符串,而I.foo属性引发AttributeError异常:
>>> C.foo
'bar'
>>> I.foo
Traceback (most recent call last):
File "<pyshell#8>", line 1, in <module>
I.foo
AttributeError: type object 'I' has no attribute 'foo'
毕竟,foo作为C的一个属性被实例化,但并不作为I的属性被实例化。由于直接访问I而不是I的实例,因此__init__函数还没有被运行。即使I的实例被创建后,也只有实例包含foo属性,而类并不包含该属性。
>>> i = I()
>>> i.foo
'bar'
>>> I.foo
Traceback (most recent call last):
File "<pyshell#11>", line 1, in <module>
I.foo
AttributeError: type object 'I' has no attribute 'foo'
然而,这里C和I之间的区别较小,该区别包括如果修改后的foo属性与它的实例不同时会发生什么。
考虑下面两个已经实例的C实例:
>>> c1 = C()
>>> c2 = C()
现在修改其中一个实例的foo属性,如下所示:
>>> c1.foo = 'xyz'
可以看到,现在c2实例依然使用类的属性,而c1有自己的属性:
>>> c1.foo
'xyz'
>>> c2.foo
'bar'
在此进行的查找并不十分相同。c1写了一个实例属性,名称为foo,值为’xyz’。而c2并没有这种实例属性。但是,由于类C有该属性,因此查找使用类属性。
考虑如果修改类属性时会发生的情况,如下所示:
>>> C.foo = 'abc'
>>> c1.foo
'xyz'
>>> c2.foo
'abc'
在这里,c1.foo不受影响,这是由于c1有名称为foo的实例属性。而c2.foo的值却发生了改变,因为实例中并没有这种属性。因此,当一个类的属性改变时,请观察实例的改变。
在Python的内部数据模型中可以通过这两个实例的__dict__属性来观察这一点:
>>> c1.__dict__
{'foo': 'xyz'}
>>> c2.__dict__
{}
在正常情况下,特殊的__dict__属性存储对象的所有属性(和值)。但也有例外,类A可能会自定义一个__getattr__或__getattribute__方法,或定义一个特殊属性__slots__,该属性也会引入替代属性行为(该属性很少使用,只有在特定场景下内存使用量很重要时才会被使用)。注意c1在__dict__中存在foo键。
2.类方法的限制
类方法是哪些并不需要类的实例就可以执行的方法,但需要类本身。它们通常使用@classmethod装饰器装饰一个方法来完成声明,并且方法的第一个参数按照传统会被命名为cls 而不是self。
考虑下面的类C,该类中包含能够访问并返回foo的类方法:
class C(object):
foo = 'bar'
@classmethod
def classfoo(cls):
return cls.foo
在classfoo方法的上下文中,foo属性在类中而不是实例中被显示访问。使用新的类定义重新运行该示例,并考虑如下代码:
>>> c1 = C()
>>> c1.foo
'bar'
>>> c1.classfoo()
'bar'
>>> c1.foo = 'xyz'
>>> c1.foo
'xyz'
>>> c1.classfoo()
'bar'
实际上,无法从类方法中访问实例属性。这也是类方法的要点,毕竟,它们并不需要一个实例。
3.使用类工厂尝试
需要类工厂的一个最大原因是当你继承一个现有类并且所依赖的类属性必须调整时。
本质上,在你无法控制的代码中,如果一个已存在的类设置某个必须自定义的类属性,类工厂是生成带有重载属性的恰当子类的一种恰当方式。
考虑这样一种情况,当一个类中包含了必须在运行时(或是在静态代码中子类的选择过多时)被重载的属性。在这种情况下,类工厂将会是一个非常有效的方案。
def create_C_subclass(new_foo):
class SubC(C):
foo = new_foo
return SubC
这里的重点是并不需要在类创建之前,也就是函数运行时,知道foo的值。与其他大多数类工厂的使用并无二致,是关于在运行时获得属性值。
以执行C子类的classfoo()类方法创建类的方式将会返回你期望的结果:
>>> S = create_C_subclass('spam')
>>> S.classfoo()
'spam'
>>> E = create_C_subclass('eggs')
>>> E.classfoo()
'eggs'
值得注意的是,在很多情况下,创建一个仅仅在__init__方法中接受该值的子类就简单多了。然而,也有一些情况使用这种方法并不可行。例如,父类依赖于类方法,此时将一个新值赋给实例并不会导致类方法接收到新值,这时该子类创建的模型将会是一个有价值的解决方案。
(3)关于单例模式问题的解答
让类工厂函数难以使用的一点是类工厂返回的是类,而不是类的实例。
这意味着如果你需要一个实例,则必须调用类工厂函数返回的结果才可以。例如,实例化一个使用create_C_subclass生成的子类,正确代码应该是create_C_subclass(‘eggs’)()。
这种做法并没有错,但可能并不是你所需要的结果。有些时候,通过类工厂创建的类功能上类似单例模式。单例模式是一种只允许一个实例的类模式。
在函数中生成类的情况下,有可能函数的目的就是作为一个类构造函数。最终开发人员必须不断考虑如何再次实例化所生成的类。
如果不需要面对在其他地方重用类或类工厂可以处理类重用的情况,就不需要处理这种情况,让类工厂返回其创建类的实例而不是类本身完全是合理且有用的。
def CPrime(new_foo = 'bar'):
if new_foo == 'bar':
return C()
class SubC(C):
foo = new_foo
return SubC()
现在,调用CPrime将会返回合适的C子类的实例,该类带有按需修改后的foo属性。
这种方式存在的一个问题是,很多(很可能是绝大多数)类需要将参数发送给__init__方法,此时该函数就无法处理这种情况。