python第三方库Faker源码解读

源码背景

Faker是一个Python第三方库,GITHUB开源项目,主要用于创建伪数据创建的数据包含地理信息类、基础信息类、个人账户信息类、网络基础信息类、浏览器信息类、文件信息类、数字类 文本加密类、时间信息类、其他类别等。

源码的地址:https://github.com/joke2k/faker
收集的函数速查:https://blog.csdn.net/qq_41545431/article/details/105006681

源码解读

主源码解读

通过直接点击初始化的类进入类初始化模块

fake = Faker(locale='zh_CN')

核心的源码搬运如下:

proxy.py文件
from __future__ import absolute_import, unicode_literals
from collections import OrderedDict
import random
import re
import six
from faker.config import DEFAULT_LOCALE
from faker.factory import Factory
from faker.generator import Generator
from faker.utils.distribution import choices_distribution

class Faker(object):
    """Proxy class capable of supporting multiple locales"""

    cache_pattern = re.compile(r'^_cached_\w*_mapping$')
    generator_attrs = [
        attr for attr in dir(Generator)
        if not attr.startswith('__')
        and attr not in ['seed', 'seed_instance', 'random']
    ]
    def __init__(self, locale=None, providers=None,
                 generator=None, includes=None, **config):
        self._factory_map = OrderedDict()
        self._weights = None

        if isinstance(locale, six.string_types):
            locales = [locale.replace('-', '_')]

        # This guarantees a FIFO ordering of elements in `locales` based on the final
        # locale string while discarding duplicates after processing
        elif isinstance(locale, (list, tuple, set)):
            assert all(isinstance(l, six.string_types) for l in locale)
            locales = []
            for l in locale:
                final_locale = l.replace('-', '_')
                if final_locale not in locales:
                    locales.append(final_locale)

        elif isinstance(locale, OrderedDict):
            assert all(isinstance(v, (int, float)) for v in locale.values())
            odict = OrderedDict()
            for k, v in locale.items():
                key = k.replace('-', '_')
                odict[key] = v
            locales = list(odict.keys())
            self._weights = list(odict.values())

        else:
            locales = [DEFAULT_LOCALE]

        for locale in locales:
            self._factory_map[locale] = Factory.create(locale, providers, generator, includes, **config)

        self._locales = locales
        self.AQ_factories = list(self._factory_map.values())

文件主要引入了内部类以及collections.OrderedDict,random,six等外部包。
以下对相应的关键外部包做个说明:

1、collections.OrderedDict实现了对字典对象中元素的排序,由于python的字典是按照hash值进行存储的所以,导致字典是无序状态,OrderedDict实现了对字典对象中元素的排序
2、six这个名字来源于 6 = 2 x 3,其产生主要是为了解决Python2 和 Python3 代码兼容性

初始化的开头按一定规则将Generator类下的属性保存在generator_attrs中,以备后续方法调用。在init的初始化中规定了类的入参有哪些?同时定义了两个名义上的私有变量,来防止外部调用类方法,说他是名义上的私有变量是因为python中没有真正的私有化,不管是方法还是属性,为了编程的需要,约定加了下划线 _的属性和方法不属于API,不应该在类的外面访问,也不会被from M import * 导入。但是注意你想调用也可以调用。self._factory_map中保存的是OrderedDict()的实例化对象;self._weights为了保证其在类中被调用,赋予初始化的None值。接下来的是对入参locale的条件判断,示意图基本如下:

proxy.py文件
if isinstance(locale, six.string_types):
    如果入参的locale是字符串,则替换-线为_线,保存在locales中

elif isinstance(locale, (list, tuple, set)):
    如果入参的locale是列表,元祖,集合,则遍历入参判断元素为字符串后将元素替换-线为_线保存在locales中
elif isinstance(locale, OrderedDict):
    如果入参的locale是有序字典,则遍历入参判断键为字符串后将键替换-线为_线保存在locales中,将键的值保存在之前定义的self._weights中
    locales = list(odict.keys())
    self._weights = list(odict.values())
else:
    以上条件都不满足时,将配置文件中自定义的locale保存到列表中赋值给locales

为什么在locale这个入参要做那么多的校验呢,是因为在初始化是locale做了一件很重要的事,而这件事对locale的要求很高,具体来看源码:

proxy.py文件
for locale in locales:
        self._factory_map[locale] = Factory.create(locale, providers, generator, includes, **config)

源码在这里主要做了对每种语言创建了一个map字典,里面涉及到了Factory工厂模式下的创建方法,入参基本为当前类的入参。那么Faker除了对locale入参进行了校验外,有没有做其他的校验呢?答案是肯定的在对关键属性self._weights、self._factories、self._factory_map.items(),通过object下的@property装饰器进行了只读的校验,外部修改。

魔法方法解读

对类的实例化后需要使用实例里面的属性,那么为了增加其扩展性加了getitem的魔法方法使的我们可以对Fake()['pujen']操作,那么在Faker中Fake()['pujen']会返回啥呢,源码中运算结果为KeyError,当然了因为Faker中没有pujen这个语言包。

proxy.py文件
def __getitem__(self, locale):
    return self._factory_map[locale.replace('-', '_')]
fake = Faker(locale='zh_CN')
print(fake['zh_CN'])

>>>    <faker.generator.Generator object at 0x0000021AEE18FDD8>

接下来看一个实例来更好的去理解什么是getitem魔法方法

class Fake(object):
    def __init__(self):
        self.name = 'jack'

    def __getitem__(self,item):
        if item in self.__dict__:       # item = key,判断该key是否存在对象的 __dict__ 里,
            return self.__dict__[item]  # 返回该对象 __dict__ 里key对应的value

    def __setitem__(self, key, value):
        self.__dict__[key] = value      # 在对象 __dict__ 为指定的key设置value

    def __delitem__(self, key):
        del self.__dict__[key]          # 在对象 __dict__ 里删除指定的key

f1 = Fake()
print(f1['name'])   # jack
f1['age'] =10       
print(f1['age'])    # 10
del f1['name']
print(f1.__dict__)  # {'age': 10}

接下来看一下getattribute__方法,这个方法出现在这个类中主要是因为防止seed()方法的直接调用而是要形如Faker.seed()这样的调用,在Faker的源码中seed()实际是Generator.seed()一种随机种子函数。假设调用类的方法中不是seed()而是其他非此类方法,那么会执行__getattr方法,这个方法在Faker里面主要是干了什么呢:

proxy.py文件
def __getattr__(self, attr):
    """
    Handles cache access and proxying behavior
    :param attr: attribute name
    :return: the appropriate attribute
    """
    条件语句判断异常情况,最后走如下代码
        factory = self._select_factory(attr)
        return getattr(factory, attr)
工厂模式

在初始化中我们会发现核心的内容最后都是由工厂模式的Factory.create()创建接下来看一下此工厂函数。在Factory中create()是以静态类方法来体现

factory.py文件
@classmethod
    def create(
            cls,
            locale=None,
            providers=None,
            generator=None,
            includes=None,
            **config):
        if includes is None:
            includes = []

        # fix locale to package name
        locale = locale.replace('-', '_') if locale else DEFAULT_LOCALE
        locale = pylocale.normalize(locale).split('.')[0]#返回规范化的语言环境代码
        if locale not in AVAILABLE_LOCALES:
            msg = 'Invalid configuration for faker locale `{0}`'.format(locale)
            raise AttributeError(msg)

        config['locale'] = locale
        providers = providers or PROVIDERS#排序的集合

        providers += includes

        faker = generator or Generator(**config)

        for prov_name in providers:
            if prov_name == 'faker.providers':
                continue

            prov_cls, lang_found = cls._get_provider_class(prov_name, locale)#prov_cls=faker.providers,lang_found语言包名称
            provider = prov_cls(faker)#继承在Generator类中
            provider.__provider__ = prov_name
            provider.__lang__ = lang_found
            faker.add_provider(provider)#增加类的方法和属性
        return faker

从上面的源码可以梳理出来,基本就是给类增加方法和规范一下语言包。我们对里面的一些细节代码梳理一下:

factory.py文件
1、
providers += includes

providers是一个空列表
includes是一个集合数据

那么假设providers=[],includes={1,2,3,4}
则providers += includes运行结果,会使的providers=[1,2,3,4],实际这段代码就是将集合的数据放到空列表中。
2、
faker = generator or Generator(**config)
provider = prov_cls(faker)

这里faker是generator类,prov_cls实际上是一个类,那么prov_cls(faker)实际就是继承了Generator类
3、
provider.__provider__ = prov_name
provider.__lang__ = lang_found
faker.add_provider(provider)#增加类的方法和属性

给这些类赋予方法名和语言包,同时通过魔法方法增加类的方法和属性,这里面涉及到Generator.add_provider()方法
Faker隐藏主方法类

以上工厂模式中create()主函数方法基本也介绍完了,类内部的其他方法暂时不过多的研究。接下来看一下在create()中涉及到的Generator.add_provider()方法,方法的源码如下:

generator.py文件
def  add_provider(self, provider):

    if isinstance(provider, type):
        provider = provider(self)

    self.providers.insert(0, provider)#将provider插入到0索引位置

    for method_name in dir(provider):
        # skip 'private' method
        if method_name.startswith('_'):
            continue

        faker_function = getattr(provider, method_name)#动态运行函数

        if callable(faker_function):#函数用于检查一个对象是否是可调用的
            # add all faker method to generator
            self.set_formatter(method_name, faker_function)

针对如下的这个用法做一下基本的说明,后续我们写代码的时候可以作为借鉴

if isinstance(provider, type):

说明:如果对象参数是classinfo参数的实例,或者是它的一个(直接、间接或虚拟)子类的实例,则返回True。如果对象不是给定类型的对象,则该函数始终返回False。如果classinfo是类型对象的元组(或者递归地,其他类似的元组),如果对象是任何类型的实例,则返回True。如果classinfo不是类型的类型或元组,而这些元组又不是类型的元组,则会引发类型错误异常。

for method_name in dir(provider):

dir的用法说明,如果provider类或者模块没有定义dir方法则返回类或者模块的方法属性

接下来看一下这两个方法,主要是用于动态调用函数返回运行对象

faker_function = getattr(provider, method_name)#动态运行函数

if callable(faker_function):#函数用于检查一个对象是否是可调用的

至此,Generator类中的核心方法介绍完成!

Faker里方法运行内部逻辑

当我们在pycharm里面写好方法打算去看一下类函数时,Ctrl+鼠标左击。奇怪的事情发生了,并没有进入到对应的方法里面去,同时pycharm智能提示我们:

fake = Faker(locale='zh_CN')
fake.random_digit_not_null()


通过上面的源码解析也可以很清晰的发现,Faker的方法和属性不像我们往常写的类一样在类的下面,全文解析基本没看到创建伪数据的直接方法和属性。那么下面来看一下方法的基本运行内部逻辑实现方式。

 

如图,在内部运行逻辑中实际上调用的是generator.py文件内容下的Generator.add_provider方法中有一个需要特别注意就是法,上面我们也提到了,在add_provider方法中有一个需要特别注意就是

for method_name in dir(provider):

通过这个基本的循环将所有的方法和属性加载到对应的语言包中,也就说Faker的属性和方法实际是在另外一个地方存放着,在使用的时候在拿过来,这样做使的Faker的本身类看起来简洁。那么外部是以什么形式来存放的呢?


可以看出在外部有一个provider包,包里面对应很多个方法归类包,在往内部层级就是对应每个语言包下的方法。来看一下具体方法的内部表现形式是如何的

 


可以发现基本是以元祖的方式存放的原始数据,我们方法运行后最终的结果都是来自于此,那么函数最后是如何运行方法的呢?其实最上面的源码解析已经提及到了,就是使用了init()下的

 

return getattr(factory, attr)

具体到每个方法或者函数上的实现方式由于太多了就不一一解读了,大范围的是使用random这个基本库来实现的。

文章原创首发于微信公众号 软件测试微课堂

posted @ 2020-04-01 21:03  pujen_yuan  阅读(958)  评论(1编辑  收藏  举报