Skip to content

celikmustafa89/sololearn-python-notes

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PYTHON NOTES


Table of Contents

Source: SoloLearn Python Tutorial

Functional Programming

Lambdas


Creating a function normally (using def) assigns it to a variable automatically.
This is different from the creation of other objects - such as strings and integers - which can be created on the fly, without assigning them to a variable.
The same is possible with functions, provided that they are created using lambda syntax. Functions created this way are known as anonymous.
This approach is most commonly used when passing a simple function as an argument to another function. The syntax is shown in the next example and consists of the lambda keyword followed by a list of arguments, a colon, and the expression to evaluate and return.

def my_func(f, arg):
  return f(arg)

my_func(lambda x: 2*x*x, 5)

Note: Lambda functions get their name from lambda calculus, which is a model of computation invented by Alonzo Church.

Map


The built-in functions map and filter are very useful higher-order functions that operate on lists (or similar objects called iterables).
The function map takes a function and an iterable as arguments, and returns a new iterable with the function applied to each argument.

Example:

def add_five(x):
  return x + 5

nums = [11, 22, 33, 44, 55]
result = list(map(add_five, nums))
print(result)

Result:

[16, 27, 38, 49, 60]

We could have achieved the same result more easily by using lambda syntax.

nums = [11, 22, 33, 44, 55]

result = list(map(lambda x: x+5, nums))
print(result)

Note: To convert the result into a list, we used list explicitly.

Filter


The function filter filters an iterable by removing items that don’t match a predicate (a function that returns a Boolean).
Example:

nums = [11, 22, 33, 44, 55]
res = list(filter(lambda x: x%2==0, nums))
print(res)

Result:

[22, 44]

Note: Like map, the result has to be explicitly converted to a list if you want to print it.

Example:
Fill in the blanks to remove all items that are greater than 4 from the list.

nums = [1, 2, 5, 8, 3, 0, 7]
res = list(filter(lambda x: x < 5, nums))
print(res)

Generators-1


Generators are a type of iterable, like lists or tuples.
Unlike lists, they don’t allow indexing with arbitrary indices, but they can still be iterated through with for loops.
They can be created using functions and the yield statement.
Example:

def countdown():
  i=5
  while i > 0:
    yield i
    i -= 1

for i in countdown():
  print(i)

Result:

5
4
3
2
1

The yield statement is used to define a generator, replacing the return of a function to provide a result to its caller without destroying local variables.

Generators-2


Due to the fact that they yield one item at a time, generators don’t have the memory restrictions of lists.
In fact, they can be infinite!

def infinite_sevens():
  while True:
    yield 7

for i in infinite_sevens():
  print(i)

Result:

7
7
7
7
7
7
7

Note: In short, generators allow you to declare a function that behaves like an iterator, i.e. it can be used in a for loop.

Example:
Fill in the blanks to create a prime number generator, that yields all prime numbers in a loop. (Consider having an is_prime function already defined):

 def get_primes():
  num = 2
  while True:
    if is_prime(num):
      yield num
    num += 1

Generators-3


Finite generators can be converted into lists by passing them as arguments to the list function.

def numbers(x):
  for i in range(x):
    if i % 2 == 0:
      yield i

print(list(numbers(11)))

Result:

[0, 2, 4, 6, 8, 10]

Note: Using generators results in improved performance, which is the result of the lazy (on demand) generation of values, which translates to lower memory usage. Furthermore, we do not need to wait until all the elements have been generated before we start to use them.

Decorators-1


Decorators provide a way to modify functions using other functions.
This is ideal when you need to extend the functionality of functions that you don’t want to modify.
Example:

def decor(func):
  def wrap():
    print("============")
    func()
    print("============")
  return wrap

def print_text():
  print("Hello world!")

decorated = decor(print_text)
decorated()

We defined a function named decor that has a single parameter func. Inside decor, we defined a nested function named wrap. The wrap function will print a string, then call func(), and print another string. The decor function returns the wrap function as its result.
We could say that the variable decorated is a decorated version of print_text - it’s print_text plus something.
In fact, if we wrote a useful decorator we might want to replace print_text with the decorated version altogether so we always got our “plus something” version of print_text.
This is done by re-assigning the variable that contains our function:

print_text = decor(print_text)
print_text()

Now print_text corresponds to our decorated version.

Decorators-2


In our previous example, we decorated our function by replacing the variable containing the function with a wrapped version.

def print_text():
  print("Hello world!")

print_text = decor(print_text)

This pattern can be used at any time, to wrap any function.
Python provides support to wrap a function in a decorator by pre-pending the function definition with a decorator name and the @ symbol.
If we are defining a function we can “decorate” it with the @ symbol like:

@decor
def print_text():
  print("Hello world!")
Try It Yourself

This will have the same result as the above code.

Note: A single function can have multiple decorators.

Recursion-1


Recursion is a very important concept in functional programming.
The fundamental part of recursion is self-reference - functions calling themselves. It is used to solve problems that can be broken up into easier sub-problems of the same type.

A classic example of a function that is implemented recursively is the factorial function, which finds the product of all positive integers below a specified number.
For example, 5! (5 factorial) is 5 * 4 * 3 * 2 * 1 (120). To implement this recursively, notice that 5! = 5 * 4!, 4! = 4 * 3!, 3! = 3 * 2!, and so on. Generally, n! = n * (n-1)!.
Furthermore, 1! = 1. This is known as the base case, as it can be calculated without performing any more factorials.
Below is a recursive implementation of the factorial function.

def factorial(x):
  if x == 1:
    return 1
  else: 
    return x * factorial(x-1)

print(factorial(5))

Result:

120

Note: The base case acts as the exit condition of the recursion.

Recursion-2


Recursive functions can be infinite, just like infinite while loops. These often occur when you forget to implement the base case.
Below is an incorrect version of the factorial function. It has no base case, so it runs until the interpreter runs out of memory and crashes.

def factorial(x):
  return x * factorial(x-1)

print(factorial(5))

Result:

RuntimeError: maximum recursion depth exceeded

Recursion-3


Recursion can also be indirect. One function can call a second, which calls the first, which calls the second, and so on. This can occur with any number of functions.
Example:

def is_even(x):
  if x == 0:
    return True
  else:
    return is_odd(x-1)

def is_odd(x):
  return not is_even(x)


print(is_odd(17))
print(is_even(23))

Result:

True
False

Sets-1


Sets are data structures, similar to lists or dictionaries. They are created using curly braces, or the set function. They share some functionality with lists, such as the use of in to check whether they contain a particular item.

num_set = {1, 2, 3, 4, 5}
word_set = set(["spam", "eggs", "sausage"])

print(3 in num_set)
print("spam" not in word_set)

Result:

True
False

Note: To create an empty set, you must use set(), as {} creates an empty dictionary.

Sets-2


Sets differ from lists in several ways, but share several list operations such as len.
They are unordered, which means that they can’t be indexed.
They cannot contain duplicate elements.
Due to the way they’re stored, it’s faster to check whether an item is part of a set, rather than part of a list.
Instead of using append to add to a set, use add.
The method remove removes a specific element from a set; pop removes an arbitrary element.

nums = {1, 2, 1, 3, 1, 4, 5, 6}
print(nums)
nums.add(-7)
nums.remove(3)
print(nums)

Result:

{1, 2, 3, 4, 5, 6}
{1, 2, 4, 5, 6, -7}

Note: Basic uses of sets include membership testing and the elimination of duplicate entries.

Sets-3


Sets can be combined using mathematical operations.
The union operator | combines two sets to form a new one containing items in either.
The intersection operator & gets items only in both.
The difference operator - gets items in the first set but not in the second.
The symmetric difference operator ^ gets items in either set, but not both.

first = {1, 2, 3, 4, 5, 6}
second = {4, 5, 6, 7, 8, 9}

print(first | second)
print(first & second)
print(first - second)
print(second - first)
print(first ^ second)

Result:

{1, 2, 3, 4, 5, 6, 7, 8, 9}
{4, 5, 6}
{1, 2, 3}
{8, 9, 7}
{1, 2, 3, 7, 8, 9}

StackEdit stores your documents in your browser, which means all your documents are automatically saved locally and are accessible offline!

Data Structures


As we have seen in the previous lessons, Python supports the following data structures: lists, dictionaries, tuples, sets.

When to use a dictionary:

  • When you need a logical association between a key:value pair.
  • When you need fast lookup for your data, based on a custom key.
  • When your data is being constantly modified. Remember, dictionaries are mutable.

When to use the other types:

  • Use lists if you have a collection of data that does not need random access. Try to choose lists when you need a simple, iterable collection that is modified frequently.
  • Use a set if you need uniqueness for the elements.
  • Use tuples when your data cannot change.

Many times, a tuple is used in combination with a dictionary, for example, a tuple might represent a key, because it’s immutable.

itertools-1


The module itertools is a standard library that contains several functions that are useful in functional programming.
One type of function it produces is infinite iterators.
The function count counts up infinitely from a value.
The function cycle infinitely iterates through an iterable (for instance a list or string).
The function repeat repeats an object, either infinitely or a specific number of times.
Example:

from itertools import count

for i in count(3):
  print(i)
  if i >=11:
    break

Result:

3
4
5
6
7
8
9
10
11

itertools-2


There are many functions in itertools that operate on iterables, in a similar way to map and filter.
Some examples:
takewhile - takes items from an iterable while a predicate function remains true;
chain - combines several iterables into one long one;
accumulate - returns a running total of values in an iterable.

from itertools import accumulate, takewhile

nums = list(accumulate(range(8)))
print(nums)
print(list(takewhile(lambda x: x<= 6, nums)))

Result:

[0, 1, 3, 6, 10, 15, 21, 28]
[0, 1, 3, 6]

itertools-3


There are also several combinatoric functions in itertool, such as product and permutation.
These are used when you want to accomplish a task with all possible combinations of some items.
Example:

from itertools import product, permutations

letters = ("A", "B")
print(list(product(letters, range(2))))
print(list(permutations(letters))) 

Result:

[(‘A’, 0), (‘A’, 1), (‘B’, 0), (‘B’, 1)]
[(‘A’, ‘B’), (‘B’, ‘A’)]

Object-Oriented Programming

Classes-1


We have previously looked at two paradigms of programming - imperative (using statements, loops, and functions as subroutines), and functional (using pure functions, higher-order functions, and recursion).

Another very popular paradigm is object**-**oriented programming (OOP).
Objects are created using classes, which are actually the focal point of OOP.
The class describes what the object will be, but is separate from the object itself. In other words, a class can be described as an object’s blueprint, description, or definition.
You can use the same class as a blueprint for creating multiple different objects.

Classes are created using the keyword class and an indented block, which contains class methods (which are functions).
Below is an example of a simple class and its objects.

class Cat:
  def __init__(self, color, legs):
    self.color = color
    self.legs = legs

felix = Cat("ginger", 4)
rover = Cat("dog-colored", 4)
stumpy = Cat("brown", 3)

Note: This code defines a class named Cat, which has two attributes: color and legs.
Then the class is used to create 3 separate objects of that class.

__init__


The init method is the most important method in a class.
This is called when an instance (object) of the class is created, using the class name as a function.

All methods must have self as their first parameter, although it isn’t explicitly passed, Python adds the self argument to the list for you; you do not need to include it when you call the methods. Within a method definition, self refers to the instance calling the method.

Instances of a class have attributes, which are pieces of data associated with them.
In this example, Cat instances have attributes color and legs. These can be accessed by putting a dot, and the attribute name after an instance.
In an init method, self.attribute can therefore be used to set the initial value of an instance’s attributes.
Example:

class Cat:
  def __init__(self, color, legs):
    self.color = color
    self.legs = legs

felix = Cat("ginger", 4)
print(felix.color)

Result:

Note: ginger

Note: In the example above, the init method takes two arguments and assigns them to the object’s attributes. The init method is called the class constructor.

Methods


Classes can have other methods defined to add functionality to them.
Remember, that all methods must have self as their first parameter.
These methods are accessed using the same dot syntax as attributes.
Example:

class Dog:
  def __init__(self, name, color):
    self.name = name
    self.color = color

  def bark(self):
    print("Woof!")

fido = Dog("Fido", "brown")
print(fido.name)
fido.bark()

Result:

Fido
Woof!

Classes can also have class attributes, created by assigning variables within the body of the class. These can be accessed either from instances of the class, or the class itself.
Example:

class Dog:
  legs = 4
  def __init__(self, name, color):
    self.name = name
    self.color = color

fido = Dog("Fido", "brown")
print(fido.legs)
print(Dog.legs)

Result:

4
4

NOTE: Class attributes are shared by all instances of the class.

Classes-2


Trying to access an attribute of an instance that isn’t defined causes an AttributeError. This also applies when you call an undefined method.

Example:

class Rectangle: 
  def __init__(self, width, height):
    self.width = width
    self.height = height

rect = Rectangle(7, 8)
print(rect.color)

Result:

AttributeError: ‘Rectangle’ object has no attribute ‘color’

Inheritance-1


Inheritance provides a way to share functionality between classes.
Imagine several classes, Cat, Dog, Rabbit and so on. Although they may differ in some ways (only Dog might have the method bark), they are likely to be similar in others (all having the attributes color and name).
This similarity can be expressed by making them all inherit from a superclass Animal, which contains the shared functionality.
To inherit a class from another class, put the superclass name in parentheses after the class name.
Example:

class Animal: 
  def __init__(self, name, color):
    self.name = name
    self.color = color

class Cat(Animal):
  def purr(self):
    print("Purr...")

class Dog(Animal):
  def bark(self):
    print("Woof!")

fido = Dog("Fido", "brown")
print(fido.color)
fido.bark()

Result:

brown
Woof!

Inheritance-2


A class that inherits from another class is called a subclass.
A class that is inherited from is called a superclass.
If a class inherits from another with the same attributes or methods, it overrides them.

class Wolf: 
  def __init__(self, name, color):
    self.name = name
    self.color = color

  def bark(self):
    print("Grr...")

class Dog(Wolf):
  def bark(self):
    print("Woof")

husky = Dog("Max", "grey")
husky.bark()

Result:

Woof

Note: In the example above, Wolf is the superclass, Dog is the subclass.

Inheritance-3


Inheritance can also be indirect. One class can inherit from another, and that class can inherit from a third class.
Example:

class A:
  def method(self):
    print("A method")

class B(A):
  def another_method(self):
    print("B method")

class C(B):
  def third_method(self):
    print("C method")

c = C()
c.method()
c.another_method()
c.third_method()

Result:

A method
B method
C method

Note: However, circular inheritance is not possible.

Inheritance-4


The function super is a useful inheritance-related function that refers to the parent class. It can be used to find the method with a certain name in an object’s superclass.
Example:

class A:
  def spam(self):
    print(1)

class B(A):
  def spam(self):
    print(2)
    super().spam()

B().spam()

Result:

2
1

Note: super().spam() calls the spam method of the superclass.

Magic Methods-1


Magic methods are special methods which have double underscores at the beginning and end of their names.
They are also known as dunders.
So far, the only one we have encountered is __init__, but there are several others.
They are used to create functionality that can’t be represented as a normal method.

One common use of them is operator overloading.
This means defining operators for custom classes that allow operators such as + and * to be used on them.
An example magic method is __add__ for +.

class Vector2D:
  def __init__(self, x, y):
    self.x = x
    self.y = y
  def __add__(self, other):
    return Vector2D(self.x + other.x, self.y + other.y)

first = Vector2D(5, 7)
second = Vector2D(3, 9)
result = first + second
print(result.x)
print(result.y)

Result:

8
16

The __add__ method allows for the definition of a custom behavior for the + operator in our class.
As you can see, it adds the corresponding attributes of the objects and returns a new object, containing the result.
Once it’s defined, we can add two objects of the class together.

Magic Methods-2


More magic methods for common operators:
__sub__ for -
__mul__ for *
__truediv__ for /
__floordiv__ for //
__mod__ for %
__pow__ for **
__and__ for &
__xor__ for ^
__or__ for |

The expression x + y is translated into x.__add__(y).
However, if x hasn’t implemented __add__, and x and y are of different types, then y.__radd__(x) is called.
There are equivalent r methods for all magic methods just mentioned.
Example:

class SpecialString:
  def __init__(self, cont):
    self.cont = cont

  def __truediv__(self, other):
    line = "=" * len(other.cont)
    return "\n".join([self.cont, line, other.cont])

spam = SpecialString("spam")
hello = SpecialString("Hello world!")
print(spam / hello)

Result:

spam
============
Hello world!

Note: In the example above, we defined the division operation for our class SpecialString.

Magic Methods-3


Python also provides magic methods for comparisons.
__lt__ for <
__le__ for <=
__eq__ for ==
__ne__ for !=
__gt__ for >
__ge__ for >=

If __ne__ is not implemented, it returns the opposite of __eq__.
Note: There are no other relationships between the other operators.
Example:

class SpecialString:
  def __init__(self, cont):
    self.cont = cont

  def __gt__(self, other):
    for index in range(len(other.cont)+1):
      result = other.cont[:index] + ">" + self.cont
      result += ">" + other.cont[index:]
      print(result)

spam = SpecialString("spam")
eggs = SpecialString("eggs")
spam > eggs

Result:

>spam>eggs
e>spam>ggs
eg>spam>gs
egg>spam>s
eggs>spam>

Note: As you can see, you can define any custom behavior for the overloaded operators.

Magic Methods-4


There are several magic methods for making classes act like containers.
__len__ for len()
__getitem__ for indexing
__setitem__ for assigning to indexed values
__delitem__ for deleting indexed values
__iter__ for iteration over objects (e.g., in for loops)
__contains__ for in

There are many other magic methods that we won’t cover here, such as __call__ for calling objects as functions, and __int__, __str__, and the like, for converting objects to built-in types.
Example:

import random

class VagueList:
  def __init__(self, cont):
    self.cont = cont

  def __getitem__(self, index):
    return self.cont[index + random.randint(-1, 1)]

  def __len__(self):
    return random.randint(0, len(self.cont)*2)

vague_list = VagueList(["A", "B", "C", "D", "E"])
print(len(vague_list))
print(len(vague_list))
print(vague_list[2])
print(vague_list[2])

Result:

6
7
D
C

Note: We have overridden the len() function for the class VagueList to return a random number.
The indexing function also returns a random item in a range from the list, based on the expression.

Object Lifecycle-1


The lifecycle of an object is made up of its creation, manipulation, and destruction.

The first stage of the life-cycle of an object is the definition of the class to which it belongs.
The next stage is the instantiation of an instance, when __init__ is called. Memory is allocated to store the instance. Just before this occurs, the __new__ method of the class is called. This is usually overridden only in special cases.

Note: After this has happened, the object is ready to be used. Other code can then interact with the object, by calling functions on
it and accessing its attributes. Eventually, it will finish being
used, and can be destroyed.

Object Lifecycle-2


When an object is destroyed, the memory allocated to it is freed up, and can be used for other purposes.
Destruction of an object occurs when its reference count reaches zero. Reference count is the number of variables and other elements that refer to an object.
If nothing is referring to it (it has a reference count of zero) nothing can interact with it, so it can be safely deleted.

In some situations, two (or more) objects can be referred to by each other only, and therefore can be deleted as well.
The del statement reduces the reference count of an object by one, and this often leads to its deletion.
The magic method for the del statement is __del__.
The process of deleting objects when they are no longer needed is called garbage collection.
In summary, an object’s reference count increases when it is assigned a new name or placed in a container (list, tuple, or dictionary). The object’s reference count decreases when it’s deleted with del, its reference is reassigned, or its reference goes out of scope. When an object’s reference count reaches zero, Python automatically deletes it.
Example:

a = 42  # Create object <42>
b = a  # Increase ref. count  of <42> 
c = [a]  # Increase ref. count  of <42> 

del a  # Decrease ref. count  of <42>
b = 100  # Decrease ref. count  of <42> 
c[0] = -1  # Decrease ref. count  of <42>

Note: Lower level languages like C don’t have this kind of automatic memory management.

Data Hiding-1


A key part of object-oriented programming is encapsulation, which involves packaging of related variables and functions into a single easy-to-use object - an instance of a class.
A related concept is data hiding, which states that implementation details of a class should be hidden, and a clean standard interface be presented for those who want to use the class.
In other programming languages, this is usually done with private methods and attributes, which block external access to certain methods and attributes in a class.

The Python philosophy is slightly different. It is often stated as “we are all consenting adults here”, meaning that you shouldn’t put arbitrary restrictions on accessing parts of a class. Hence there are no ways of enforcing a method or attribute be strictly private.

Note: However, there are ways to discourage people from accessing parts of a class, such as by denoting that it is an implementation
detail, and should be used at their own risk.

Data Hiding-2


Weakly private methods and attributes have a single underscore at the beginning.
This signals that they are private, and shouldn’t be used by external code. However, it is mostly only a convention, and does not stop external code from accessing them.
Its only actual effect is that from module_name import * won’t import variables that start with a single underscore.
Example:

class Queue:
  def __init__(self, contents):
    self._hiddenlist = list(contents)

  def push(self, value):
    self._hiddenlist.insert(0, value)

  def pop(self):
    return self._hiddenlist.pop(-1)

  def __repr__(self):
    return "Queue({})".format(self._hiddenlist)

queue = Queue([1, 2, 3])
print(queue)
queue.push(0)
print(queue)
queue.pop()
print(queue)
print(queue._hiddenlist)

Result:

Queue([1, 2, 3])
Queue([0, 1, 2, 3])
Queue([0, 1, 2])
[0, 1, 2]

Note: In the code above, the attribute _hiddenlist is marked as private, but it can still be accessed in the outside code.
The __repr__ magic method is used for string representation of the instance.

Data Hiding-3


Strongly private methods and attributes have a double underscore at the beginning of their names. This causes their names to be mangled, which means that they can’t be accessed from outside the class.
The purpose of this isn’t to ensure that they are kept private, but to avoid bugs if there are subclasses that have methods or attributes with the same names.
Name mangled methods can still be accessed externally, but by a different name. The method __privatemethod of class Spam could be accessed externally with _Spam__privatemethod.
Example:

class Spam:
  __egg = 7
  def print_egg(self):
    print(self.__egg)

s = Spam()
s.print_egg()
print(s._Spam__egg)
print(s.__egg)

Result:

7
7
AttributeError: ‘Spam’ object has no attribute ‘__egg’

Note: Basically, Python protects those members by internally changing the name to include the class name.

Class Methods


Methods of objects we’ve looked at so far are called by an instance of a class, which is then passed to the self parameter of the method.
Class methods are different - they are called by a class, which is passed to the cls parameter of the method.
A common use of these are factory methods, which instantiate an instance of a class, using different parameters than those usually passed to the class constructor.
Class methods are marked with a classmethod decorator.
Example:

class Rectangle:
  def __init__(self, width, height):
    self.width = width
    self.height = height

  def calculate_area(self):
    return self.width * self.height

  @classmethod
  def new_square(cls, side_length):
    return cls(side_length, side_length)

square = Rectangle.new_square(5)
print(square.calculate_area())

Result:

25

new_square is a class method and is called on the class, rather than on an instance of the class. It returns a new object of the class cls.
Technically, the parameters self and cls are just conventions; they could be changed to anything else. However, they are universally followed, so it is wise to stick to using them.

Static Methods


Static methods are similar to class methods, except they don’t receive any additional arguments; they are identical to normal functions that belong to a class.
They are marked with the staticmethod decorator.
Example:

class Pizza:
  def __init__(self, toppings):
    self.toppings = toppings

  @staticmethod
  def validate_topping(topping):
    if topping == "pineapple":
      raise ValueError("No pineapples!")
    else:
      return True

ingredients = ["cheese", "onions", "spam"]
if all(Pizza.validate_topping(i) for i in ingredients):
  pizza = Pizza(ingredients) 

Note: Static methods behave like plain functions, except for the fact that you can call them from an instance of the class.

Properties-1


Properties provide a way of customizing access to instance attributes.
They are created by putting the property decorator above a method, which means when the instance attribute with the same name as the method is accessed, the method will be called instead.
One common use of a property is to make an attribute read-only.
Example:

class Pizza:
  def __init__(self, toppings):
    self.toppings = toppings

  @property
  def pineapple_allowed(self):
    return False

pizza = Pizza(["cheese", "tomato"])
print(pizza.pineapple_allowed)
pizza.pineapple_allowed = True

Result:

False

AttributeError: can’t set attribute

Properties-2


Properties can also be set by defining setter/getter functions.
The setter function sets the corresponding property’s value.
The getter gets the value.
To define a setter, you need to use a decorator of the same name as the property, followed by a dot and the setter keyword.
The same applies to defining getter functions.
Example:

class Pizza:
  def __init__(self, toppings):
    self.toppings = toppings
    self._pineapple_allowed = False

  @property
  def pineapple_allowed(self):
    return self._pineapple_allowed

  @pineapple_allowed.setter
  def pineapple_allowed(self, value):
    if value:
      password = input("Enter the password: ")
      if password == "Sw0rdf1sh!":
        self._pineapple_allowed = value
      else:
        raise ValueError("Alert! Intruder!")

pizza = Pizza(["cheese", "tomato"])
print(pizza.pineapple_allowed)
pizza.pineapple_allowed = True
print(pizza.pineapple_allowed)

Result:

False
Enter the password to permit pineapple: Sw0rdf1sh!
True

A Simple Game


Object-orientation is very useful when managing different objects and their relations. That is especially useful when you are developing games with different characters and features.

Let’s look at an example project that shows how classes are used in game development.
The game to be developed is an old fashioned text-based adventure game.
Below is the function handling input and simple parsing.

def get_input():
  command = input(": ").split()
  verb_word = command[0]
  if verb_word in verb_dict:
    verb = verb_dict[verb_word]
  else:
    print("Unknown verb {}". format(verb_word))
    return

  if len(command) >= 2:
    noun_word = command[1]
    print (verb(noun_word))
  else:
    print(verb("nothing"))

def say(noun):
  return 'You said "{}"'.format(noun)

verb_dict = {
  "say": say,
}

while True:
  get_input()

Result:

: say Hello!
You said "Hello!"
: say Goodbye!
You said "Goodbye!"

: test
Unknown verb test

Note: The code above takes input from the user, and tries to match the first word with a command in verb_dict. If a match is found, the
corresponding function is called.

The next step is to use classes to represent game objects.

class GameObject:
  class_name = ""
  desc = ""
  objects = {}

  def __init__(self, name):
    self.name = name
    GameObject.objects[self.class_name] = self

  def get_desc(self):
    return self.class_name + "\n" + self.desc

class Goblin(GameObject):
  class_name = "goblin"
  desc = "A foul creature"

goblin = Goblin("Gobbly")

def examine(noun):
  if noun in GameObject.objects:
    return GameObject.objects[noun].get_desc()
  else:
    return "There is no {} here.".format(noun)

We created a Goblin class, which inherits from the GameObjects class.
We also created a new function examine, which returns the objects description.
Now we can add a new “examine” verb to our dictionary and try it out!

verb_dict = {
  "say": say,
  "examine": examine,
}

Combine this code with the one in our previous example, and run the program.

>>>
: say Hello!
You said "Hello!"

: examine goblin
goblin
A foul creature

: examine elf
There is no elf here.
:

This code adds more detail to the Goblin class and allows you to fight goblins.

class Goblin(GameObject):
  def __init__(self, name):
    self.class_name = "goblin"
    self.health = 3
    self._desc = " A foul creature"
    super().__init__(name)

  @property
  def desc(self):
    if self.health >=3:
      return self._desc
    elif self.health == 2:
      health_line = "It has a wound on its knee."
    elif self.health == 1:
      health_line = "Its left arm has been cut off!"
    elif self.health <= 0:
      health_line = "It is dead."
    return self._desc + "\n" + health_line

  @desc.setter
  def desc(self, value):
    self._desc = value

def hit(noun):
  if noun in GameObject.objects:
    thing = GameObject.objects[noun]
    if type(thing) == Goblin:
      thing.health = thing.health - 1
      if thing.health <= 0:
        msg = "You killed the goblin!"
      else: 
        msg = "You hit the {}".format(thing.class_name)
  else:
    msg ="There is no {} here.".format(noun) 
  return msg

Result:

>>>
: hit goblin
You hit the goblin

: examine goblin
goblin
 A foul creature
It has a wound on its knee.

: hit goblin
You hit the goblin

: hit goblin
You killed the goblin!

: examine goblin
A goblin

goblin
 A foul creature
It is dead.
:

Note: This was just a simple sample. You could create different classes
(e.g., elves, orcs, humans), fight them, make them fight each other,
and so on.

Releases

No releases published

Packages

No packages published