源码背景
Faker是一个Python第三方库,GITHUB开源项目,主要用于创建伪数据创建的数据包含地理信息类、基础信息类、个人账户信息类、网络基础信息类、浏览器信息类、文件信息类、数字类 文本加密类、时间信息类、其他类别等。
源码解读
主源码解读
通过直接点击初始化的类进入类初始化模块
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'])
>>>
接下来看一个实例来更好的去理解什么是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这个基本库来实现的。
文章原创首发于微信公众号 软件测试微课堂