Python类工厂
类工厂顾名思义就是创造类的工厂(函数),也就是函数的返回值是一个类对象。我们可以使用这个类对象生成实例。而每一次执行函数都会得到一个"不同"(地址不同)的类对象,我们可以用不同的变量去接收这些类对象,并使用这些个类对象完成实例化得到类的实例。因此类工厂最大的作用就是,可以不用在执行前(编码时)就确定好我的类需要有哪些属性,有哪些方法,而是在执行过程中根据值执行结果自动生成我们所需要的类,这个类包含着我们需要的属性和方法(其实这里可以深刻的体会出什么叫做Python是一个动态语言)。
1、类工厂函数
我们在元类这一章(Python元类)讲到了我们可以通过type(类名, (object,), {...})的方式创造一个类对象,进而通过类对象可以得到对应的实例对象。采用这种元类方法创建对象可以自主的确定类的成员(函数/属性),如果我们把这个过程封装进一个函数里,当调用函数的时候,就返回一个类对象,那么这个函数就是类工厂函数。例如下面这个代码:
def create_class():
"""类工厂返回类对象, 注意返回的是类, 并不是类的实例"""
class People(object):
def __init__(self, name, age):
self.name = name
self.age = age
def study(self):
print(f"{self.name}正在学习")
return People
## 这里是两个类对象, 类似于以下两个语句
# People1 = type("People", (object,), {...})
# People2 = type("People", (object,), {...})
People1 = create_class()
People2 = create_class()
我们定义的create_class()函数里面就只有一个操作,那就是通过class关键字声明了一个类,并把这个类对象返回了。这里需要有元类的了解,其实函数内部每次都是在执行type("People", (object,), {...}),并把执行得到的类对象返回了,只不过采用了class关键字的方式去完成这个过程罢了。我们看上述代码的最后两行,我们使用了两个变量People1和People2去接收函数的返回值,意味着这里的变量People1和People2指向的其实时类对象,那么我们可以使用这两个变量创造实例了。
print(id(People1) == id(People2))
print(People1)
print(People2)
person1 = People1("小明",13)
person2 = People2("小华",14)
print(isinstance(person1, People1)) # true
print(isinstance(person2, People1)) # false
我们首先看一看这两个类对象的地址是不是一样的,再分别使用People1和People2去创造了两个实例,紧接着使用isinstance来判断实例与类之间的关系,执行结果如下:
可以看到首先,People1和People2指向的类对象地址是不一样的,着意味着尽管是通过同一个函数得到的类对象,但实际上是两个不同的类对象,虽然都叫People(这里的People是类的名字)。下面我们分别使用People1和People2创造了两个实例(People1创造的实例person1,People2创造的实例person2),并使用isinstance来判断实例与类的关系。我们发现,person2通过isinstance判断出与People1是没有关系的。这也进一步说明,实际上通过类工厂函数创造出的类对象,其实没有任何关系,这里来看只不过是两个person的包含的功能一样罢了。
2、类工厂函数使用场景
类工厂函数有什么用呢?一般情况下使用类工厂函数,是为了能根据传递进函数的参数,来选择性的构建类对象。比如有个需求创建People类,Dog类,Car类,每一个类都有自己的初始化属性,比如People类需要name和age,Dog类需要name和color,Cat类需要brand等。一种很容易想到的方式就是我直接创建三个类,需要用哪个就用哪个。我们能不能进行整合呢?比如我先把这些信息配置到一个文件中,并通过类工厂函数的参数,从文件中得到我们需要的初始化属性,并创建一个类对象返回呢?看下面的代码:
import csv
def create_class(classname):
params = [] ## 用于记录需要由类初始化时需要由那些属性
with open("类配置.csv", "r", encoding="utf-8-sig") as csv_file:
for row in csv.reader(csv_file): # 按行读取, row为列表, 以,分歌元素
if row == [] or row[0] != classname: ## 如果不是属于该类的属性则跳过
continue
params.append(row[1])
class Animal(object):
expected_param = params # 类变量
def __init__(self, **kwargs):
if set(self.expected_param) != set(kwargs.keys()):
raise ValueError(f"初始化属性与类不匹配, 需要有属性{self.expected_param}")
for k, v in kwargs.items():
setattr(self, k, v) ## 采用这种方式取代 self.属性名 = 属性值 的方式进行实例对象初始化(因为k,v都是字符串)
def print_info(self):
info_str = "对象具有以下属性: "
for key in self.expected_param: ## self.xxx也能访问类属性
info_str += f"{key}={getattr(self, key)} "
return info_str
return Animal # 返回类
代码的第5-9行通过读取“类配置.csv”这个文件的信息,并根据传入的classsname得到我们需要创建的类的信息。其中"类配置.csv"文件如下:
第一行列表示类的名称,第二列表示对应类需要那些初始化属性。代码的11到24行则是根据这些属性动态的创造了一个类,需要注意的是由于我们编码时并不知道要创造哪个类,因此我们在__init__中使用setattr(self,属性名,属性值)的方式创造属性(也就是代码中18行所作的操作)。我们测试一下:
MyClass1 = create_class("People")
MyClass2 = create_class("Car")
my_object1 = MyClass1(name="小明", age=14, gender="男") ## 用MyClass1类创造对象
my_object2 = MyClass2(money=20000, color="红色", brand="宝马") ## 用MyClass2类创造对象
print(my_object1.print_info())
print(my_object2.print_info())
我们传入不同的参数,执行create_class()类工厂函数,得到了两个类对象。并使用其分别创建了对应的实例对象,并执行实例对象的print_info()方法:
可以看到,确实正确执行了。
3、总结
当我们需要在运行时确认类的成员有哪些而不是在编码时,类工厂的作用就显现出来了。