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、总结

  当我们需要在运行时确认类的成员有哪些而不是在编码时,类工厂的作用就显现出来了。

 

posted @ 2023-04-03 17:32  Circle_Wang  阅读(267)  评论(0编辑  收藏  举报