One place for hosting & domains

      Function

      Understanding Cryptography’s Meaning and Function


      Cryptography is a cornerstone of modern secure communication practices. From digital signatures to disk encryption, these everyday applications of cryptography enable users of the Internet, developers, and business to keep sensitive data private. This guide provides an overview of what cryptography is, a brief history of cryptography, and the differences between symmetric asymmetric encryption.

      What Is Cryptography?

      The discipline of cryptography includes the study and practice of transforming data from its original format into an unintelligible format. The goal of cryptography is to keep information secure at rest and during its transfer. In the context of computer science, cryptography focuses on the mathematical concepts and algorithms that keep communications hidden from unauthorized viewers. There are three basic types of cryptographic algorithms that are used: secret key, public key, and hash function algorithms. Data encryption applies the principles of cryptography and refers to the method used to encode data into an unintelligible format.

      Cryptography enables cybersecurity professionals to
      secure sensitive company information
      . Well-known examples of cryptographic techniques used in cybersecurity are digital signatures, time stamping, the SSL protocol, and
      public key authentication with secure shell (SSH)
      .

      History of Cryptography

      While the use of cryptography in network communications began with the advent of computers, the origins of cryptography extends much further back into history. The earliest known use to date is in an inscription that belonged to a nobleman’s tomb in Egypt in 1900 B.C. The inscriber inserted unusual symbols in place of more common hieroglyphic symbols to transform the inscription. It is widely theorized that this behavior was not intended to hide the inscription, but to make it appear more dignified and educated. However, the original text was transformed much in the same way that cryptography seeks to transform text to keep its original meaning secret.

      Early uses of cryptography intended to hide a message date back to numerous early civilizations. Keeping information private has been a consistent need for human societies. One early stage example,
      Arthashastra
      , is a classic on statecraft written circa 350-275 BCE. It includes mentions of India’s early espionage service and the “secret writings” used to communicate with spies. Julius Caesar was known to use cryptography to communicate with his army generals in 100 BC, as did numerous other leaders with armies and wars to fight.

      According to Britannica, there
      are three distinct stages in the development of cryptography
      over time. The first is manual cryptography, the second is mechanized cryptography, and the third is digital cryptography.

      The first cipher requiring a decryption key was developed in the 16th century. It is known as the
      Vigenere cipher
      which is described as “a poly-alphabetic substitution system that uses a key and a double-entry table.”

      An example of the second stage, that is the mechanization of cryptography, is the
      Hebern rotor machine
      which was developed after electricity became available in the 18th century. It embedded a secret key on a rotating disk. Another example is the famous
      Enigma machine
      which was invented at the end of World War II. It used multiple rotors that rotated at different rates while the user typed. The key was the initial setting of the rotors.

      Cryptography was used almost exclusively for military purposes for most of its history. That changed substantially in the early 1970s when IBM customers demanded additional security when using computers. For this reason, IBM developed a cipher called
      Lucifer
      .

      As computer usage increased within government agencies, the demand for less militarized applications of cryptography increased. This began the era of digital cryptography which sought to counter the growing cybersecurity attacks. In 1973, the U.S. National Bureau of Standards (NIST) sought a block cipher to become the national standard. Lucifer was accepted and dubbed the
      Data Encryption Standard (DES)
      . However, it failed to withstand intensifying brute force attacks as computing and cyber attacks became more powerful. In response, NIST solicited a new block cipher in 1997 and received 50 submissions of possible contenders. NIST chose Rijndael in 2000 and renamed it the
      Advanced Encryption Standard (AES)
      .

      Although encryption standards exist today, cryptography continues to evolve. The cryptography of the present is anchored to computer science algorithms and mathematics, like number theory.

      Symmetric vs. Asymmetric Cryptography

      The two main forms of encryption utilized by cryptography are symmetric and asymmetric. Symmetric cryptography encrypts and decrypts with a single key. Asymmetric cryptography uses two linked keys, one public and the other private.

      Both forms of encryption are used everyday, although most computer users typically don’t notice them. They’re at work in the background every time someone uses their web browser, answers emails, submits a web form, as well as other activities.

      People tend to notice cryptography when they initiate its use or directly observe it in use. One example is when using OpenSSL key management services. Another example is when emailing an encrypted document, like an Adobe PDF file that requires a password in order for it to be opened.

      Symmetric encryption is the most widely used and the oldest form of encryption. It dates back to Julius Caesar’s cipher. Symmetric encryption uses either
      stream
      or
      block cipher
      to encrypt plain text data.

      While symmetric encryption requires the sender and recipient to use the same key, that key’s use is not limited to two people in a linear conversation. Others can also be designated recipients and use the same key. Likewise, any of the recipients can respond to the sender, plus anyone on the approved list of recipients using the same key from the initial encrypted message.

      Thus, if an unauthorized person were to gain the symmetric key, that person could see, read, copy, forward the message to new recipients, and even respond to the original group. Hackers gain access to the key either by pilfering it from a storage space on a device that hasn’t been properly secured, or by extracting it from the message itself.

      The key must be transmitted when the sender and receiver are not in the same location. It is therefore vulnerable if the network or channel are compromised and must be closely protected.

      By comparison, asymmetric cryptography uses two linked keys, one public and the other private, on each side of the conversation or transaction. Both sender and receiver have a private key in their possession alone. Each also has a public key – meaning a unique key of their own made public only by virtue of being exchanged with another person. The sender uses the recipient’s public key to encrypt the file. The recipient then uses their private key to decrypt it. Only the recipient can decrypt the file because no one else has access to that person’s private key. Asymmetric encryption also enables digital signature authentication.

      Examples of asymmetric cryptography in everyday use include
      RSA
      , the
      Digital Signature Standard (DSS/DSA)
      , and the
      TLS/SSL protocol
      .

      Both forms are considered secure, but the level of security in any given encrypted message has more to do with the size of the key(s) than the form of encryption. Just like passwords, keys must be complex, difficult to obtain, decode, or reveal.

      The Objectives of Cryptography

      Cryptography has four major goals: confidentiality, integrity, authentication, and non-repudiation. Put another way, the goals are data privacy (confidential treatment), data authenticity (verified source), and data integrity (original and unaltered message). Non-repudiation refers to the combination of each of these three things to prove undeniable validity of the message or data. One example of non-repudiation in use is a service used to authenticate digital signatures and to ensure that a person cannot reasonably deny having signed a document. Some popular examples are
      DocuSign
      and
      PandaDoc
      .

      Of these goals, confidentiality carries the most weight. The need to ensure that an unauthorized party cannot access the data is the ultimate objective of cryptography. That does not mean that the remaining goals are of less importance.

      Data integrity is vital to ensure that the message has not been altered in some way. Otherwise, the receiving party could be manipulated into taking a wrong or undesirable action. Whether a spy is sending a message to their country’s leadership, or a company is sending instructions to a field office, both sender and receiver need assurance that the message sent is identical to the message received.

      Authenticity is essential to ensure that the user or system is known and trusted. Establishing the identity of the user (sender or recipient) is the crux of this assurance. However, the system must also be known in order to
      prevent ransomware attacks
      that involve phishing (fraudulent emails), vishing (fraudulent voice mails and phone calls), smishing (fraudulent texts), and other deceptive forms of communication.

      Types of Cryptography

      There are three types of cryptography: secret key cryptography, public key cryptography, and hash functions.

      The least complicated and fastest to use is secret key cryptography, also known as symmetric cryptography. This type uses one key to encrypt and decrypt communications. It protects data at rest and data in transit, and is most often used on data at rest. The most well-known algorithms used in secret key cryptography are
      Advanced Encryption Standard (AES)
      ,
      Triple Data Encryption Standard (3DES)
      , and
      Rivest Cipher 4 (RC4)
      .

      Public key cryptography, or asymmetric cryptography, uses two keys on each end of the communication. Each pair consists of a public and a private key. Public keys are exchanged between sender and recipient. The sender then uses the recipient’s public key to encrypt the message. The recipient uses their private key to decrypt the message. Examples of public key use are plentiful in just about any communication over the Internet such as
      HTTPS
      ,
      SSH
      ,
      OpenPGP
      ,
      S/MIME
      , and a
      website’s SSL/TLS certificate
      .

      The math connecting public and private keys makes it impossible to derive the private key from the public key. However, the public key is derived from the private key, which is why private keys should never be shared.

      Hash functions are one-way functions and completely irreversible. This renders the original message unrecoverable. A hashing algorithm produces unique outputs for each input. Examples include
      SHA-256 and SHA3-256
      , both of which change any input into a new and complex 256-bit output. Bitcoin, the largest and best known of the cryptocurrencies, uses SHA-256 cryptographic hash function in its algorithm. Almost all passwords are stored securely as hashed functions which are then used to verify the correct password is being used. A hacker must try every input possible to find the exact same hash, which renders the effort useless.

      What is Cryptography in Cyber Security?

      Modern cryptography is based on mathematical theory and computer science. It continues to evolve as computing becomes more powerful. For example,
      quantum computers will break today’s encryption
      standards in the foreseeable future. Computer scientists are already hard at work developing quantum-safe algorithms and security protocols. Whatever the solutions turn out to be, they’ll be built based on the laws of physics and the rules of mathematics.

      Both now and in the future, cryptography is central to cybersecurity efforts. Whether it is protecting data points and documents across communication channels, or large data sets in transit or at rest in storage and on devices; cryptography is the first line of defense. Nothing is fool-proof, and therefore all things in cybersecurity, including cryptography, must evolve to match increasingly sophisticated threats and evermore powerful computers.

      To understand the necessity of encryption, one need only to look at the headlines. The frequency of data breaches and intercepted or leaked messages is readily apparent. In February 2022 alone, more than 5.1 million records were breached, according to research by
      IT Governance
      .

      The central assumption with cryptography is that other parties are going to try to breach data and many are going to be successful. Encryption is meant to thwart their efforts even if they succeed in reaching the data. It is an essential line of defense in cybersecurity architecture and hinders an attacker’s efforts to access sensitive information.

      Other forms of cybersecurity focus on other fronts such as protecting the network, limiting or stopping access to data, and protecting data from manipulation, i.e. deliberate corruption of meaning or readability.

      Layers of different cybersecurity methods
      work in tandem to provide a better, stronger defense. Even so, encrypting data is a primary defense used across all efforts in protecting data. Its use is of particular value to secure communications which by necessity must be shared with parties beyond secure company walls.

      Conclusion

      Cybersecurity and encryption are tasks that require research, time, and effort in order to be effective. Many companies prefer to leverage the efforts of vendor teams rather than overburden their internal cybersecurity teams to develop these additional layers of protection. However, there are many tools available to encrypt areas of your infrastructure and network. For example, you can use
      LUKS to encrypt a Linux server’s filesystem disk
      . Similarly, you can use
      GPG keys to send encrypted messages via email
      .



      Source link

      How To Use unittest to Write a Test Case for a Function in Python


      The author selected the COVID-19 Relief Fund to receive a donation as part of the Write for DOnations program.

      Introduction

      The Python standard library includes the unittest module to help you write and run tests for your Python code.

      Tests written using the unittest module can help you find bugs in your programs, and prevent regressions from occurring as you change your code over time. Teams adhering to test-driven development may find unittest useful to ensure all authored code has a corresponding set of tests.

      In this tutorial, you will use Python’s unittest module to write a test for a function.

      Prerequisites

      To get the most out of this tutorial, you’ll need:

      Defining a TestCase Subclass

      One of the most important classes provided by the unittest module is named TestCase. TestCase provides the general scaffolding for testing our functions. Let’s consider an example:

      test_add_fish_to_aquarium.py

      import unittest
      
      def add_fish_to_aquarium(fish_list):
          if len(fish_list) > 10:
              raise ValueError("A maximum of 10 fish can be added to the aquarium")
          return {"tank_a": fish_list}
      
      
      class TestAddFishToAquarium(unittest.TestCase):
          def test_add_fish_to_aquarium_success(self):
              actual = add_fish_to_aquarium(fish_list=["shark", "tuna"])
              expected = {"tank_a": ["shark", "tuna"]}
              self.assertEqual(actual, expected)
      

      First we import unittest to make the module available to our code. We then define the function we want to test—here it is add_fish_to_aquarium.

      In this case our add_fish_to_aquarium function accepts a list of fish named fish_list, and raises an error if fish_list has more than 10 elements. The function then returns a dictionary mapping the name of a fish tank "tank_a" to the given fish_list.

      A class named TestAddFishToAquarium is defined as a subclass of unittest.TestCase. A method named test_add_fish_to_aquarium_success is defined on TestAddFishToAquarium. test_add_fish_to_aquarium_success calls the add_fish_to_aquarium function with a specific input and verifies that the actual returned value matches the value we’d expect to be returned.

      Now that we’ve defined a TestCase subclass with a test, let’s review how we can execute that test.

      Executing a TestCase

      In the previous section, we created a TestCase subclass named TestAddFishToAquarium. From the same directory as the test_add_fish_to_aquarium.py file, let’s run that test with the following command:

      • python -m unittest test_add_fish_to_aquarium.py

      We invoked the Python library module named unittest with python -m unittest. Then, we provided the path to our file containing our TestAddFishToAquarium TestCase as an argument.

      After we run this command, we receive output like the following:

      Output

      . ---------------------------------------------------------------------- Ran 1 test in 0.000s OK

      The unittest module ran our test and told us that our test ran OK. The single . on the first line of the output represents our passed test.

      Note: TestCase recognizes test methods as any method that begins with test. For example, def test_add_fish_to_aquarium_success(self) is recognized as a test and will be run as such. def example_test(self), conversely, would not be recognized as a test because it does not begin with test. Only methods beginning with test will be run and reported when you run python -m unittest ....

      Now let’s try a test with a failure.

      We modify the following highlighted line in our test method to introduce a failure:

      test_add_fish_to_aquarium.py

      import unittest
      
      def add_fish_to_aquarium(fish_list):
          if len(fish_list) > 10:
              raise ValueError("A maximum of 10 fish can be added to the aquarium")
          return {"tank_a": fish_list}
      
      
      class TestAddFishToAquarium(unittest.TestCase):
          def test_add_fish_to_aquarium_success(self):
              actual = add_fish_to_aquarium(fish_list=["shark", "tuna"])
              expected = {"tank_a": ["rabbit"]}
              self.assertEqual(actual, expected)
      

      The modified test will fail because add_fish_to_aquarium won’t return "rabbit" in its list of fish belonging to "tank_a". Let’s run the test.

      Again, from the same directory as test_add_fish_to_aquarium.py we run:

      • python -m unittest test_add_fish_to_aquarium.py

      When we run this command, we receive output like the following:

      Output

      F ====================================================================== FAIL: test_add_fish_to_aquarium_success (test_add_fish_to_aquarium.TestAddFishToAquarium) ---------------------------------------------------------------------- Traceback (most recent call last): File "test_add_fish_to_aquarium.py", line 13, in test_add_fish_to_aquarium_success self.assertEqual(actual, expected) AssertionError: {'tank_a': ['shark', 'tuna']} != {'tank_a': ['rabbit']} - {'tank_a': ['shark', 'tuna']} + {'tank_a': ['rabbit']} ---------------------------------------------------------------------- Ran 1 test in 0.001s FAILED (failures=1)

      The failure output indicates that our test failed. The actual output of {'tank_a': ['shark', 'tuna']} did not match the (incorrect) expectation we added to test_add_fish_to_aquarium.py of: {'tank_a': ['rabbit']}. Notice also that instead of a ., the first line of the output now has an F. Whereas . characters are outputted when tests pass, F is the output when unittest runs a test that fails.

      Now that we’ve written and run a test, let’s try writing another test for a different behavior of the add_fish_to_aquarium function.

      Testing a Function that Raises an Exception

      unittest can also help us verify that the add_fish_to_aquarium function raises a ValueError Exception if given too many fish as input. Let’s expand on our earlier example, and add a new test method named test_add_fish_to_aquarium_exception:

      test_add_fish_to_aquarium.py

      import unittest
      
      def add_fish_to_aquarium(fish_list):
          if len(fish_list) > 10:
              raise ValueError("A maximum of 10 fish can be added to the aquarium")
          return {"tank_a": fish_list}
      
      
      class TestAddFishToAquarium(unittest.TestCase):
          def test_add_fish_to_aquarium_success(self):
              actual = add_fish_to_aquarium(fish_list=["shark", "tuna"])
              expected = {"tank_a": ["shark", "tuna"]}
              self.assertEqual(actual, expected)
      
          def test_add_fish_to_aquarium_exception(self):
              too_many_fish = ["shark"] * 25
              with self.assertRaises(ValueError) as exception_context:
                  add_fish_to_aquarium(fish_list=too_many_fish)
              self.assertEqual(
                  str(exception_context.exception),
                  "A maximum of 10 fish can be added to the aquarium"
              )
      

      The new test method test_add_fish_to_aquarium_exception also invokes the add_fish_to_aquarium function, but it does so with a 25 element long list containing the string "shark" repeated 25 times.

      test_add_fish_to_aquarium_exception uses the with self.assertRaises(...) context manager provided by TestCase to check that add_fish_to_aquarium rejects the inputted list as too long. The first argument to self.assertRaises is the Exception class that we expect to be raised—in this case, ValueError. The self.assertRaises context manager is bound to a variable named exception_context. The exception attribute on exception_context contains the underlying ValueError that add_fish_to_aquarium raised. When we call str() on that ValueError to retrieve its message, it returns the correct exception message we expected.

      From the same directory as test_add_fish_to_aquarium.py, let’s run our test:

      • python -m unittest test_add_fish_to_aquarium.py

      When we run this command, we receive output like the following:

      Output

      .. ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK

      Notably, our test would have failed if add_fish_to_aquarium either didn’t raise an Exception, or raised a different Exception (for example TypeError instead of ValueError).

      Note: unittest.TestCase exposes a number of other methods beyond assertEqual and assertRaises that you can use. The full list of assertion methods can be found in the documentation, but a selection are included here:

      Method Assertion
      assertEqual(a, b) a == b
      assertNotEqual(a, b) a != b
      assertTrue(a) bool(a) is True
      assertFalse(a) bool(a) is False
      assertIsNone(a) a is None
      assertIsNotNone(a) a is not None
      assertIn(a, b) a in b
      assertNotIn(a, b) a not in b

      Now that we’ve written some basic tests, let’s see how we can use other tools provided by TestCase to harness whatever code we are testing.

      Using the setUp Method to Create Resources

      TestCase also supports a setUp method to help you create resources on a per-test basis. setUp methods can be helpful when you have a common set of preparation code that you want to run before each and every one of your tests. setUp lets you put all this preparation code in a single place, instead of repeating it over and over for each individual test.

      Let’s take a look at an example:

      test_fish_tank.py

      import unittest
      
      class FishTank:
          def __init__(self):
              self.has_water = False
      
          def fill_with_water(self):
              self.has_water = True
      
      class TestFishTank(unittest.TestCase):
          def setUp(self):
              self.fish_tank = FishTank()
      
          def test_fish_tank_empty_by_default(self):
              self.assertFalse(self.fish_tank.has_water)
      
          def test_fish_tank_can_be_filled(self):
              self.fish_tank.fill_with_water()
              self.assertTrue(self.fish_tank.has_water)
      

      test_fish_tank.py defines a class named FishTank. FishTank.has_water is initially set to False, but can be set to True by calling FishTank.fill_with_water(). The TestCase subclass TestFishTank defines a method named setUp that instantiates a new FishTank instance and assigns that instance to self.fish_tank.

      Since setUp is run before every individual test method, a new FishTank instance is instantiated for both test_fish_tank_empty_by_default and test_fish_tank_can_be_filled. test_fish_tank_empty_by_default verifies that has_water starts off as False. test_fish_tank_can_be_filled verifies that has_water is set to True after calling fill_with_water().

      From the same directory as test_fish_tank.py, we can run:

      • python -m unittest test_fish_tank.py

      If we run the previous command, we will receive the following output:

      Output

      .. ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK

      The final output shows that the two tests both pass.

      setUp allows us to write preparation code that is run for all of our tests in a TestCase subclass.

      Note: If you have multiple test files with TestCase subclasses that you’d like to run, consider using python -m unittest discover to run more than one test file. Run python -m unittest discover --help for more information.

      Using the tearDown Method to Clean Up Resources

      TestCase supports a counterpart to the setUp method named tearDown. tearDown is useful if, for example, we need to clean up connections to a database, or modifications made to a filesystem after each test completes. We’ll review an example that uses tearDown with filesystems:

      test_advanced_fish_tank.py

      import os
      import unittest
      
      class AdvancedFishTank:
          def __init__(self):
              self.fish_tank_file_name = "fish_tank.txt"
              default_contents = "shark, tuna"
              with open(self.fish_tank_file_name, "w") as f:
                  f.write(default_contents)
      
          def empty_tank(self):
              os.remove(self.fish_tank_file_name)
      
      
      class TestAdvancedFishTank(unittest.TestCase):
          def setUp(self):
              self.fish_tank = AdvancedFishTank()
      
          def tearDown(self):
              self.fish_tank.empty_tank()
      
          def test_fish_tank_writes_file(self):
              with open(self.fish_tank.fish_tank_file_name) as f:
                  contents = f.read()
              self.assertEqual(contents, "shark, tuna")
      

      test_advanced_fish_tank.py defines a class named AdvancedFishTank. AdvancedFishTank creates a file named fish_tank.txt and writes the string "shark, tuna" to it. AdvancedFishTank also exposes an empty_tank method that removes the fish_tank.txt file. The TestAdvancedFishTank TestCase subclass defines both a setUp and tearDown method.

      The setUp method creates an AdvancedFishTank instance and assigns it to self.fish_tank. The tearDown method calls the empty_tank method on self.fish_tank: this ensures that the fish_tank.txt file is removed after each test method runs. This way, each test starts with a clean slate. The test_fish_tank_writes_file method verifies that the default contents of "shark, tuna" are written to the fish_tank.txt file.

      From the same directory as test_advanced_fish_tank.py let’s run:

      • python -m unittest test_advanced_fish_tank.py

      We will receive the following output:

      Output

      . ---------------------------------------------------------------------- Ran 1 test in 0.000s OK

      tearDown allows you to write cleanup code that is run for all of your tests in a TestCase subclass.

      Conclusion

      In this tutorial, you have written TestCase classes with different assertions, used the setUp and tearDown methods, and run your tests from the command line.

      The unittest module exposes additional classes and utilities that you did not cover in this tutorial. Now that you have a baseline, you can use the unittest module’s documentation to learn more about other available classes and utilities. You may also be interested in How To Add Unit Testing to Your Django Project.



      Source link

      How To Use the Python Map Function


      Introduction

      We can use the Python built-in function map() to apply a function to each item in an iterable (like a list or dictionary) and return a new iterator for retrieving the results. map() returns a map object (an iterator), which we can use in other parts of our program. We can also pass the map object to the list() function, or another sequence type, to create an iterable.

      The syntax for the map() function is as follows:

      map(function, iterable, [iterable 2, iterable 3, ...])
      

      Instead of using a for loop, the map() function provides a way of applying a function to every item in an iterable. Therefore it can often be more performant since it is only applying the function one item at a time rather than making copies of the items into another iterable. This is particularly useful when working on programs processing large data sets. map() can also take multiple iterables as arguments to the function by sending one item from each iterable to the function at a time.

      In this tutorial, we’ll review three different ways of working with map(): with a lambda function, with a user-defined function, and finally with a built-in function using multiple iterable arguments.

      Using a Lambda Function

      The first argument to map() is a function, which we use to apply to each item. Python calls the function once for every item in the iterable we pass into map() and it returns the manipulated item within a map object. For the first function argument, we can either pass a user-defined function or we can make use of lambda functions, particularly when the expression is less complex.

      The syntax of map() with a lambda function is as follows:

      map(lambda item: item[] expression, iterable)
      

      With a list like the following, we can implement a lambda function with an expression that we want to apply to each item in our list:

      numbers = [10, 15, 21, 33, 42, 55]
      

      To apply an expression against each of our numbers, we can use map() and lambda:

      mapped_numbers = list(map(lambda x: x * 2 + 3, numbers))
      

      Here we declare an item in our list as x. Then we add our expression. We pass in our list of numbers as the iterable for map().

      In order to receive the results of this immediately we print a list of the map object:

      print(mapped_numbers)
      

      Output

      [23, 33, 45, 69, 87, 113]

      We have used list() so that the map object is returned to us as a list, rather than a less human-readable object like: <map object at 0x7fc250003a58>. The map object is an iterator over our results, so we could loop over it with for or we can use list() to turn it into a list. We’re doing this here because it’s a good way to review the results.

      Ultimately map() is most useful when working with large datasets, so we would likely work with the map object further, and generally would not be using a constructor like list() on them.

      For smaller datasets, list comprehensions may be more suitable, but for the purposes of this tutorial we’re using a small dataset to demonstrate map().

      Implementing a User-defined Function

      Similarly to a lambda we can use a function we have defined to apply to an iterable. While lambda functions are more useful to implement when you’re working with a one-line expression, user-defined functions are more appropriate when the expression grows in complexity. Furthermore, when we need to pass another piece of data to the function that you’re applying to your iterable, user-defined functions can be a better choice for readability.

      For example, in the following iterable, each item is a dictionary that contains different details about each of our aquarium creatures:

      aquarium_creatures = [
          {"name": "sammy", "species": "shark", "tank number": 11, "type": "fish"},
          {"name": "ashley", "species": "crab", "tank number": 25, "type": "shellfish"},
          {"name": "jo", "species": "guppy", "tank number": 18, "type": "fish"},
          {"name": "jackie", "species": "lobster", "tank number": 21, "type": "shellfish"},
          {"name": "charlie", "species": "clownfish", "tank number": 12, "type": "fish"},
          {"name": "olly", "species": "green turtle", "tank number": 34, "type": "turtle"}
      ]
      

      We’ve decided that all the aquarium creatures are in fact going to move into the same tank. We need to update our records to reflect that all of our creatures are moving into tank 42. To have map() access each dictionary and each key:value pair in the dictionaries, we construct a nested function:

      def assign_to_tank(aquarium_creatures, new_tank_number):
          def apply(x):
              x["tank number"] = new_tank_number
              return x
          return map(apply, aquarium_creatures)
      

      We define an assign_to_tank() function that takes aquarium_creatures and new_tank_number as parameters. In assign_to_tank() we pass apply() as the function to map() on the final line. The assign_to_tank function will return the iterator resulting from map().

      apply() takes x as an argument, which represents an item in our list — a single dictionary.

      Next we define that x is the "tank number" key from aquarium_creatures and that it should store the passed in new_tank_number. We return each item after applying the new tank number.

      We call assign_to_tank() with our list of dictionaries and the new tank number we want to replace for each creature:

      assigned_tanks = assign_to_tank(aquarium_creatures, 42)
      

      Once the function completes we have our map object stored in the assigned_tanks variable, which we turn into a list and print:

      print(list(assigned_tanks))
      

      We’ll receive the following output from this program:

      Output

      [{'name': 'sammy', 'species': 'shark', 'tank number': 42, 'type': 'fish'}, {'name': 'ashley', 'species': 'crab', 'tank number': 42, 'type': 'shellfish'}, {'name': 'jo', 'species': 'guppy', 'tank number': 42, 'type': 'fish'}, {'name': 'jackie', 'species': 'lobster', 'tank number': 42, 'type': 'shellfish'}, {'name': 'charlie', 'species': 'clownfish', 'tank number': 42, 'type': 'fish'}, {'name': 'olly', 'species': 'green turtle', 'tank number': 42, 'type': 'turtle'}]

      We’ve mapped the new tank number to our list of dictionaries. Using a function that we define, we can incorporate map() to apply the function efficiently on each item of the list.

      Using a Built-in Function with Multiple Iterables

      In the same way as lambda functions or our own defined functions, we can use Python built-in functions with map(). To apply a function with multiple iterables, we pass in another iterable name following the first one. For example, using the pow() function that takes in two numbers to find the power of the base number to the provided exponent.

      Here we have our lists of integers that we would like to use with pow():

      base_numbers = [2, 4, 6, 8, 10]
      powers = [1, 2, 3, 4, 5]
      

      Next we pass in pow() as our function into map() and provide the two lists as our iterables:

      numbers_powers = list(map(pow, base_numbers, powers))
      
      print(numbers_powers)
      

      map() will apply the pow() function to the same item in each list to provide the power. Therefore our results will show 2**1, 4**2, 6**3, and so on:

      Output

      [2, 16, 216, 4096, 100000]

      If we were to provide map() with an iterable that was longer than the other, map() would stop calculating once it reaches the end of the shortest iterable. In the following program we’re extending base_numbers with three additional numbers:

      base_numbers = [2, 4, 6, 8, 10, 12, 14, 16]
      powers = [1, 2, 3, 4, 5]
      
      numbers_powers = list(map(pow, base_numbers, powers))
      
      print(numbers_powers)
      

      As a result, nothing will change within the calculation of this program and so it will still yield the same result:

      Output

      [2, 16, 216, 4096, 100000]

      We’ve used the map() function with a Python built-in function and have seen that it can handle multiple iterables. We’ve also reviewed that map() will continue to process multiple iterables until it has reached the end of the iterable with the fewest items.

      Conclusion

      In this tutorial, we’ve learned the different ways of using the map() function in Python. Now you can use map() with your own function, a lambda function, and with any other built-in functions. You can also implement map() with functions that require multiple iterables.

      In this tutorial, we printed the results from map() immediately to a list format for demonstration purposes. In our programs we would typically use the returned map object to further manipulate the data.

      If you would like to learn more Python, check out our How To Code in Python 3 series and our Python topic page. To learn more about working with data sets in functional programming, check out our article on the filter() function.



      Source link