Генератор имени страны Марков

Я написал генератор названия страны в Python 3.5. Моя цель состояла в том, чтобы получить рандомизированные имена, которые будут похожи на имена реальных людей, насколько это возможно. Каждое имя должно иметь существительное и прилагательную форму (например, Italy и Italian).

Я начал со списка реальных стран, регионов и городов, которые я сохранил в текстовом файле. Имена разбиваются по слогам, причем разделение существительное и прилагательное (например, i-ta-l y/ian). Программа разбивает каждое имя на слоги, а каждый слог на три сегмента : начало , ядро ​​и коду (т. е. ведущие согласные, гласные и конечные согласные). Затем он использует частоту этих сегментов по отношению друг к другу для управления процессом Маркова , который генерирует имя. (Это не чистый марковский процесс, потому что я хотел обеспечить распределение количества слогов, аналогичное набору входных данных. Я также специально обрезал окончания.) Отклонено несколько типов нежелательных имен.

Основной код

#!/usr/bin/python3

import re, random

# A regex that matches a syllable, with three groups for the three
# segments of the syllable: onset (initial consonants), nucleus (vowels),
# and coda (final consonants).
# The regex also matches if there is just an onset (even an empty
# onset); this case corresponds to the final partial syllable of the
# stem, which is usually the consonant before a vowel ending (for
# example, the d in "ca-na-d a").
syllableRgx = re.compile(r"(y|[^aeiouy]*)([aeiouy]+|$)([^aeiouy]*)")
nameFile = "names.txt"

# Dictionary that holds the frequency of each syllable count (note that these
# are the syllables *before* the ending, so "al-ba-n ia" only counts two)
syllableCounts = {}

# List of four dictionaries (for onsets, nuclei, codas, and endings):
# Each dictionary's key/value pairs are prevSegment:segmentDict, where
# segmentDict is a frequency dictionary of various onsets, nuclei, codas,
# or endings, and prevSegment is a segment that can be the last nonempty
# segment preceding them. A prevSegment of None marks segments at the
# beginnings of names.
segmentData = [{}, {}, {}, {}]
ONSET = 0
NUCLEUS = 1
CODA = 2
ENDING = 3

# Read names from file and generate the segmentData structure
with open(nameFile) as f:
    for line in f.readlines():
        # Strip whitespace, ignore blank lines and comments
        line = line.strip()
        if not line:
            continue
        if line[0] == "#":
            continue
        stem, ending = line.split()
        # Endings should be of the format noun/adj
        if "/" not in ending:
            # The noun ending is given; the adjective ending can be
            # derived by appending -n
            ending = "{}/{}n".format(ending, ending)
        # Syllable count is the number of hyphens
        syllableCount = stem.count("-")
        if syllableCount in syllableCounts:
            syllableCounts[syllableCount] += 1
        else:
            syllableCounts[syllableCount] = 1

        # Add the segments in this name to segmentData
        prevSegment = None
        for syllable in stem.split("-"):
            segments = syllableRgx.match(syllable).groups()
            if segments[NUCLEUS] == segments[CODA] == "":
                # A syllable with emtpy nucleus and coda comes right before
                # the ending, so we only process the onset
                segments = (segments[ONSET],)
            for segType, segment in enumerate(segments):
                if prevSegment not in segmentData[segType]:
                    segmentData[segType][prevSegment] = {}
                segFrequencies = segmentData[segType][prevSegment]
                if segment in segFrequencies:
                    segFrequencies[segment] += 1
                else:
                    segFrequencies[segment] = 1
                if segment:
                    prevSegment = segment
        # Add the ending to segmentData
        if prevSegment not in segmentData[ENDING]:
            segmentData[ENDING][prevSegment] = {}
        endFrequencies = segmentData[ENDING][prevSegment]
        if ending in endFrequencies:
            endFrequencies[ending] += 1
        else:
            endFrequencies[ending] = 1


def randFromFrequencies(dictionary):
    "Returns a random dictionary key, where the values represent frequencies."

    keys = dictionary.keys()
    frequencies = dictionary.values()
    index = random.randrange(sum(dictionary.values()))
    for key, freq in dictionary.items():
        if index < freq:
            # Select this one
            return key
        else:
            index -= freq
    # Weird, should have returned something
    raise ValueError("randFromFrequencies didn't pick a value "
                     "(index remainder is {})".format(index))

def markovName(syllableCount):
    "Generate a country name using a Markov-chain-like process."

    prevSegment = None
    stem = ""
    for syll in range(syllableCount):
        for segType in [ONSET, NUCLEUS, CODA]:
            try:
                segFrequencies = segmentData[segType][prevSegment]
            except KeyError:
                # In the unusual situation that the chain fails to find an
                # appropriate next segment, it's too complicated to try to
                # roll back and pick a better prevSegment; so instead,
                # return None and let the caller generate a new name
                return None
            segment = randFromFrequencies(segFrequencies)
            stem += segment
            if segment:
                prevSegment = segment

    endingOnset = None
    # Try different onsets for the last syllable till we find one that's
    # legal before an ending; we also allow empty onsets. Because it's
    # possible we won't find one, we also limit the number of retries
    # allowed.
    retries = 10
    while (retries and endingOnset != ""
           and endingOnset not in segmentData[ENDING]):
        segFrequencies = segmentData[ONSET][prevSegment]
        endingOnset = randFromFrequencies(segFrequencies)
        retries -= 1
    stem += endingOnset
    if endingOnset != "":
        prevSegment = endingOnset
    if prevSegment in segmentData[ENDING]:
        # Pick an ending that goes with the prevSegment
        endFrequencies = segmentData[ENDING][prevSegment]
        endings = randFromFrequencies(endFrequencies)
    else:
        # It can happen, if we used an empty last-syllable onset, that
        # the previous segment does not appear before any ending in the
        # data set. In this case, we'll just use -a(n) for the ending.
        endings = "a/an"
    endings = endings.split("/")
    nounForm = stem + endings[0]
    # Filter out names that are too short or too long
    if len(nounForm) < 3:
        # This would give two-letter names like Mo, which don't appeal
        # to me
        return None
    if len(nounForm) > 11:
        # This would give very long names like Imbadossorbia that are too
        # much of a mouthful
        return None
    # Filter out names with weird consonant clusters at the end
    for consonants in ["bl", "tn", "sr", "sn", "sm", "shm"]:
        if nounForm.endswith(consonants):
            return None
    # Filter out names that sound like anatomical references
    for bannedSubstring in ["vag", "coc", "cok", "kok", "peni"]:
        if bannedSubstring in stem:
            return None
    if nounForm == "ass":
        # This isn't a problem if it's part of a larger name like Assyria,
        # so filter it out only if it's the entire name
        return None
    return stem, endings

Код проверки

def printCountryNames(count):
    for i in range(count):
        syllableCount = randFromFrequencies(syllableCounts)
        nameInfo = markovName(syllableCount)
        while nameInfo is None:
            nameInfo = markovName(syllableCount)
        stem, endings = nameInfo
        stem = stem.capitalize()
        noun = stem + endings[0]
        adjective = stem + endings[1]
        print("{} ({})".format(noun, adjective))

if __name__ == "__main__":
    printCountryNames(10)

Пример names.txt содержание

# Comments are ignored
i-ta-l y/ian
# A suffix can be empty
i-ra-q /i
# The stem can end with a syllable break
ge-no- a/ese
# Names whose adjective suffix just adds an -n need only list the noun suffix
ar-me-n ia
sa-mo- a

Мой полный names.txt файл можно найти вместе с кодом в this Gist .

Пример вывода

Сгенерировано с использованием полного файла данных:

 Slorujarnia (Slorujarnian)
Ashmar (Ashmari)
Babya (Babyan)
Randorkia (Randorkian)
Esanoa (Esanoese)
Manglalia (Manglalic)
Konara (Konaran)
Lilvispia (Lilvispian)
Cenia (Cenian)
Rafri (Rafrian)

Вопросы

  • Является ли мой код доступным для чтения? Очистить имена переменных и функций? Достаточные комментарии?
  • Должен ли я реструктурировать что-нибудь?
  • Есть ли возможности Python 3, которые я мог бы использовать или использовать лучше? Я особенно не использую формат format и различные подходы к его использованию.

Если вы видите что-нибудь еще, что можно улучшить, сообщите мне. Единственное исключение: я знаю, что стандарт PEP - это snake_case, но Я хочу использовать camelCase , и я не намерен его изменять. Другие советы по форматированию приветствуются.

32 голоса | спросил DLosc 14 AM00000080000000531 2017, 08:35:05

2 ответа


16
  • Лучше следовать PEP8, в котором говорится, что заявления импорта , например, в вашем случае, следует использовать несколько строк:

    import re
    import random
    
  • Независимо от используемого вами языка программирования, лучше избегать операций ввода-вывода, когда это возможно. Поэтому вместо хранения имен стран в текстовом файле вы можете выбрать подходящую структуру данных Python для этой цели.
  • Когда кто-то читает вашу основную программу, он должен прямо знать, что он делает. Это не относится к вашему файлу main.py , где я вижу много отвлекающей информации и шума. Например, вы должны сохранить все эти константы в отдельном модуле, который вы можете вызвать configurations.py , cfg.py , settings.py или что угодно имя, которое вы считаете, вписывается в архитектуру вашего проекта.
  • Выберите значимые имена: в то время как большинство имен, которые вы выбрали, являются съедобными, я считаю, что вы по-прежнему можете внести некоторые улучшения для некоторых из них. Это относится, например, к nameFile, который является слишком неопределенным и не содержит никакой информации, кроме одной из самой операции присваивания: nameFile = "names.txt". Уверен, что это имя файла, но только после прочтения заявления вашей программы, что можно догадаться, что вы бы подразумевали под nameFile, и начать предлагать вам сразу более подходящее и информативное имя, например countries_names. Обратите внимание, что в моем предложении нет имени контейнера. Я имею в виду, я не заставляю читателя вашего кода знать информацию о программировании, например, хранят ли вы информацию в файле или той или иной структуре данных. Имена должны быть «высокоуровневыми» и независимыми от структуры данных, которые они представляют. Это дает вам преимущество не указывать одно и то же имя в вашей программе, чтобы переписать его только потому, что вы изменили данные хранилища из файла на другую структуру данных. Это также относится к syllableRgx: точно, когда кто-то читает syllableRgx = re.compile(r"..."), он понимает, что вы сохраняете результат выражения регулярного выражения. Но вы должны изменить это имя на что-то лучше по причинам, которые я объяснил раньше.
  • Вы должны следовать стандартным соглашениям об именах . Например, syllableCounts и segmentData должны быть записаны syllable_counts и segment_data соответственно.
  • Я хочу использовать camelCase №. Когда вы развиваетесь на данном языке программирования, принимайте его дух и философию. Это похоже на то, когда вы присоединяетесь к команде разработчиков новой компании: приспосабливайтесь к себе и не просите их приспособиться к вашим привычкам и желаниям.
ответил Billal BEGUERADJ 14 AM00000090000004631 2017, 09:14:46
13

Цитирование по строкам файла

Вероятно, незначительный nitpick, но при использовании объекта файла, возвращаемого open(), вы можете просто перебирать объект вместо вызова readlines(), например так:

# Read names from file and generate the segmentData structure
with open(nameFile) as input_names:
    for line in input_names:

Из документов :

  

readlines(hint=-1)

     

Прочитайте и верните список строк из потока. подсказка может быть   для контроля количества строк: нет больше строк.   прочитайте, если общий размер (в байтах /символах) всех строк до сих пор   превышает подсказка .

     

Обратите внимание, что уже можно итерации по объектам файлов с помощью for line in file: ... без вызова file.readlines().

Итак, если вы не собираетесь ограничивать чтение данных, тогда нет необходимости использовать readlines().

Тестирование, если какой-либо элемент соответствует условию

... можно выполнить с помощью any() , map() и соответствующей функции, поэтому:

# Filter out names with weird consonant clusters at the end
weird_consonant_clusters = ["bl", "tn", "sr", "sn", "sm", "shm"]
if any(map(nounForm.endswith, weird_consonant_clusters)):
    return None

В случае с кодом bannedSubstring нет прямого эквивалента in: вам нужно будет использовать count() или написать лямбда, поэтому здесь не так много.

Увеличивает или набор

Для частот, где вы делаете приращение или установку , вы можете использовать get или defaultdict , чтобы это:

if ending in endFrequencies:
    endFrequencies[ending] += 1
else:
    endFrequencies[ending] = 1

становится:

endFrequencies[ending] = endFrequencies.get(ending, 0) + 1

Или, если endFrequencies является defaultdict(int), просто:

endFrequencies[ending] += 1
ответил muru 14 PM00000010000003831 2017, 13:02:38

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132