Cater Waiter

Cater Waiter

Exercício de aprendizagem

Introdução

Um set é uma coleção mutável e não ordenada de objetos hashable. Os membros de um set têm de ser distintos: não são permitidos itens duplicados. Podem conter vários tipos de dados diferentes e até estruturas aninhadas, como um tuple de tuples, desde que todos os elementos possam ser hashed. Os sets também existem numa versão imutável: os frozensets.

Os sets são usados sobretudo para remover rapidamente duplicados de outras estruturas de dados ou agrupamentos de itens. Também são usados para comparações eficientes quando não é preciso manter sequências nem acompanhar duplicados.

Tal como outros tipos de coleção (dictionaries, lists, tuples), os sets suportam:

  • Iteração com for item in <set>
  • Verificação de pertença com in e not in,
  • Cálculo do comprimento com len(), e
  • Cópias superficiais com copy()

os sets não suportam:

  • Indexação de qualquer tipo
  • Ordenação através de ordenação ou inserção
  • Slicing
  • Concatenação com +

Verificar se um elemento pertence a um set tem complexidade temporal constante (em média), ao passo que verificar a pertença num list ou string tem uma complexidade temporal que cresce à medida que o comprimento dos dados aumenta. Métodos como <set>.union(), <set>.intersection() ou <set>.difference() também têm complexidade temporal constante (em média).

Literais de sets

Um set pode ser escrito diretamente como um literal de set com chavetas {} e vírgulas entre os elementos. Os duplicados são omitidos silenciosamente:

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

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

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

Os literais de set usam as mesmas chavetas que os literais de dict, o que significa que tens de usar set() para criar um set vazio.

O construtor de sets

set() (o construtor da classe set) pode ser usado com qualquer iterable passado como argumento. Os elementos do iterable são percorridos um a um e adicionados ao set individualmente. A ordem dos elementos não é preservada e os duplicados são omitidos silenciosamente:

# 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}

Armadilhas ao criar sets

Devido ao seu comportamento de «unpacking», usar set() com uma string pode ser surpreendente:

# 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('अभ्यास')
{'अ', 'भ', 'य', 'स', 'ा', '्'}

Os sets podem conter tipos de dados diferentes e tipos de dados aninhados, mas todos os elementos de um set têm de ser hashable:

# 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'

Trabalhar com sets

Os sets têm métodos que imitam, de um modo geral, as operações matemáticas com conjuntos. A maioria (não todos) destes métodos tem um equivalente na forma de operador. Os métodos aceitam geralmente qualquer iterable como argumento, ao passo que os operadores exigem que ambos os lados da operação sejam sets ou frozensets.

Sets disjuntos

O método <set>.isdisjoint(<other_collection>) serve para testar se os elementos de um set se sobrepõem de alguma forma aos elementos de outro set. O método aceita qualquer iterable ou set como argumento. Devolve True se os dois sets não tiverem elementos em comum, e False se houver elementos partilhados. Não existe equivalente na forma de operador:

# 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

Subconjuntos e superconjuntos

<set>.issubset(<other_collection>) serve para verificar se todos os elementos de <set> também estão em <other_collection>. A forma com operador é <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>) é o inverso de .issubset(). Serve para verificar se todos os elementos de <other_collection> também estão em <set>. A forma com operador é <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

Interseções de sets

<set>.intersection(*<other iterables>) devolve um novo set com os elementos comuns ao set original e a todos os <others> (por outras palavras, o set onde tudo se interseta). A versão com operador deste método é <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'}

Uniões de sets

<set>.union(*<other iterables>) devolve um novo set com os elementos de <set> e de todos os <other iterables>. A forma com operador deste método é <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'}

Diferenças de sets

<set>.difference(*<other iterables>) devolve um novo set com os elementos do <set> original que não estão em <others>. A versão com operador deste método é <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>) devolve um novo set que contém os elementos que estão em <set> OU em <other>, mas não em ambos. A versão com operador deste método é <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

Uma diferença simétrica de mais de dois sets resulta num set que inclui tanto os elementos únicos de cada set COMO os elementos partilhados entre mais de dois sets da série (mais detalhes no artigo da Wikipédia sobre diferença simétrica).

Para obter apenas os itens únicos de cada set da série, é preciso agregar as interseções entre todas as combinações de dois sets num passo separado e removê-las:

>>> 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'}

Instruções

Tu e os teus sócios gerem uma pequena empresa de catering. Acabaste de aceitar organizar um evento para um clube de culinária local, que se destaca pelos pratos "favoritos do clube". O clube não tem experiência a organizar eventos de grande dimensão e precisa de ajuda com a organização, as compras, a preparação e o serviço. Decidiste escrever alguns pequenos scripts em Python para acelerar todo o processo de planeamento.

1. Limpar os ingredientes dos pratos

As receitas do evento foram adicionadas a partir de várias fontes e os seus ingredientes parecem ter entradas duplicadas (ou mais). Não queres acabar por comprar artigos a mais! Antes de começar a fazer compras e a cozinhar, é preciso "limpar" a lista de ingredientes de cada prato.

Implementa a função clean_ingredients(<dish_name>, <dish_ingredients>), que recebe o nome de um prato e uma list de ingredientes. Esta função deve devolver um tuple com o nome do prato como primeiro elemento, seguido do set de ingredientes sem duplicados.

>>> 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. Cocktails e Mocktails

O evento vai incluir cocktails e "mocktails", bebidas mistas sem álcool. Tens de garantir que as bebidas "mocktail" são realmente sem álcool e que os cocktails incluem efetivamente álcool.

Implementa a função check_drinks(<drink_name>, <drink_ingredients>), que recebe o nome de uma bebida e uma list de ingredientes. A função deve devolver o nome da bebida seguido de "Mocktail" se a bebida não tiver ingredientes alcoólicos, e o nome da bebida seguido de "Cocktail" se a bebida incluir álcool. Para efeitos deste exercício, os cocktails só incluem álcoois da constante ALCOHOLS em sets_categories_data.py:

>>> 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. Categorizar os pratos

A lista de convidados inclui pessoas com diferentes necessidades alimentares, e o teu pessoal vai ter de separar os pratos em Vegan, Vegetarian, Paleo, Keto e Omnivore. Um prato pertence a uma categoria apenas se todos os seus ingredientes constarem do conjunto de ingredientes dessa categoria.

Implementa a função categorize_dish(<dish_name>, <dish_ingredients>), que recebe o nome de um prato e um set dos ingredientes desse prato. A função deve devolver uma string com o dish name: <CATEGORY> (a que categoria de refeição pertence o prato). Todos os pratos fornecidos "cabem" numa das categorias importadas de sets_categories_data.py (VEGAN, VEGETARIAN, PALEO, KETO ou 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. Etiquetar alergénios e alimentos restritos

Alguns convidados têm alergias e restrições alimentares adicionais. Estes ingredientes precisam de ser etiquetados/anotados para cada prato, para que não causem problemas.

Implementa a função tag_special_ingredients(<dish>), que recebe um tuple com o nome do prato na primeira posição e uma list ou set de ingredientes desse prato na segunda posição. Devolve o nome do prato seguido do set de ingredientes que exigem uma nota especial na descrição do prato. Os ingredientes de um prato dentro de uma list podem ou não ter duplicados. Para efeitos deste exercício, todos os alergénios ou ingredientes especiais que precisam de ser etiquetados estão na constante SPECIAL_INGREDIENTS importada de sets_categories_data.py.

>>> 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. Compilar uma "lista principal" de ingredientes

Para preparar a encomenda e as compras, vais precisar de compilar uma "lista principal" de ingredientes para tudo o que está no menu (quantidades a preencher mais tarde).

Implementa a função compile_ingredients(<dishes>), que recebe uma list de pratos e devolve um conjunto de todos os ingredientes de todos os pratos listados. Cada prato individual é representado pelo seu set de ingredientes.

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. Retirar os aperitivos para servir em tabuleiros

Os anfitriões deram-te uma lista de pratos que gostariam de preparar como aperitivos "de uma só dentada" para servir em tabuleiros. Tens de os retirar da lista principal de pratos a preparar em doses maiores.

Implementa a função separate_appetizers(<dishes>, <appetizers>), que recebe uma list de nomes de pratos e uma list de nomes de aperitivos. A função deve devolver a list de nomes de pratos sem os nomes dos aperitivos. Tanto a <dishes> como a <appetizers> podem conter duplicados e podem precisar de ser deduplicadas.

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. Encontrar ingredientes usados em apenas uma receita

Dentro de cada categoria (Vegan, Vegetarian, Paleo, Keto e Omnivore), vais retirar os ingredientes que aparecem em apenas um prato. Estes ingredientes "singleton" serão atribuídos a um comprador especial, para garantir que não são esquecidos na pressa de fazer tudo o resto.

Implementa a função singleton_ingredients(<dishes>, <INTERSECTIONS>), que recebe uma list de pratos e uma constante <CATEGORY>_INTERSECTIONS da mesma categoria. Cada prato é representado por um set dos seus ingredientes. Cada <CATEGORY>_INTERSECTIONS é um set de ingredientes que aparecem em mais de um prato da categoria. Recorrendo a operações de conjuntos, a tua função deve devolver um set de ingredientes "singleton" (ingredientes que aparecem em apenas um prato da categoria).

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'}
Editar via GitHub A ligação abre numa nova janela ou separador
Python Exercism

Estás pronto para começar Cater Waiter?

Inscreve-te no Exercism para aprenderes e dominares Python com 17 conceitos146 exercícios, e mentoria humana real, tudo grátis.