挑戰:工資計算器讀寫數據文件

重新實現上一個挑戰中的計算器,可以支持從配置文件中讀取社保的稅率,並讀取員工工資數據 CSV 文件,同時將輸出信息寫入員工工資單 CSV 文件中。

計算器執行中包含下面的三個參數:

-c 社保比例配置文件:由於各地的社保比例稍有不同,需要爲每個城市提供一個單獨的社保比例的配置,本挑戰假定不考慮各地社保差異,僅提供一份通用配置。
-d 員工工資數據文件(CSV 格式): 指定員工工資數據文件,文件中包含兩列內容,分別爲員工工號和工資金額。
-o 員工工資單數據文件(CSV 格式): 輸出內容,將員工繳納的社保、稅前、稅後工資等詳細信息輸出到文件中。
1. 配置文件說明
社保比例配置文件格式示例如下(等號兩邊均有空格):
JiShuL = 2193.00
JiShuH = 16446.00
YangLao = 0.08
YiLiao = 0.02
ShiYe = 0.005
GongShang = 0
ShengYu = 0
GongJiJin = 0.06

將以上數據寫入 /home/shiyanlou/test.cfg 文件中。

配置文件中,各類保險以其漢語拼音命名(養老保險 → YangLao,公積金 → GongJiJin 等)。特別需要注意的是:

JiShuL 爲社保繳費基數的下限,即工資低於 JiShuL 的值的時候,需要按照 JiShuL 的數值乘以繳費比例來繳納社保。
JiShuH 爲社保繳費基數的上限,即工資高於 JiShuH 的值的時候,需要按照 JiShuH 的數值乘以繳費比例繳納社保。
當工資在 JiShuL 和 JiShuH 之間的時候,按照你實際的工資金額乘以繳費比例計算社保費用。
例如:當工資爲 20000 時,因爲社保基數爲 2193(JiShuL)~ 16446(JiShuH),所以是按照社保基數上限 16446(而不是用 20000) 去乘以社保的繳費比例計算實際繳納的社保數額。

  1. 員工工資數據文件說明
    員工工資數據文件,即本實驗中輸入的數據文件。每位員工工資數據單獨佔一行,文件格式爲 工號,稅前工資,舉例如下:
101,5000
203,6500
309,15000

將以上數據寫入 /home/shiyanlou/user.csv 文件中。

  1. 員工工資單數據文件說明
    員工工資單數據文件,即本實驗需要輸出得到的數據文件。同樣,輸出的員工工資單數據文件中,每行各項數據用逗號隔開,各項數據爲 工號,稅前工資,社保金額,個稅金額,稅後工資,舉例如下:
101,5000,825.00,0.00,4175.00
203,6500,1072.50,12.82,5414.68
309,15000,2475.00,542.50,11982.50

需要特別注意的是:

上面只是示例輸出(3 行數據),測試時候用的數據文件可能有更多行,輸出的文件行數要與測試文件行數相同,但不需要保持相同的順序。

程序的執行過程如下,配置文件 test.cfg 和輸入的員工數據文件 user.csv 需要自己創建並填入數據(可參考上述內容示例)。文件可以放在任何位置,只要參數中指定文件的路徑就可以了,示例如下:

$ ./calculator.py -c /home/shiyanlou/test.cfg -d /home/shiyanlou/user.csv -o /tmp/gongzi.csv

執行成功不需要輸出信息到屏幕,執行失敗或有異常出現則將錯誤信息輸出到屏幕。

import sys
import csv
from collections import namedtuple

# 稅率表條目類,該類由 namedtuple 動態創建,代表一個命名元組
IncomeTaxQuickLookupItem = namedtuple(
    'IncomeTaxQuickLookupItem',
    ['start_point', 'tax_rate', 'quick_subtractor']
)

# 起徵點常量
INCOME_TAX_START_POINT = 5000

# 稅率表,裏面的元素類型爲前面創建的 IncomeTaxQuickLookupItem
INCOME_TAX_QUICK_LOOKUP_TABLE = [
    IncomeTaxQuickLookupItem(80000, 0.45, 15160),
    IncomeTaxQuickLookupItem(55000, 0.35, 7160),
    IncomeTaxQuickLookupItem(35000, 0.30, 4410),
    IncomeTaxQuickLookupItem(25000, 0.25, 2660),
    IncomeTaxQuickLookupItem(12000, 0.2, 1410),
    IncomeTaxQuickLookupItem(3000, 0.1, 210),
    IncomeTaxQuickLookupItem(0, 0.03, 0)
]

class Args(object):
    """
    命令行參數處理類
    """

    def __init__(self):
        # 保存命令行參數列表
        self.args = sys.argv[1:]

    def _value_after_option(self, option):
        """
        內部函數,用來獲取跟在選項後面的值
        """

        try:
            # 獲得選項位置
            index = self.args.index(option)
            # 下一位置即爲選項值
            return self.args[index + 1]
        except (ValueError, IndexError):
            print('Parameter Error')
            exit()

    @property
    def config_path(self):
        """
        配置文件路徑
        """

        return self._value_after_option('-c')

    @property
    def userdata_path(self):
        """
        用戶工資文件路徑
        """

        return self._value_after_option('-d')

    @property
    def export_path(self):
        """
        稅後工資文件路徑
        """

        return self._value_after_option('-o')


# 創建一個全局參數類對象供後續使用
args = Args()

class Config(object):
    """
    配置文件處理類
    """

    def __init__(self):
        # 讀取配置文件
        self.config = self._read_config()

    def _read_config(self):
        """
        內部函數,用來讀取配置文件中的配置項
        """

        config = {}
        with open(args.config_path) as f:
            # 依次讀取配置文件裏的每一行並解析得到配置項名稱和值
            for line in f.readlines():
                key, value = line.strip().split('=')
                try:
                    # 去掉前後可能出現的空格
                    config[key.strip()] = float(value.strip())
                except ValueError:
                    print('Parameter Error')
                    exit()

        return config

    def _get_config(self, key):
        """
        內部函數,用來獲得配置項的值
        """

        try:
            return self.config[key]
        except KeyError:
            print('Config Error')
            exit()

    @property
    def social_insurance_baseline_low(self):
        """
        獲取社保基數下限
        """

        return self._get_config('JiShuL')

    @property
    def social_insurance_baseline_high(self):
        """
        獲取社保基數上限
        """

        return self._get_config('JiShuH')

    @property
    def social_insurance_total_rate(self):
        """
        獲取社保總費率
        """

        return sum([
            self._get_config('YangLao'),
            self._get_config('YiLiao'),
            self._get_config('ShiYe'),
            self._get_config('GongShang'),
            self._get_config('ShengYu'),
            self._get_config('GongJiJin')
        ])


# 創建一個全局的配置文件處理對象供後續使用
config = Config()

class UserData(object):
    """
    用戶工資文件處理類
    """

    def __init__(self):
        # 讀取用戶工資文件
        self.userlist = self._read_users_data()

    def _read_users_data(self):
        """
        內部函數,用來讀取用戶工資文件
        """

        userlist = []
        with open(args.userdata_path) as f:
            # 依次讀取用戶工資文件中的每一行並解析得到用戶 ID 和工資
            for line in f.readlines():
                employee_id, income_string = line.strip().split(',')
                try:
                    income = int(income_string)
                except ValueError:
                    print('Parameter Error')
                    exit()
                userlist.append((employee_id, income))

        return userlist

    def get_userlist(self):
        """
        獲取用戶數據列表
        """

        # 直接返回屬性 userlist 列表對象
        return self.userlist

class IncomeTaxCalculator(object):
    """
    稅後工資計算類
    """

    def __init__(self, userdata):
        # 初始化時接收一個 UserData 對象
        self.userdata = userdata

    @classmethod
    def calc_social_insurance_money(cls, income):
        """
        計算社保金額
        """

        if income < config.social_insurance_baseline_low:
            return config.social_insurance_baseline_low * \
                config.social_insurance_total_rate
        elif income > config.social_insurance_baseline_high:
            return config.social_insurance_baseline_high * \
                config.social_insurance_total_rate
        else:
            return income * config.social_insurance_total_rate

    @classmethod
    def calc_income_tax_and_remain(cls, income):
        """
        計算稅後工資
        """

        # 計算社保金額
        social_insurance_money = cls.calc_social_insurance_money(income)

        # 計算應納稅額
        real_income = income - social_insurance_money
        taxable_part = real_income - INCOME_TAX_START_POINT

        # 從高到低判斷落入的稅率區間,如果找到則用該區間的參數計算納稅額並返回結果
        for item in INCOME_TAX_QUICK_LOOKUP_TABLE:
            if taxable_part > item.start_point:
                tax = taxable_part * item.tax_rate - item.quick_subtractor
                return '{:.2f}'.format(tax), '{:.2f}'.format(real_income - tax)

        # 如果沒有落入任何區間,則返回 0
        return '0.00', '{:.2f}'.format(real_income)

    def calc_for_all_userdata(self):
        """
        計算所有用戶的稅後工資
        """

        result = []
        # 循環計算每一個用戶的稅後工資,並將結果彙總到結果集中
        for employee_id, income in self.userdata.get_userlist():
            # 計算社保金額
            social_insurance_money = '{:.2f}'.format(
                self.calc_social_insurance_money(income))

            # 計算稅後工資
            tax, remain = self.calc_income_tax_and_remain(income)

            # 添加到結果集
            result.append(
                [employee_id, income, social_insurance_money, tax, remain])

        return result

    def export(self):
        """
        導出所有用戶的稅後工資到文件
        """

        # 計算所有用戶的稅後工資
        result = self.calc_for_all_userdata()

        with open(args.export_path, 'w', newline='') as f:
            # 創建 csv 文件寫入對象
            writer = csv.writer(f)
            # 寫入多行數據
            writer.writerows(result)


if __name__ == '__main__':
    # 創建稅後工資計算器
    calculator = IncomeTaxCalculator(UserData())

    # 調用 export 方法導出稅後工資到文件
    calculator.export()
需要注意社保基數的處理,比如 20000 元工資高於社保基數的上限 JiShuH 的值,就應該用 JiShuH 這個值去乘以比例計算需要繳納的社保金額。

可以實現一個配置類 Config,來獲取並存儲配置文件中的信息,Config 類 def __init__(self, configfile) 中定義一個字典 self._config = {} 來存儲每個配置項和值,從文件中讀取的時候需要注意使用 strip() 去掉空格,並可以使用字符串的 split('=') 將配置項和值切分開。從 Config 對象中獲得配置信息的方法可以定義爲 def get_config(self),使用類似 config.get_config('JiShuH')。

可以實現一個員工數據類 UserData,來獲取並存儲員工數據,同樣 def __init__(self, userdatafile) 中定義一個字典 self.userdata = {} 存儲文件中讀取的用戶 ID及工資,並實現相應的金額計算的方法def calculator(self) 及輸出到文件中的方法 def dumptofile(self, outputfile)。

需要在上述類中實現文件讀取和寫入等操作,寫入的格式需要保證符合上述描述內容。

處理命令行參數的方式:

首先使用 args = sys.argv[1:] 獲得所有的命令行參數列表,即包括 -c test.cfg -d user.csv -o gongzi.csv 這些內容。
使用 index = args.index('-c') 獲得 -c 參數的索引,那麼配置文件的路徑就是 -c 後的參數即 configfile = args[index+1],同樣,其他的 -d 和 -o 參數也用這種方法獲得。
在 Windows 系統中使用 Python 代碼寫入 csv 文件會出現空行,加個參數 newline='' 即可解決:

>>> with open('xxx.csv', 'w', newline='') as f:
...     csv.writer(f).writerows(data)
...
深入理解python @classmethod
 
 
被@classmethod裝飾的方法
1. 強制帶個參數,cls,cls代表這個類本身
2. 不用實例化,和靜態方法一樣,直接 類().方法() 即可調用
3. cls是個位置參數,可隨意換成其他詞,如this
 
如想獲取類屬性x的值,可直接cls.x,等價於A.x
class A():
    x = 1
 
    @classmethod
    def B(cls):
        print(cls.x)
 
>> A.B()
1
 
 
已知cls代表類本身,那麼cls(123),就等價於A(123),調用init初始化,實例化爲x
cls(123) 等價於 x = A(123)
 
class A():
 
    def __init__(self, q):
        self.q = q
 
    @classmethod
    def B(cls):
        return cls(123)
 
>> x = A.B()
>> print(x.q)
123
運行邏輯:A() - B() - cls(123) - x = A(123)

也許:https://blog.csdn.net/qq_39698985/article/details/106729710?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.nonecase

使用Python內置的@property裝飾器就是負責把一個方法變成屬性調用:
https://www.cnblogs.com/phpper/p/10618775.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章