學習軌道
/
Python
Python
/
練習
/
外燴服務生
外燴服務生

外燴服務生

學習練習

簡介

集合 是一種 可變 且 無序 的群組,用來存放 可雜湊 的物件。 集合的成員必須互不相同,不允許有重複的項目。 它們可以存放多種不同的資料型態,甚至可以存放像由 tuples 組成的 tuple 這類巢狀結構,只要其中所有元素都能被_雜湊_即可。 集合還有不可變的 frozensets 版本。

集合最常被用來快速移除其他資料結構或項目分組中的重複項。 當不需要排序與追蹤重複項時,集合也很適合用來進行高效率的比較。

和其他集合型別(字典、陣列、元組)一樣,set 支援:

  • 透過 for item in <set> 進行疊代
  • 透過 in 和 not in 檢查成員資格,
  • 透過 len() 計算長度,以及
  • 透過 copy() 建立淺拷貝

sets 不支援:

  • 任何形式的索引
  • 透過排序或插入來決定順序
  • 切片
  • 透過 + 串接

檢查 set 中是否包含某個元素,平均而言具有常數時間複雜度;相對地,檢查 list 或 string 中的成員時,時間複雜度會隨著資料長度增加而成長。 像 <set>.union()、<set>.intersection() 或 <set>.difference() 這些方法,平均而言也具有常數時間複雜度。

集合字面值

set 可以直接以 集合字面值 的形式建立,使用大括號{},並以逗號分隔各元素。 重複的項目會被默默省略:

>>> one_element = {'➕'}
{'➕'}

>>> multiple_elements = {'➕', '🔻', '🔹', '🔆'}
{'➕', '🔻', '🔹', '🔆'}

>>> multiple_duplicates =  {'Hello!', 'Hello!', 'Hello!', 
                            '¡Hola!','Привіт!', 'こんにちは!', 
                            '¡Hola!','Привіт!', 'こんにちは!'}
{'こんにちは!', '¡Hola!', 'Hello!', 'Привіт!'}

集合字面值使用與 dict 字面值相同的大括號,因此你必須使用 set() 才能建立空的 set。

集合建構子

set()(set 類別的建構子)可以接受任何 iterable 作為引數。 iterable 的元素會被逐一走訪,並個別加入 set 中。 元素的順序不會被保留,重複的項目則會被默默省略:

# To create an empty set, the constructor must be used.
>>> no_elements = set()
set()

# The tuple is unpacked & each element is added.  
# Duplicates are removed.
>>> elements_from_tuple = set(("Parrot", "Bird", 
                               334782, "Bird", "Parrot"))
{334782, 'Bird', 'Parrot'}

# The list is unpacked & each element is added.
# Duplicates are removed.
>>> elements_from_list = set([2, 3, 2, 3, 3, 3, 5, 
                              7, 11, 7, 11, 13, 13])
{2, 3, 5, 7, 11, 13}

建立集合時的陷阱

因為它具有「展開」的行為,把字串傳給 set() 可能會出乎意料:

# String elements (Unicode code points) are 
# iterated through and added *individually*.
>>> elements_string = set("Timbuktu")
{'T', 'b', 'i', 'k', 'm', 't', 'u'}

# Unicode separators and positioning code points 
# are also added *individually*.
>>> multiple_code_points_string = set('अभ्यास')
{'अ', 'भ', 'य', 'स', 'ा', '्'}

集合可以存放不同的資料型態與_巢狀_資料型態,但所有 set 元素都必須是_可雜湊_的:

# Attempting to use a list for a set member throws a TypeError
>>> lists_as_elements = {['🌈','💦'], 
                        ['☁️','⭐️','🌍'], 
                        ['⛵️', '🚲', '🚀']}

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'


# Standard sets are mutable, so they cannot be hashed.
>>> sets_as_elements = {{'🌈','💦'}, 
                        {'☁️','⭐️','🌍'}, 
                        {'⛵️', '🚲', '🚀'}}

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'set'

操作集合

集合有一些方法,大致上模仿了數學集合運算。 這些方法大多(但並非全部)都有對應的運算子。 方法通常可以接受任何 iterable 作為引數,而運算子則要求運算的兩邊都是 sets 或 frozensets。

不相交集合

<set>.isdisjoint(<other_collection>) 方法用來測試某個集合的元素是否與另一個集合的元素有任何重疊。 這個方法可以接受任何 iterable 或 set 作為引數。 如果兩個集合沒有共同元素,它會回傳 True;如果元素有共用,則回傳 False。 它沒有對應的運算子:

# Both mammals and additional_animals are lists.
>>> mammals = ['squirrel','dog','cat','cow', 'tiger', 'elephant']
>>> additional_animals = ['pangolin', 'panda', 'parrot', 
                          'lemur', 'tiger', 'pangolin']

# Animals is a dict.
>>> animals = {'chicken': 'white',
               'sparrow': 'grey',
               'eagle': 'brown and white',
               'albatross': 'grey and white',
               'crow': 'black',
               'elephant': 'grey', 
               'dog': 'rust',
               'cow': 'black and white',
               'tiger': 'orange and black',
               'cat': 'grey',
               'squirrel': 'black'}
               
# Birds is a set.
>>> birds = {'crow','sparrow','eagle','chicken', 'albatross'}

# Mammals and birds don't share any elements.
>>> birds.isdisjoint(mammals)
True

# There are also no shared elements between 
# additional_animals and birds.
>>> birds.isdisjoint(additional_animals)
True

# Animals and mammals have shared elements.
# **Note** The first object needs to be a set or converted to a set
# since .isdisjoint() is a set method.
>>> set(animals).isdisjoint(mammals)
False

子集合與超集合

<set>.issubset(<other_collection>) 用來檢查 <set> 中的每個元素是否也都在 <other_collection> 中。 運算子的形式是 <set> <= <other_set>:

# Both mammals and additional_animals are lists.
>>> mammals = ['squirrel','dog','cat','cow', 'tiger', 'elephant']
>>> additional_animals = ['pangolin', 'panda', 'parrot', 
                          'lemur', 'tiger', 'pangolin']

# Animals is a dict.
>>> animals = {'chicken': 'white',
               'sparrow': 'grey',
               'eagle': 'brown and white',
               'albatross': 'grey and white',
               'crow': 'black',
               'elephant': 'grey', 
               'dog': 'rust',
               'cow': 'black and white',
               'tiger': 'orange and black',
               'cat': 'grey',
               'squirrel': 'black'}

# Birds is a set.
>>> birds = {'crow','sparrow','eagle','chicken', 'albatross'}

# Set methods will take any iterable as an argument.
# All members of birds are also members of animals.
>>> birds.issubset(animals)
True

# All members of mammals also appear in animals.
# **Note** The first object needs to be a set or converted to a set
# since .issubset() is a set method.
>>> set(mammals).issubset(animals)
True

# Both objects need to be sets to use a set operator
>>> birds <= set(mammals)
False

# A set is always a loose subset of itself.
>>> set(additional_animals) <= set(additional_animals)
True

<set>.issuperset(<other_collection>) 是 .issubset() 的反操作。 它用來檢查 <other_collection> 中的每個元素是否也都在 <set> 中。 運算子的形式是 <set> >= <other_set>:

# All members of mammals also appear in animals.
# **Note** The first object needs to be a set or converted to a set
# since .issuperset() is a set method.
>>> set(animals).issuperset(mammals)
True

# All members of animals do not show up as members of birds.
>>> birds.issuperset(animals)
False

# Both objects need to be sets to use a set operator
>>> birds >= set(mammals)
False

# A set is always a loose superset of itself.
>>> set(animals) >= set(animals)
True

集合交集

<set>.intersection(*<other iterables>) 會回傳一個新的 set,其中包含原始 set 與所有 <others> 共有的元素(換句話說,就是所有元素都相交的那個 set)。 這個方法的運算子版本是 <set> & <other set> & <other set 2> & ... <other set n>:

>>> perennials = {'Annatto','Asafetida','Asparagus','Azalea',
                 'Winter Savory', 'Broccoli','Curry Leaf','Fennel', 
                 'Kaffir Lime','Kale','Lavender','Mint','Oranges',
                 'Oregano', 'Tarragon', 'Wild Bergamot'}

>>> annuals = {'Corn', 'Zucchini', 'Sweet Peas', 'Marjoram', 
              'Summer Squash', 'Okra','Shallots', 'Basil', 
              'Cilantro', 'Cumin', 'Sunflower', 'Chervil', 
              'Summer Savory'}

>>> herbs = ['Annatto','Asafetida','Basil','Chervil','Cilantro',
            'Curry Leaf','Fennel','Kaffir Lime','Lavender',
            'Marjoram','Mint','Oregano','Summer Savory', 
            'Tarragon','Wild Bergamot','Wild Celery',
            'Winter Savory']


# Methods will take any iterable as an argument.
>>> perennial_herbs = perennials.intersection(herbs)
{'Annatto', 'Asafetida', 'Curry Leaf', 'Fennel', 'Kaffir Lime',
 'Lavender', 'Mint', 'Oregano', 'Wild Bergamot','Winter Savory'}

# Operators require both groups be sets.
>>> annuals & set(herbs)
 {'Basil', 'Chervil', 'Marjoram', 'Cilantro'}

集合聯集

<set>.union(*<other iterables>) 會回傳一個新的 set,其中包含來自 <set> 以及所有 <other iterables> 的元素。 這個方法的運算子形式是 <set> | <other set 1> | <other set 2> | ... | <other set n>:

>>> perennials = {'Asparagus', 'Broccoli', 'Sweet Potato', 'Kale'}
>>> annuals = {'Corn', 'Zucchini', 'Sweet Peas', 'Summer Squash'}
>>> more_perennials = ['Radicchio', 'Rhubarb', 
                      'Spinach', 'Watercress']

# Methods will take any iterable as an argument.
>>> perennials.union(more_perennials)
{'Asparagus','Broccoli','Kale','Radicchio','Rhubarb',
'Spinach','Sweet Potato','Watercress'}

# Operators require sets.
>>> set(more_perennials) | perennials
{'Asparagus',
 'Broccoli',
 'Kale',
 'Radicchio',
 'Rhubarb',
 'Spinach',
 'Sweet Potato',
 'Watercress'}

集合差集

<set>.difference(*<other iterables>) 會回傳一個新的 set,其中包含原始 <set> 中不在 <others> 裡的元素。 這個方法的運算子版本是 <set> - <other set 1> - <other set 2> - ...<other set n>。

>>> berries_and_veggies = {'Asparagus', 
                          'Broccoli', 
                          'Watercress', 
                          'Goji Berries', 
                          'Goose Berries', 
                          'Ramps', 
                          'Walking Onions', 
                          'Blackberries', 
                          'Strawberries', 
                          'Rhubarb', 
                          'Kale', 
                          'Artichokes', 
                          'Currants'}

>>> veggies = ('Asparagus', 'Broccoli', 'Watercress', 'Ramps',
               'Walking Onions', 'Rhubarb', 'Kale', 'Artichokes')

# Methods will take any iterable as an argument.
>>> berries = berries_and_veggies.difference(veggies)
{'Blackberries','Currants','Goji Berries',
 'Goose Berries', 'Strawberries'}

# Operators require sets.
>>> berries_and_veggies - berries
{'Artichokes','Asparagus','Broccoli','Kale',
'Ramps','Rhubarb','Walking Onions','Watercress'}

<set>.symmetric_difference(<other iterable>) 會回傳一個新的 set,其中包含在 <set> 或 <other> 中、但不同時存在於兩者的元素。 這個方法的運算子版本是 <set> ^ <other set>:

>>> plants_1 = {'🌲','🍈','🌵', '🥑','🌴', '🥭'}
>>> plants_2 = ('🌸','🌴', '🌺', '🌲', '🌻', '🌵')


# Methods will take any iterable as an argument.
>>> fruit_and_flowers = plants_1.symmetric_difference(plants_2)
>>> fruit_and_flowers
{'🌸', '🌺', '🍈', '🥑', '🥭','🌻' }


# Operators require both groups be sets.
>>> fruit_and_flowers ^ plants_1
{'🌲',  '🌸', '🌴', '🌵','🌺', '🌻'}

>>> fruit_and_flowers ^ set(plants_2)
{'🥭', '🌴', '🌵', '🍈', '🌲', '🥑'}
Note

超過兩個集合的對稱差集,會產生一個 set,其中同時包含每個 set 各自獨有的元素,以及序列中超過兩個集合之間共用的元素(詳情請見維基百科上關於對稱差集的文章)。

若只想取得序列中每個 set 獨有的項目,則需要在另一個步驟中彙整所有兩兩集合組合之間的交集,並將其移除:

>>> one = {'black pepper','breadcrumbs','celeriac','chickpea flour',
           'flour','lemon','parsley','salt','soy sauce',
           'sunflower oil','water'}

>>> two = {'black pepper','cornstarch','garlic','ginger',
           'lemon juice','lemon zest','salt','soy sauce','sugar',
           'tofu','vegetable oil','vegetable stock','water'}

>>> three = {'black pepper','garlic','lemon juice','mixed herbs',
             'nutritional yeast', 'olive oil','salt','silken tofu',
             'smoked tofu','soy sauce','spaghetti','turmeric'}

>>> four = {'barley malt','bell pepper','cashews','flour',
            'fresh basil','garlic','garlic powder', 'honey',
            'mushrooms','nutritional yeast','olive oil','oregano',
            'red onion', 'red pepper flakes','rosemary','salt',
            'sugar','tomatoes','water','yeast'}

>>> intersections = (one & two | one & three | one & four | 
                     two & three | two & four | three & four)
...
{'black pepper','flour','garlic','lemon juice','nutritional yeast', 
'olive oil','salt','soy sauce', 'sugar','water'}

# The ^ operation will include some of the items in intersections, 
# which means it is not a "clean" symmetric difference - there
# are overlapping members.
>>> (one ^ two ^ three ^ four) & intersections
{'black pepper', 'garlic', 'soy sauce', 'water'}

# Overlapping members need to be removed in a separate step
# when there are more than two sets that need symmetric difference.
>>> (one ^ two ^ three ^ four) - intersections
...
{'barley malt','bell pepper','breadcrumbs', 'cashews','celeriac',
  'chickpea flour','cornstarch','fresh basil', 'garlic powder',
  'ginger','honey','lemon','lemon zest','mixed herbs','mushrooms',
  'oregano','parsley','red onion','red pepper flakes','rosemary',
  'silken tofu','smoked tofu','spaghetti','sunflower oil', 'tofu', 
  'tomatoes','turmeric','vegetable oil','vegetable stock','yeast'}

說明

你和你的生意夥伴一起經營一家小型外燴公司。你們剛接下一個案子,要為當地的烹飪社團舉辦活動,菜色主打「社團最愛」。這個社團沒有舉辦大型活動的經驗,需要有人幫忙籌劃、採買、備料和上菜。於是你決定寫幾支小小的 Python 腳本,加快整個籌備流程。

1. 清理菜餚的食材

活動的食譜來自四面八方,裡面的食材似乎有重複(甚至更多)的項目,你總不希望最後買了一堆多餘的東西吧! 在開始採買和下廚之前,每道菜的食材清單都得先「整理乾淨」。

實作clean_ingredients(<dish_name>, <dish_ingredients>)函式,它會接收菜餚名稱和一串食材的list。 這個函式應回傳一個tuple,第一項是菜餚名稱,接著是去重後的食材set。

>>> clean_ingredients('Punjabi-Style Chole', ['onions', 'tomatoes', 'ginger paste', 'garlic paste', 'ginger paste', 'vegetable oil', 'bay leaves', 'cloves', 'cardamom', 'cilantro', 'peppercorns', 'cumin powder', 'chickpeas', 'coriander powder', 'red chili powder', 'ground turmeric', 'garam masala', 'chickpeas', 'ginger', 'cilantro'])

>>> ('Punjabi-Style Chole', {'garam masala', 'bay leaves', 'ground turmeric', 'ginger', 'garlic paste', 'peppercorns', 'ginger paste', 'red chili powder', 'cardamom', 'chickpeas', 'cumin powder', 'vegetable oil', 'tomatoes', 'coriander powder', 'onions', 'cilantro', 'cloves'})

2. 雞尾酒與無酒精調酒

活動上會有雞尾酒,也會有「mocktails」,也就是不含酒精的調酒。 你得確保「無酒精調酒」真的不含酒精,而雞尾酒確實含有酒精。

實作check_drinks(<drink_name>, <drink_ingredients>)函式,它會接收飲料名稱和一串食材的list。 若飲料不含酒精成分,函式應回傳飲料名稱加上「Mocktail」;若飲料含有酒精,則回傳飲料名稱加上「Cocktail」。 就本練習而言,雞尾酒的酒精只會來自sets_categories_data.py中的 ALCOHOLS 常數:

>>> from sets_categories_data import ALCOHOLS 

>>> check_drinks('Honeydew Cucumber', ['honeydew', 'coconut water', 'mint leaves', 'lime juice', 'salt', 'english cucumber'])
...
'Honeydew Cucumber Mocktail'

>>> check_drinks('Shirley Tonic', ['cinnamon stick', 'scotch', 'whole cloves', 'ginger', 'pomegranate juice', 'sugar', 'club soda'])
...
'Shirley Tonic Cocktail'

3. 將菜餚分類

賓客名單裡有各種不同飲食需求的客人,你的員工得把菜餚分成 Vegan、Vegetarian、Paleo、Keto 和 Omnivore 幾類。 一道菜只有在所有食材都出現在該類別的食材集合中時,才屬於那個類別。

實作categorize_dish(<dish_name>, <dish_ingredients>)函式,它會接收菜餚名稱和該菜餚食材的set。 函式應回傳一個字串,內容為dish name: <CATEGORY>(這道菜屬於哪個餐點類別)。 所有提供的菜餚都會「歸入」從sets_categories_data.py匯入的其中一個類別(VEGAN、VEGETARIAN、PALEO、KETO 或 OMNIVORE)。

>>> from sets_categories_data import VEGAN, VEGETARIAN, PALEO, KETO, OMNIVORE


>>> categorize_dish('Sticky Lemon Tofu', {'tofu', 'soy sauce', 'salt', 'black pepper', 'cornstarch', 'vegetable oil', 'garlic', 'ginger', 'water', 'vegetable stock', 'lemon juice', 'lemon zest', 'sugar'})
...
'Sticky Lemon Tofu: VEGAN'

>>> categorize_dish('Shrimp Bacon and Crispy Chickpea Tacos with Salsa de Guacamole', {'shrimp', 'bacon', 'avocado', 'chickpeas', 'fresh tortillas', 'sea salt', 'guajillo chile', 'slivered almonds', 'olive oil', 'butter', 'black pepper', 'garlic', 'onion'})
...
'Shrimp Bacon and Crispy Chickpea Tacos with Salsa de Guacamole: OMNIVORE'

4. 標示過敏原與限制飲食的食材

有些賓客有過敏和額外的飲食限制。 這些食材必須在每道菜上標記/註解,以免出問題。

實作tag_special_ingredients(<dish>)函式,它會接收一個tuple,第一項是菜餚名稱,第二項是該菜餚食材的list或set。 回傳菜餚名稱,接著是需要在菜餚說明上加註的食材set。 list中的菜餚食材可能有重複,也可能沒有。 就本練習而言,所有需要標示的過敏原或特殊食材,都在從sets_categories_data.py匯入的 SPECIAL_INGREDIENTS 常數裡。

>>> from sets_categories_data import SPECIAL_INGREDIENTS

>>> tag_special_ingredients(('Ginger Glazed Tofu Cutlets', ['tofu', 'soy sauce', 'ginger', 'corn starch', 'garlic', 'brown sugar', 'sesame seeds', 'lemon juice']))
...
('Ginger Glazed Tofu Cutlets', {'garlic','soy sauce','tofu'})

>>> tag_special_ingredients(('Arugula and Roasted Pork Salad', ['pork tenderloin', 'arugula', 'pears', 'blue cheese', 'pine nuts', 'balsamic vinegar', 'onions', 'black pepper']))
...
('Arugula and Roasted Pork Salad', {'pork tenderloin', 'blue cheese', 'pine nuts', 'onions'})

5. 彙整食材「總清單」

為了準備訂購和採買,你得把菜單上所有項目的食材彙整成一份「總清單」(數量之後再填)。

實作compile_ingredients(<dishes>)函式,它會接收菜餚的list,並回傳所有列出菜餚中全部食材的集合。 每道菜都以自己的食材set表示。

dishes = [ {'tofu', 'soy sauce', 'ginger', 'corn starch', 'garlic', 'brown sugar', 'sesame seeds', 'lemon juice'},
           {'pork tenderloin', 'arugula', 'pears', 'blue cheese', 'pine nuts',
           'balsamic vinegar', 'onions', 'black pepper'},
           {'honeydew', 'coconut water', 'mint leaves', 'lime juice', 'salt', 'english cucumber'}]

>>> compile_ingredients(dishes)
...
{'arugula', 'brown sugar', 'honeydew', 'coconut water', 'english cucumber', 'balsamic vinegar', 'mint leaves', 'pears', 'pork tenderloin', 'ginger', 'blue cheese', 'soy sauce', 'sesame seeds', 'black pepper', 'garlic', 'lime juice', 'corn starch', 'pine nuts', 'lemon juice', 'onions', 'salt', 'tofu'}

6. 挑出要放在托盤上端出的開胃菜

主辦方給了你一份菜單,希望把這些菜做成一口大小的開胃菜,放在托盤上端出。 你得把這些菜色從準備做成大份量的主菜清單中挑出來。

實作separate_appetizers(<dishes>, <appetizers>)函式,它會接收菜餚名稱的list和開胃菜名稱的list。 函式應回傳移除開胃菜名稱後的菜餚名稱list。 <dishes>或<appetizers>的list都可能含有重複項目,需要去重。

dishes =    ['Avocado Deviled Eggs','Flank Steak with Chimichurri and Asparagus', 'Kingfish Lettuce Cups',
             'Grilled Flank Steak with Caesar Salad','Vegetarian Khoresh Bademjan','Avocado Deviled Eggs',
             'Barley Risotto','Kingfish Lettuce Cups']
          
appetizers = ['Kingfish Lettuce Cups','Avocado Deviled Eggs','Satay Steak Skewers',
              'Dahi Puri with Black Chickpeas','Avocado Deviled Eggs','Asparagus Puffs',
              'Asparagus Puffs']
              
>>> separate_appetizers(dishes, appetizers)
...
['Vegetarian Khoresh Bademjan', 'Barley Risotto', 'Flank Steak with Chimichurri and Asparagus', 
 'Grilled Flank Steak with Caesar Salad']

7. 找出只用在一道食譜裡的食材

在每個類別(Vegan、Vegetarian、Paleo、Keto、Omnivore)中,你要挑出只出現在一道菜裡的食材。 這些「單例」食材會指派給專門的採買人員,確保在忙著搞定其他事情時不會漏掉它們。

實作singleton_ingredients(<dishes>, <INTERSECTIONS>)函式,它會接收菜餚的list,以及同一類別的<CATEGORY>_INTERSECTIONS常數。 每道菜都以自己的食材set表示。 每個<CATEGORY>_INTERSECTIONS都是該類別中出現在一道以上菜餚裡的食材set。 運用集合運算,你的函式應回傳「單例」食材的set(只出現在該類別中一道菜裡的食材)。

from sets_categories_data import example_dishes, EXAMPLE_INTERSECTION

>>> singleton_ingredients(example_dishes, EXAMPLE_INTERSECTION)
...
{'garlic powder', 'sunflower oil', 'mixed herbs', 'cornstarch', 'celeriac', 'honey', 'mushrooms', 'bell pepper', 'rosemary', 'parsley', 'lemon', 'yeast', 'vegetable oil', 'vegetable stock', 'silken tofu', 'tofu', 'cashews', 'lemon zest', 'smoked tofu', 'spaghetti', 'ginger', 'breadcrumbs', 'tomatoes', 'barley malt', 'red pepper flakes', 'oregano', 'red onion', 'fresh basil'}
透過 GitHub 編輯 連結會在新視窗或分頁中開啟
Python Exercism

準備好開始 外燴服務生 了嗎?

註冊 Exercism,透過 17 個概念146 個練習 和真人引導來學習並精通 Python,全部免費。