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這個基本庫來實現的。

文章原創首發於微信公眾號 軟件測試微課堂


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM