为脚本提供多个文件还是为每个函数只提供一个大脚本文件更好?

Is it better to have multiple files for scripts or just one large script file with every function?

大约一年前,我开始了一个项目,涉及一个简单的基于终端的角色扮演游戏 Python 3。我没有认真考虑就直接投入其中。我开始为每个.. 函数组织多个脚本。但是在项目进行到一半时,为了最终目标,我不确定只拥有一个非常大的脚本文件或多个文件是否 easier/more 有效。

因为我正在为终端使用 cmd 模块,我意识到让实际的应用程序 运行 成为一个循环游戏可能对所有这些外部文件具有挑战性,但在同时我有一个 __init__.py 文件来组合主 运行 脚本的所有功能。这是文件结构。

澄清一下,我不是最伟大的程序员,而且我是 Python 的新手。我不确定 cmd 模块的兼容性问题。

所以我的问题是这样的; 我应该保留这个结构并且它应该按预期工作吗? 或者我应该将所有这些 assets 脚本合并到一个文件中吗?或者甚至将它们与使用 cmd 的 start.py 分开?这是启动函数,加上各种脚本的一些片段。

start.py

from assets import *
from cmd import Cmd
import pickle
from test import TestFunction
import time
import sys
import os.path
import base64

class Grimdawn(Cmd):

    def do_start(self, args):
        """Start a new game with a brand new hero."""
        #fill
    def do_test(self, args):
        """Run a test script. Requires dev password."""
        password = str(base64.b64decode("N0tRMjAxIEJSRU5ORU1BTg=="))
        if len(args) == 0:
            print("Please enter the password for accessing the test script.")
        elif args == password:
            test_args = input('> Enter test command.\n> ')
            try:
                TestFunction(test_args.upper())
            except IndexError:
                print('Enter a command.')
        else:
            print("Incorrect password.")
    def do_quit(self, args):
        """Quits the program."""
        print("Quitting.")
        raise SystemExit


if __name__ == '__main__':

    prompt = Grimdawn()
    prompt.prompt = '> '
    #ADD VERSION SCRIPT TO PULL VERSION FROM FOR PRINT
    prompt.cmdloop('Joshua B - Grimdawn v0.0.3 |')

test.py

from assets import *
def TestFunction(args):
    player1 = BaseCharacter()
    player2 = BerserkerCharacter('Jon', 'Snow')
    player3 = WarriorCharacter('John', 'Smith')
    player4 = ArcherCharacter('Alexandra', 'Bobampkins')
    shop = BaseShop()
    item = BaseItem()
    #//fix this to look neater, maybe import switch case function
    if args == "BASE_OFFENSE":
        print('Base Character: Offensive\n-------------------------\n{}'.format(player1.show_player_stats("offensive")))
        return
    elif args == "BASE_DEFENSE":
        print('Base Character: Defensive\n-------------------------\n{}'.format(player1.show_player_stats("defensive")))
        return

 *   *   *

player.py

#import functions used by script
#random is a math function used for creating random integers
import random
#pickle is for saving/loading/writing/reading files
import pickle
#sys is for system-related functions, such as quitting the program
import sys
#create a class called BaseCharacter, aka an Object()
class BaseCharacter:
    #define what to do when the object is created, or when you call player = BaseCharacter()
    def __init__(self):
        #generate all the stats. these are the default stats, not necessarily used by the final class when player starts to play.
        #round(random.randint(25,215) * 2.5) creates a random number between 25 and 215, multiplies it by 2.5, then roudns it to the nearest whole number
        self.gold = round(random.randint(25, 215) * 2.5)
        self.currentHealth = 100
        self.maxHealth = 100
        self.stamina = 10
        self.resil = 2
        self.armor = 20
        self.strength = 15
        self.agility = 10
        self.criticalChance = 25
        self.spellPower = 15
        self.intellect = 5
        self.speed = 5
        self.first_name = 'New'
        self.last_name = 'Player'
        self.desc = "Base Description"
        self.class_ = None
        self.equipment = [None] * 6
    #define the function to update stats when the class is set
    def updateStats(self, attrs, factors):
        #try to do a function
        try:
            #iterate, or go through data
            for attr, fac in zip(attrs, factors):
                val = getattr(self, attr)
                setattr(self, attr, val * fac)
        #except an error with a value given or not existing values
        except:
            raise("Error updating stats.")
    #print out the stats when called
    #adding the category line in between the ( ) makes it require a parameter when called
    def show_player_stats(self, category):
 *   *   *

注意

脚本的目的是显示它们具有什么样的结构,因此它有助于支持我是否应该合并的问题

将内容放入单个文件的 pythonic 方法(我将讨论它主要适用于 类)是单个文件是 模块(不是我之前说的包裹)。

许多工具通常会存在于单个包中,但单个模块中的所有工具都应保持以单个主题为中心。话虽如此,我通常会将一个非常小的项目保存在一个文件中,其中包含多个函数,可能还有几个 类 。然后我会使用 if main 来包含我想要的完整脚本 运行。

if __name__== '__main__': 

我会尽可能将逻辑分解为功能,以便脚本的主体作为更高级别的逻辑可读。

简短回答:每个功能的文件在任何规模上都不可管理。您应该将具有相关功能的东西放在一起放入文件(模块)中。是否将当前功能集中到模块中,由您决定。

组织代码的方法有多种,最终归结为:

  1. 个人喜好
  2. 项目的团队编码标准
  3. 贵公司的命名/结构/架构约定

我组织 Python 代码的方式是创建几个目录:

  • class_files(可重用代码)
  • input_files(脚本读取的文件)
  • output_files(脚本编写的文件)
  • 脚本(执行的代码)

这对我很有帮助。相对地导入您的路径,以便代码可以从任何克隆的地方 运行。以下是我在脚本文件中处理导入的方式:

import sys
# OS Compatibility for importing Class Files
if(sys.platform.lower().startswith('linux')):
  sys.path.insert(0,'../class_files/')
elif(sys.platform.lower().startswith('win')):
  sys.path.insert(0,'..\class_files\')

from some_class_file import my_reusable_method

这种方法还可以让您的代码 运行 出现在 Python 的各种版本中,并且您的代码可以根据需要进行检测和导入。

if(sys.version.find('3.4') == 0):
  if(sys.platform.lower().startswith('linux') or sys.platform.lower().startswith('mac')):
            sys.path.insert(0,'../modules/Python34/')
            sys.path.insert(0,'../modules/Python34/certifi/')
            sys.path.insert(0,'../modules/Python34/chardet/')
            sys.path.insert(0,'../modules/Python34/idna/')
            sys.path.insert(0,'../modules/Python34/requests/')
            sys.path.insert(0,'../modules/Python34/urllib3/')
    elif(sys.platform.lower().startswith('win')):
            sys.path.insert(0,'..\modules\Python34\')
            sys.path.insert(0,'..\modules\Python34\certifi\')
            sys.path.insert(0,'..\modules\Python34\chardet\')
            sys.path.insert(0,'..\modules\Python34\idna\')
            sys.path.insert(0,'..\modules\Python34\requests\')
            sys.path.insert(0,'..\modules\Python34\urllib3\')
    else:
            print('OS ' + sys.platform + ' is not supported')
elif(sys.version.find('2.6') == 0):
    if(sys.platform.lower().startswith('linux') or sys.platform.lower().startswith('mac')):
            sys.path.insert(0,'../modules/Python26/')
            sys.path.insert(0,'../modules/Python26/certifi/')
            sys.path.insert(0,'../modules/Python26/chardet/')
            sys.path.insert(0,'../modules/Python26/idna/')
            sys.path.insert(0,'../modules/Python26/requests/')
            sys.path.insert(0,'../modules/Python26/urllib3/')
    elif(sys.platform.lower().startswith('win')):
            sys.path.insert(0,'..\modules\Python26\')
            sys.path.insert(0,'..\modules\Python26\certifi\')
            sys.path.insert(0,'..\modules\Python26\chardet\')
            sys.path.insert(0,'..\modules\Python26\idna\')
            sys.path.insert(0,'..\modules\Python26\requests\')
            sys.path.insert(0,'..\modules\Python26\urllib3\')
    else:
            print('OS ' + sys.platform + ' is not supported')
else:
    print("Your OS and Python Version combination is not yet supported")

首先是一些术语:

  • “脚本”是旨在直接执行的 python (.py) 文件 (python myscript.py)
  • “模块”是一个 python 文件(通常主要包含函数和 类 定义),旨在由脚本或其他模块导入。
  • “包”是一个最终包含模块的目录和(在 py2 中是必需的,而不是在 py3 中)一个 __init__.py 文件。

您可以查看教程以了解有关模块和包的更多信息。

基本上,您想要的是以连贯的单元(包/模块/脚本)组织代码。

对于一个完整的应用程序,您通常会有一个“主”模块(不必命名为“main.py”——实际上它通常被命名为应用程序本身),它只会导入一些定义(来自标准库、第三部分库和您自己的模块),设置内容和 运行 应用程序的入口点。在您的示例中,这将是“start.py”脚本。

剩下的代码,你想要的是每个模块的内聚性强(里面定义的函数和类密切相关,共同实现相同的特性)和低耦合(每个模块都是一样的)尽可能独立于其他模块)。从技术上讲,您可以在单个模块中放置任意多的功能和 类,但是太大的模块会变得难以维护,因此如果在基于高内聚/低耦合的第一次重组之后您发现自己一个 5000+klocs 的模块,你可能想把它变成一个包含更多专用子模块的包。

如果您仍然有几个实用函数显然不适合您的任何模块,通常的解决方案是将它们放在一个“utils.py”(或“misc.py”中”或“helpers.py”等)模块。

您绝对要避免的两件事是:

  1. 循环依赖,直接(模块A依赖模块B,模块B依赖模块A)或间接(模块A依赖模块B,模块B又依赖模块A)。如果你发现你有这样的情况,这意味着你应该将两个模块合并在一起或者将一些定义提取到第三个模块。

  2. 通配符导入(“from module import *”),这是一个主要的 PITA wrt/可维护性(你无法从导入中分辨出某些名称是从哪里导入的)并使代码成为主题意外 - 有时不明显 - 破损

如您所见,这仍然是一个非常通用的指南,但是无法自动决定哪些属于一起,最终取决于您自己的判断。

你目前的方式很好,我个人更喜欢很多文件,因为它更容易维护。我看到的主要问题是你的所有代码都在 assets 下,所以你要么最终把所有东西都扔在那里(打败了调用它的意义),要么你最终会得到一个一旦开始编码其他位(例如 world/levels 等),文件夹就会有点乱。

一种非常常见的项目设计方式是您的根是 Grimdawn,它包含一个文件来调用您的代码,然后您所有的实际代码都放在 Grimdawn/grimdawn 中。我个人会忘记 assets 文件夹,而是将所有内容都放在该文件夹的根目录下,只有当某些文件变得更复杂或可以分组时才会更深入。

我会建议这样的事情(例如添加一些内容):

Grimdawn/characters/Jon_Snow
Grimdawn/characters/New_Player
Grimdawn/start.py
Grimdawn/grimdawn/utils/(files containing generic functions that are not game specific)
Grimdawn/grimdawn/classes.py
Grimdawn/grimdawn/combat.py
Grimdawn/grimdawn/items.py
Grimdawn/grimdawn/mobs/generic.py
Grimdawn/grimdawn/mobs/bosses.py
Grimdawn/grimdawn/player.py
Grimdawn/grimdawn/quests/quest1.py
Grimdawn/grimdawn/quests/quest2.py
Grimdawn/grimdawn/shops.py