检查嵌套字典中的成员资格

1 投票
2 回答
1452 浏览
提问于 2025-04-15 23:07

这是一个跟之前问题相关的后续提问:

Python DictReader - 跳过缺少列的行?

结果我发现自己搞错了,使用了错误的ID字段。

顺便说一下,我这里用的是Python 3.x。

我有一个员工的字典,使用一个字符串“directory_id”作为索引。每个值是一个嵌套的字典,里面包含员工的属性(比如电话号码、姓氏等)。其中一个属性是一个次要ID,叫“internal_id”,另一个是他们的经理,叫“manager_internal_id”。“internal_id”这个字段不是必须的,并不是每个员工都有。

{'6443410501': {'manager_internal_id': '989634', 'givenName': 'Mary', 'phoneNumber': '+65 3434 3434', 'sn': 'Jones', 'internal_id': '434214'}
'8117062158': {'manager_internal_id': '180682', 'givenName': 'John', 'phoneNumber': '+65 3434 3434', 'sn': 'Ashmore', 'internal_id': ''}
'9227629067': {'manager_internal_id': '347394', 'givenName': 'Wright', 'phoneNumber': '+65 3434 3434', 'sn': 'Earl', 'internal_id': '257839'}
'1724696976': {'manager_internal_id': '907239', 'givenName': 'Jane', 'phoneNumber': '+65 3434 3434', 'sn': 'Bronte', 'internal_id': '629067'}

}

(我稍微简化了一下字段,以便更容易阅读,同时也出于隐私和合规的考虑)。

这里的问题是,我们用“directory_id”来索引每个员工,但当我们查找他们的经理时,需要通过“internal_id”来找到经理。

之前,当我们的字典使用“internal_id”作为键时,employee.keys()返回的是一个“internal_id”的列表,我是用这个来检查的。现在,我的if语句的最后一部分不工作了,因为“internal_id”是字典值的一部分,而不是键本身。

def lookup_supervisor(manager_internal_id, employees):
    if manager_internal_id is not None and manager_internal_id != "" and manager_internal_id in employees.keys():
        return (employees[manager_internal_id]['mail'], employees[manager_internal_id]['givenName'], employees[manager_internal_id]['sn'])
    else:
        return ('Supervisor Not Found', 'Supervisor Not Found', 'Supervisor Not Found')

所以第一个问题是,我该如何修正if语句,以检查manager_internal_id是否在字典的internal_id列表中?

我试着把employee.keys()替换成employee.values(),但这没用。而且,我希望能有更高效的方法,不知道有没有办法获取值的子集,特别是所有员工[directory_id]['internal_id']的条目。

希望有一种Pythonic的方式来做到这一点,而不需要使用大量嵌套的for/if循环。

我的第二个问题是,我该如何干净利落地返回所需的员工属性(邮箱、名字、姓氏等)。我的for循环是遍历每个员工,并调用lookup_supervisor。我在这里感觉有点傻,不知道该怎么办。

def tidy_data(employees):
    for directory_id, data in employees.items():
        # We really shouldnt' be passing employees back and forth like this - hmm, classes?
        data['SupervisorEmail'], data['SupervisorFirstName'], data['SupervisorSurname'] = lookup_supervisor(data['manager_internal_id'], employees)

我应该重新设计我的数据结构吗?还是有其他方法?

编辑:我稍微调整了一下代码,见下文:

class Employees:

    def import_gd_dump(self, input_file="test.csv"):
        gd_extract = csv.DictReader(open(input_file), dialect='excel')
        self.employees = {row['directory_id']:row for row in gd_extract}

    def write_gd_formatted(self, output_file="gd_formatted.csv"):
        gd_output_fieldnames = ('internal_id', 'mail', 'givenName', 'sn', 'dbcostcenter', 'directory_id', 'manager_internal_id', 'PHFull', 'PHFull_message', 'SupervisorEmail', 'SupervisorFirstName', 'SupervisorSurname')
        try:
            gd_formatted = csv.DictWriter(open(output_file, 'w', newline=''), fieldnames=gd_output_fieldnames, extrasaction='ignore', dialect='excel')
        except IOError:
            print('Unable to open file, IO error (Is it locked?)')
            sys.exit(1)

        headers = {n:n for n in gd_output_fieldnames}
        gd_formatted.writerow(headers)
        for internal_id, data in self.employees.items():
            gd_formatted.writerow(data)

    def tidy_data(self):
        for directory_id, data in self.employees.items():
            data['PHFull'], data['PHFull_message'] = self.clean_phone_number(data['telephoneNumber'])
            data['SupervisorEmail'], data['SupervisorFirstName'], data['SupervisorSurname'] = self.lookup_supervisor(data['manager_internal_id'])

    def clean_phone_number(self, original_telephone_number):
        standard_format = re.compile(r'^\+(?P<intl_prefix>\d{2})\((?P<area_code>\d)\)(?P<local_first_half>\d{4})-(?P<local_second_half>\d{4})')
        extra_zero = re.compile(r'^\+(?P<intl_prefix>\d{2})\(0(?P<area_code>\d)\)(?P<local_first_half>\d{4})-(?P<local_second_half>\d{4})')
        missing_hyphen = re.compile(r'^\+(?P<intl_prefix>\d{2})\(0(?P<area_code>\d)\)(?P<local_first_half>\d{4})(?P<local_second_half>\d{4})')
        if standard_format.search(original_telephone_number):
            result = standard_format.search(original_telephone_number)
            return '0' + result.group('area_code') + result.group('local_first_half') + result.group('local_second_half'), ''
        elif extra_zero.search(original_telephone_number):
            result = extra_zero.search(original_telephone_number)
            return '0' + result.group('area_code') + result.group('local_first_half') + result.group('local_second_half'), 'Extra zero in area code - ask user to remediate. '
        elif missing_hyphen.search(original_telephone_number):
            result = missing_hyphen.search(original_telephone_number)
            return '0' + result.group('area_code') + result.group('local_first_half') + result.group('local_second_half'), 'Missing hyphen in local component - ask user to remediate. '
        else:
            return '', "Number didn't match format. Original text is: " + original_telephone_number    

    def lookup_supervisor(self, manager_internal_id):
        if manager_internal_id is not None and manager_internal_id != "":# and manager_internal_id in self.employees.values():
            return (employees[manager_internal_id]['mail'], employees[manager_internal_id]['givenName'], employees[manager_internal_id]['sn'])
        else:
            return ('Supervisor Not Found', 'Supervisor Not Found', 'Supervisor Not Found')

if __name__ == '__main__':
    our_employees = Employees()
    our_employees.import_gd_dump('test.csv')
    our_employees.tidy_data()
    our_employees.write_gd_formatted()

我想我在寻找(1)更好的方式来构建/存储员工信息,以及(2)在lookup_supervisor()方面遇到了一些问题。

我应该创建一个员工类,并把这些嵌套在员工集合里面吗?

我是否应该像现在这样使用tidy_data(),并在字典的条目上用for循环调用clean_phone_number()和lookup_supervisor()?唉,真让人困惑。

2 个回答

2

你可能需要进行一些循环操作来获取数据。我猜你不想要一个可能会过时的额外字典,所以把所有东西都用内部ID来存储可能不太划算。

试试这个方法:

def lookup_supervisor(manager_internal_id, employees):
    if manager_internal_id is not None and manager_internal_id != "":
        manager_dir_ids = [dir_id for dir_id in employees if employees[dir_id].get('internal_id') == manager_internal_id]
        assert(len(manager_dir_ids) <= 1)
        if len(manager_dir_ids) == 1:
            return manager_dir_ids[0]
    return None

def tidy_data(employees):
    for emp_data in employees.values():
        manager_dir_id = lookup_supervisor(emp_data.get('manager_internal_id'), employees)
        for (field, sup_key) in [('Email', 'mail'), ('FirstName', 'givenName'), ('Surname', 'sn')]:
            emp_data['Supervisor'+field] = (employees[manager_dir_id][sup_key] if manager_dir_id is not None else 'Supervisor Not Found')

你说得没错,使用类来传递employees是个好主意。其实,我建议不要把“主管”的键放在员工字典里,而是每次需要的时候都重新获取主管的数据,可能可以用一个get_supervisor_data的方法来实现。

你新的面向对象的版本看起来都很合理,除了我之前提到的那些改动,还有一些对clean_phone_number的调整。

def clean_phone_number(self, original_telephone_number):
    phone_re = re.compile(r'^\+(?P<intl_prefix>\d{2})\((?P<extra_zero>0?)(?P<area_code>\d)\)(?P<local_first_half>\d{4})(?P<hyph>-?)(?P<local_second_half>\d{4})')
    result = phone_re.search(original_telephone_number)
    if result is None:
        return '', "Number didn't match format. Original text is: " + original_telephone_number
    msg = ''
    if result.group('extra_zero'):
        msg += 'Extra zero in area code - ask user to remediate. '
    if result.group('hyph'):    # Note: can have both errors at once
        msg += 'Missing hyphen in local component - ask user to remediate. '
    return '0' + result.group('area_code') + result.group('local_first_half') + result.group('local_second_half'), msg

你当然可以为每个员工创建一个单独的对象,但根据你使用数据的方式和你需要的内容,我猜这样做的收益不会太大。

1

我的Python水平不太好,所以我不知道怎么把我想的东西写出来,花的时间也会很长。不过我知道怎么做面向对象的分解。

为什么Employees这个类要做所有的工作呢?其实,你的这个庞大的Employees类做了好几种事情:

  • 从文件中读取和写入数据,也就是把数据保存和加载。
  • 管理和访问每个员工的数据。
  • 管理员工之间的关系。

我建议你为每一类任务创建一个单独的类。

可以定义一个Employee类,用来记录员工的数据,并处理一些字段的整理工作。

然后用Employees类来装这些员工对象。这个类可以处理一些任务,比如找到某个员工的上司。

再定义一个虚拟基类EmployeeLoader,用来定义一个接口(比如加载、存储等)。然后为CSV文件的保存和加载实现一个子类。(这个虚拟基类是可选的——我不太确定Python是怎么处理虚拟类的,所以这可能不太适用。)

所以:

  • 创建一个EmployeeCSVLoader的实例,并指定一个文件名。
  • 这个加载器可以构建一个Employees对象,并解析文件。
  • 每读取一条记录,就创建一个新的员工对象,并把它存储在Employees对象里。
  • 接着让Employees对象来填充上司的链接。
  • 遍历Employees对象里的员工集合,让每个员工自己整理一下。
  • 最后,让序列化对象来更新数据文件。

为什么这个设计值得去做呢?

因为这样会让事情变得更容易理解。小而专注的对象更容易创建干净、一致的接口。

如果你发现需要一个XML格式的序列化,那就很简单,只需添加新的格式。为你的虚拟加载器类创建一个子类来处理XML的解析和生成。这样你就可以在CSV和XML格式之间无缝切换。

总之,使用对象来简化和结构化你的数据。把常见的数据和行为分成不同的类。保持每个类专注于一种能力。如果你的类是一个集合、访问器、工厂或者什么都装的,接口就永远无法使用:它会太大,方法也会杂乱无章。但如果你的类保持专注,就会很容易测试、维护、使用、重用和扩展。

撰写回答