To install pip
manually, follow these steps:
- Download the installation script: get-pip.py
- Execute the script to install
pip
:python get-pip.py
This manual installation process is useful in scenarios where pip
is not included or needs to be updated independently.
Python decorators provide a powerful mechanism to modify the behavior of functions without altering their core implementation. They are particularly useful when additional functionalities such as logging, permission verification, or generic checks are required before executing a function.
To create a decorator, a two-tiered function structure is employed:
- An outer function that takes a function as an argument.
- An inner function that encapsulates the logic to be executed before and after the wrapped function.
from datetime import datetime
import time
def logger(func):
def wrapper():
print("-" * 50)
print("> Execution started {}".format(datetime.today().strftime("%Y-%m-%d %H:%M:%S")))
func()
print("> Execution completed {}".format(datetime.today().strftime("%Y-%m-%d %H:%M:%S")))
print("-" * 50)
return wrapper
@logger
def demo_function():
print("Executing task!")
time.sleep(2)
print("Task completed!")
demo_function()
logger(demo_function())
Decorators can also accept arguments, allowing for more flexibility. For example, a sleep time can be passed to control the duration of the task execution.
def logger_args(func):
def wrapper(*args, **kwargs):
print("-" * 50)
print("> Execution started {}".format(datetime.today().strftime("%Y-%m-%d %H:%M:%S")))
func(*args, **kwargs)
print("> Execution completed {}".format(datetime.today().strftime("%Y-%m-%d %H:%M:%S")))
print("-" * 50)
return wrapper
@logger_args
def demo_function_args(sleep_time):
print("Executing task!")
time.sleep(sleep_time)
print("Task completed!")
demo_function_args(2)
In hacking scripts, decorators can serve various purposes, such as slowing down brute-force attempts or validating token status during automated attacks. They enhance code modularity and readability while providing a mechanism for introducing reusable pre and post-processing steps.
Python generators provide an elegant and memory-efficient way to create iterators, allowing the generation of a sequence of items one at a time. Unlike traditional functions, generators use the yield
statement instead of return
, enabling the function to pause and retain its state, facilitating the resumption of execution.
To create a generator, define a function with the yield
statement. Unlike a normal function where return
terminates execution, yield
pauses the function, saving the state and local variable information for future calls. It's important to note that a generator function cannot include a return
statement, as it would terminate execution instead of pausing.
def gen_demo():
n = 1
yield n
n += 1
yield n
n += 1
yield n
test = gen_demo()
print(test)
print(next(test)) # Prints 1
print(next(test)) # Prints 2
print(next(test)) # Prints 3
# Further calls will raise "StopIteration" error
test2 = gen_demo()
for a in test2:
print(a)
We can create generator function with loops.
def xor_static_key(a):
key = 0x5
for i in a:
yield chr(ord(i) ^ key)
for i in xor_static_key("test"):
print(i)
Here we used a generator function to perform static xor of the word "test" with the key of 5.
Similar to lambda functions, generator expressions allow the creation of anonymous generators. The syntax involves using ()
instead of []
brackets.
xor_static_key2 = (chr(ord(i) ^ 0x5) for i in "test")
print(xor_static_key2)
print(next(xor_static_key2))
print(next(xor_static_key2))
for i in xor_static_key2:
print(i)
Generators are particularly useful for memory-intensive tasks and processing large files. Due to their lazy execution, generators only produce items when explicitly requested, avoiding the need to compute the entire sequence upfront. This lazy evaluation makes generators suitable for scenarios where memory optimization is crucial.
Serialization
is the process of translating data structures or objects into a format that can be stored or transmitted, later reconstructed by deserializing the serialized object. This process is essential for preserving the state of something for future use or transmitting data across a network.
One way to perform serialization in Python is by using the pickle
library. This method is useful for creating a binary format with a consistent structure that Python understands. The serialized data can be later deserialized to reconstruct the original object.
Example:
import pickle
hackers = {"neut": 1, "geohot": 100, "neo": 1000}
# Displaying original data
for key, value in hackers.items():
print(key, value)
# Serialized version in binary format
serialized = pickle.dumps(hackers)
print(serialized)
# Deserializing the data
hackers_v2 = pickle.loads(serialized)
print(hackers_v2)
# Displaying deserialized data
for key, value in hackers_v2.items():
print(key, value)
Serialized objects can be stored in a file, facilitating the preservation of saved states, such as a game's progress or the current location in a sequence of items during a brute-force attempt.
Example:
# Storing serialized object in a file
with open("hackers.pickle", "wb") as handle:
pickle.dump(hackers, handle)
# Loading serialized object from the file
with open("hackers.pickle", "rb") as handle:
hackers_v3 = pickle.load(handle)
print(hackers_v3)
- When serializing data, only attributes of objects are stored, not the objects themselves.
- Care must be taken during deserialization to avoid potential
code injection attacks
. - Not all data structures are easily pickled; for instance, dictionaries are straightforward, while generators and lambda functions pose challenges due to their operational nature.
Closures in Python involve the concept of nested functions and local variables. A closure is a function object that remembers values in enclosing scopes, even when the variables are out of scope or removed. Understanding closures requires knowledge of how nested functions and local variables work.
def print_out(a):
print("Outer: {}".format(a))
def print_in():
print("\tInner: {}".format(a))
print_in()
print_out("testing")
Consider what happens if instead of calling the inner function directly, we return it from the outer function.
def print_out(a):
print("Outer: {}".format(a))
def print_in():
print("\tInner: {}".format(a))
return print_in
test = print_out("testing")
test()
In this example, the returned function is assigned to the variable test
. When test
is executed as a function, it remembers the value that was originally supplied to the print_out
function. This binding of data to code in Python is called a closure.
- A closure is a function object.
- It remembers values in enclosing scopes, even if those variables go out of scope or are deleted.
- The outer function does not need to exist for the nested function to execute, as the data is already attached to the code.
del print_out
test()
print(print_out("testing")) # This would result in an error, as the outer function has been deleted.
Even after deleting the outer function, the bound function test
can still be called without errors, demonstrating the independence of the closure.
Closures are useful when a nested function needs to reference a value in an outer scope. They can be applied to avoid global variables, implement callbacks, or when only a few methods need to be implemented in a Python class. When using closures, ensure that the nested function refers to a value in the outer function, and the outer function returns the nested function.
In the realm of scripting, procedural programming
has been the norm—defining data and functions separately. However, as applications grow, maintaining such scripts becomes challenging. Object-Oriented Programming (OOP)
offers a structured approach to overcome these limitations, grouping variables
and methods
into objects
and organizing software
into reusable blueprints called classes
.
- OOP organizes software using
classes
, reusable templates that groupvariables (attributes)
andmethods
. - Instances of classes are created through instantiation, generating class objects with specific attributes and methods.
- Each instance is independent, allowing customization and separation of data.
Consider creating a "Person" class:
class Person:
def __init__(self, name):
self.name = name
def say_name(self):
print(f"My name is {self.name}")
# Instantiate objects
bob = Person("Bob")
alice = Person("Alice")
# Utilize accessible methods and attributes for each instance
bob.say_name()
alice.say_name()
-
Code Organization:
- Grouping related information together makes code shorter and easier to maintain.
- Reduces complexity, leading to improved readability and maintainability.
-
Time Efficiency:
- As applications grow in size and scope, OOP saves time and effort.
-
Key Concepts:
- Inheritance: Leverage existing structures.
- Polymorphism: Enable class-specific behavior.
- Encapsulation: Secure and protect attributes and methods.
- Overloading: Extendible and modular.
Conclusion: Object-Oriented Programming provides a powerful paradigm for designing software systems. It enhances maintainability, readability, and scalability, making it an invaluable approach as applications become more complex.
In a class, the __init__
method is used to initialize an instance of the object. It takes the self
argument, a reference to the object itself. This constructor method is automatically invoked whenever a new object of the class is instantiated.
class Person:
def __init__(self, name, age):
self.name = name # instance attributes
self.age = age
# Create instances of the Person class
bob = Person("Bob", 30)
alice = Person("Alice", 20)
mallory = Person("Mallory", 50)
# Access information about each object
print(bob.name)
print(bob.age)
# Object Manipulation Functions:
print(hasattr(bob, "age"))
print(hasattr(bob, "death"))
print(getattr(bob, "age"))
setattr(bob, "death", 100)
print(getattr(bob, "death"))
delattr(bob, "death") # Delete attribute
# print(getattr(bob, "death")) # Raises AttributeError after deletion
You can add additional functions to objects. For instance, a Person2
class with functions to print details and manipulate age.
class Person2:
def __init__(self, name, age):
self.name = name # instance attributes
self.age = age
def print_name(self):
print(f"My name is {self.name}")
def print_age(self):
print("My age is {}".format(self.age))
def birthday(self):
self.age += 1
# Object usage
bob = Person2("Bob", 30)
bob.print_name()
bob.print_age()
bob.birthday()
bob.print_age()
In addition to per-instance variables, there are class attributes shared by all objects of the class. Changes to class attribute values affect all instances.
class Person3:
wants_to_hack = True
# Usage
bob = Person3("Bob", 30)
alice = Person3("Alice", 20)
print(Person3.wants_to_hack)
print(bob.wants_to_hack)
Person3.wants_to_hack = "No way!"
print(bob.wants_to_hack)
Attributes, objects, or entire classes can be deleted using the del
keyword. There are special built-in class attributes associated with all Python classes.
delattr(bob, "name")
# del Person # Deleting class itself (commented for demonstration)
# print(alice.name) # Still accessible, even after deleting the class
# Special Class Attributes:
print(Person.__dict__) # Dictionary containing class attributes
print(Person.__doc__) # Class documentation string
print(Person.__name__) # Class name
print(Person.__module__) # Module in which the class is defined
class Person:
"Person base class"
def __init__(self, name):
self.name = name
Inheritance is a way of creating a new class by using details of an already existing class without needing to make any changes to the existing class. If classes are similar or have similar functions, you don't need to copy and paste or recreate the entire new code or function; you can derive them from an existing class.
When using inheritance, the new class is referred to as the derived or child class, and the existing class is referred to as the base or parent class.
For example, let's create a parent class Person
. We will then create a child class, Hacker
, which inherits from the base class Person
.
class Person:
"Person base class (docstring for Person)"
wants_to_hack = True
def __init__(self, name, age):
self.name = name
self.age = age
def print_name(self):
print(f"My name is {self.name}")
def print_age(self):
print("My age is {}".format(self.age))
def birthday(self):
self.age += 1
class Hacker(Person):
"Child class Hacker (docstring for Hacker)"
def __init__(self, name, age, cves):
super().__init__(name, age) # Importance of super() to initialize the parent class
self.cves = cves
def print_name(self):
print("My name is {} and I have {} CVEs".format(self.name, self.cves))
def total_cves(self):
return self.cves
Create instances of both the parent and child classes:
bob = Person("Bob", 30)
alice = Hacker("Alice", 21, 5)
Access and manipulate information:
bob.print_name()
alice.print_name()
print(bob.age)
print(alice.age)
bob.birthday()
alice.birthday()
print(bob.age)
print(alice.age)
Access child-specific functions:
print(alice.total_cves())
# print(bob.total_cves()) --> This will throw an error: AttributeError: 'Person' object has no attribute 'total_cves'
Useful functions to check class relationships:
print(issubclass(Hacker, Person)) # Returns true if the given class is a subclass of the parent class.
print(issubclass(Person, Hacker))
print(isinstance(bob, Person)) # Returns true if the object is an instance of the class or its subclass.
print(isinstance(bob, Hacker))
print(isinstance(alice, Person))
print(isinstance(alice, Hacker))
In the child class Hacker
, the super().__init__(name, age)
line is crucial for initializing the parent class (Person
). It calls the constructor of the parent class, allowing the child class to inherit and set up the attributes defined in the parent class.
Without this line, the child class might not properly initialize the attributes from the parent class, leading to unexpected behavior and potential errors. super()
allows seamless integration of the child and parent class attributes during initialization.
Note that inheritance is a powerful OOP idea that enables code reuse and the creation of more modular classes, avoiding the need to rewrite or copy-paste code.
Using classes, we can restrict direct access to methods and variables. This idea of restricting access is encapsulation in OOP.
Encapsulation can prevent accidental modification of data by creating private variables and methods that are more difficult to directly change.
For now, we have only used public declarations of public variables and methods, which means we can access and change everything from outside of the class because by default, all methods and variables of a Python class are public.
class Person:
"Person base class (docstring for Person)"
wants_to_hack = True
def __init__(self, name, age):
self.name = name
self.__age = age
def print_name(self):
print(f"My name is {self.name}")
def print_age(self):
print("My age is {}".format(self.__age))
def birthday(self):
self.__age += 1
bob = Person("age", 30)
print(bob.__age) # This will throw AttributeError
In order to protect the variable in Python, we can add a double underscore in front of the variable name. Now if we try to access it directly, it will throw an AttributeError. To access this variable, OOP typically uses getter and setter methods.
class Person:
"Person base class (docstring for Person)"
wants_to_hack = True
def __init__(self, name, age):
self.name = name
self.__age = age
def get_age(self):
return self.__age
def set_age(self, age):
self.__age = age
def print_name(self):
print(f"My name is {self.name}")
def print_age(self):
print("My age is {}".format(self.__age))
def birthday(self):
self.__age += 1
bob = Person("age", 30)
print(bob.get_age())
bob.set_age(31)
print(bob.get_age())
bob.birthday()
print(bob.get_age())
print(bob.__dict__) # This reveals the internal structure, including private variables
When using encapsulation with Python, we should be using these getter and setter methods to change and access any private data. However, it's crucial to note that in Python, encapsulation is used to prevent accidental modification to data within a class instance. Still, it shouldn't be relied upon for full security.
For example, a determined individual could use the __dict__
method to see all the variables in the object, revealing private variables and potentially modifying them.
print(bob.__dict__) # This reveals all variables, even private ones
bob._Person__age = 50 # Directly changing the private variable (not recommended)
print(bob.get_age())
Note that encapsulation is very useful for preventing accidental modifications to data, but it should not be relied upon for security.
Polymorphism in object-oriented programming (OOP) is the ability to use a common interface for multiple different types. It allows us to use the same function or method even if we pass in different types to that function.
We have already encountered examples of polymorphism in Python:
print(len("string"))
print(len(['l', 'i', 's', 't']))
Here, the len
function is polymorphic, as it can work with different types (strings, lists, etc.).
Now, let's explore polymorphism in the context of classes:
class Person:
"Person base class (docstring for Person)"
wants_to_hack = True
def __init__(self, name, age):
self.name = name
self.age = age
def print_name(self):
print(f"My name is {self.name}")
def print_age(self):
print("My age is {}".format(self.age))
def birthday(self):
self.age += 1
class Hacker(Person):
"Child class Hacker (docstring for Hacker)"
def __init__(self, name, age, cves):
super().__init__(name, age)
self.cves = cves
def print_name(self):
print("My name is {} and I have {} CVEs".format(self.name, self.cves))
def total_cves(self):
return self.cves
bob = Person("bob", 30)
alice = Hacker("alice", 25, 10)
people = [bob, alice]
# Polymorphism in action
for person in people:
person.print_name()
print(type(person))
print("\n")
def obj_dump(object):
object.print_name()
print(object.age)
object.birthday()
print(object.age)
print(object.__class__)
print(object.__class__.__name__)
obj_dump(bob)
print("\n")
obj_dump(alice)
My name is bob
<class '__main__.Person'>
My name is alice and I have 10 CVEs
<class '__main__.Hacker'>
My name is bob
30
31
<class '__main__.Person'>
Person
My name is alice and I have 10 CVEs
25
26
<class '__main__.Hacker'>
Hacker
In this example, we see that polymorphism allows us to use the print_name
function on different types (instances of Person
and Hacker
). The function works seamlessly, and the type of the object is determined dynamically at runtime.
Polymorphism in Python enables us to create functions that can take any object as a parameter, making the code more flexible and adaptable to different scenarios. It plays a crucial role in enabling different types or methods to be used in various situations based on specific use cases of those class instances.
In Python, operator overloading allows changing the meaning of operators depending on the operands used. We have already seen this with the plus operator, which behaves differently when applied to numbers and strings.
print(1 + 1)
print("1" + "1")
Here, we observe the difference between addition and concatenation. Let's explore how operator overloading works with classes.
Python functions that begin with double underscores (e.g., __init__
) are special reserved functions, and there are several built-in Python functions that can be overloaded with our own classes.
For instance, consider what happens by default when we print an instance of the Person
class:
bob = Person("bob", 30)
print(bob)
By default, it prints the object's location in memory. However, we can define a __str__
method to control how objects are printed:
class Person:
"Person base class (docstring for Person)"
wants_to_hack = True
def __init__(self, name, age):
self.name = name
self.age = age
def print_name(self):
print(f"My name is {self.name}")
def print_age(self):
print("My age is {}".format(self.age))
def birthday(self):
self.age += 1
def __str__(self):
return "My name is {} and I am {} years old".format(self.name, self.age)
bob = Person("bob", 30)
print(bob)
Now, when we print bob
, it displays the string we defined in the __str__
method.
Operator overloading also extends to mathematical operators, including addition or subtraction. After implementation, these overloaded methods control what happens when we compare different objects of the same class.
For example, attempting to add two instances of the Person
class initially throws a TypeError
. To define how this addition should work, we can overload the __add__
method:
class Person:
"Person base class (docstring for Person)"
wants_to_hack = True
def __init__(self, name, age):
self.name = name
self.age = age
def print_name(self):
print(f"My name is {self.name}")
def print_age(self):
print("My age is {}".format(self.age))
def birthday(self):
self.age += 1
def __str__(self):
return "My name is {} and I am {} years old".format(self.name, self.age)
def __add__(self, other):
return self.age + other.age
def __lt__(self, other):
return self.age < other.age
bob = Person("bob", 30)
alice = Person("alice", 35)
print(bob + alice)
print(alice + bob)
print(bob < alice)
print(alice < bob)
Here, the __add__
method allows us to sum the ages of different people, and the __lt__
method enables the comparison of ages.
Operator overloading is powerful in Python, enabling the use of built-in operators and methods for classes without the need to define differently named functions for similar purposes. It enhances code readability and conciseness.
In Python, we have covered classes and decorators separately, and now we are going to combine these components by adding decorators to a class.
class Person:
"Person base class (docstring for Person)"
wants_to_hack = True
def __init__(self, name, age):
self.name = name
self.__age = age
def get_age(self):
return self.__age
def set_age(self, age):
self.__age = age
@property
def age(self):
return self.__age
@age.setter
def age(self, age):
self.__age = age
@age.deleter
def age(self):
del self.__age
@classmethod
def wants_to(cls):
return cls.wants_to_hack
@classmethod
def bob_factory(cls):
return cls("bob", 30)
@staticmethod
def static_print():
print("I am the same!")
def print_name(self):
print(f"My name is {self.name}")
def print_age(self):
print("My age is {}".format(self.__age))
def birthday(self):
self.__age += 1
bob = Person("age", 30)
- Definition: The property decorators can be used to declare a method as a property object of the class.
- Usage: In the class
Person
,@property
is used for theage
method. This makesage
a property, allowing us to access it like an attribute without invoking the method. - Benefit: It provides a way to encapsulate the access to a class attribute, allowing for more controlled attribute access.
print(bob.age) # This will throw AttributeError without using @property decorator.
Now, we can access the value of the age
property, and we can't change it directly without using the setter function.
bob.age = 50
print(bob.age)
# Uncommenting the lines below would result in an error, demonstrating the encapsulation.
# del bob.__age
# print(bob.age)
- Definition: The
@classmethod
decorator is a method bound to the class rather than its object. It can only access class method attributes, not wider per-instance attributes. - Usage: In the
Person
class, thewants_to
andbob_factory
methods are decorated with@classmethod
. - Benefit: It allows the creation of class-level methods that can be called on the class itself.
print(Person.wants_to())
We can use class methods as a type of factory to create instances of the class.
bob1 = Person.bob_factory()
bob2 = Person.bob_factory()
bob1.print_name()
bob2.print_name()
- Definition: The
@staticmethod
decorator lets us define a static method in a class. Static methods can't access class attributes or per-instance attributes. - Usage: In the
Person
class, thestatic_print
method is decorated with@staticmethod
. - Benefit: It allows the creation of methods that don't need a class instance and can be called by the class or by an instance of the class.
Person.static_print()
bob1.static_print()
bob2.static_print()
We can see the same output for both the class and the instance.
- Static methods can be useful when we don't want the subclass to change or override some specific method implementation.
- Combining decorators and classes enables programmers to create and control more complex classes and object instances by centralizing and reusing code and structures.
-
Application Programming Interface (API)
- A mechanism to interface with the Windows OS.
- Mostly described by the Microsoft Developer Network (MSDN).
- Some functions are not officially documented.
-
Components of the Windows API
- Comprised of functions, structures, and constants.
- Includes Windows-defined data types.
-
Dynamic Nature
- The Windows API is not static; it can change and expand between releases of Windows.
- The official implementation is on your Windows machine, in DLLs (Dynamic Link Libraries).
- Example DLLs:
kernel32.dll
,user32.dll
,ntdll.dll
in the Windows system directory.
- Example DLLs:
-
Python Ctypes Module
- Enables wrapping Python around C.
- Allows "Speaking C," facilitating interfacing with the Windows API.
-
Generic Windows API Functions
- We'll cover some generic Windows API functions.
-
Focus on Hacker-Related API Calls
- Exploring API calls interesting to hackers.
- Specific functions:
OpenProcess
,CreateRemoteThread
,WriteProcessMemory
from within Python.
This is python-centric notes; however, when interfacing with the Windows API, we need to have an understanding of C.
- C is a lower-level programming language.
- C is compiled (and faster), while Python is interpreted (and slower).
- C requires you to specify the type of data.
- C has fewer built-in standard functions.
- C is comparatively more challenging than Python because Python abstracts away more complex components, such as memory management.
In C, we have a concept called pointers and structures.
- When you create a variable, that variable has a memory address.
- Pointers are variables that store addresses, not values.
- Structures (structs) are collections of grouped variables.
- A structure groups variables under a single type, similar to Python objects.
The ctypes
module, included in the library, is a foreign function library for Python.
ctypes
provides C-compatible data types, allowing us to call functions in DLLs or shared libraries directly from Python.
So, let's start by working with ctypes
data types by importing ctypes
.
from ctypes import *
As we know, when declaring variables in Python, we don't need to specify their types. This makes Python scripting quicker and easier but can also cause problems. For example, if we try to concatenate a string and a number, Python can't guess what we're trying to do. In other languages, such as C, we need to specify the type before using variables.
Let's start by creating variables of a ctypes
boolean type:
b0 = c_bool(0)
b1 = c_bool(1)
print(b0)
print(type(b0))
print(b0.value)
print(b1)
print(type(b1))
print(b1.value)
We're not limited to booleans; we can also work with unsigned integers, for example:
i0 = c_uint(-1)
print(i0.value)
We can also create strings, which are null-terminated char pointers:
c0 = c_char_p(b"test1")
print(c0.value)
When changing the values of pointer-type instances, we are actually changing the memory location the variable is pointing to, not the actual content of that memory block:
p0 = create_string_buffer(5)
print(p0)
print(p0.raw)
print(p0.value)
p0.value = b"a"
print(p0)
print(p0.raw)
print(p0.value)
Here, we can see the address remains the same, but the value has changed.
In case the function you are working with expects a unicode buffer, you need to use a similar create unicode buffer function.
We only demonstrated a few of the standard data types here. In the official ctypes
documentation, you can find an extensive list of C data types available in Python:
Pointer instances are created by calling the pointer
function on a ctypes
type:
i = c_int(42)
pi = pointer(i)
print(i)
print(pi)
print(pi.contents)
These pointers are useful because sometimes a C function will expect a pointer to a specific data type as an input parameter, known as passing by reference. Passing by reference can be used if the function wants to write to that location or if the data being passed is too large to be passed by its value. To help with interfacing these functions, we can use ctypes
' byref
or pointer
functions to pass parameters by reference:
p0 = create_string_buffer(5)
p0.value = b"a"
pt = byref(p0)
print(pt)
Now that we have this reference, we can also use cast
. The cast
returns a new instance of the char
pointer type, which points to pt
and can be used to store the actual data stored at that address. For example:
print(cast(pt, c_char_p).value)
print(cast(pt, POINTER(c_int)).contents)
print(ord('a'))
Now that we have an understanding of some data types, let's move on to structures. Structures are derived from the structure-based class defined within the ctypes
module. Each subclass must define a field
attribute, and this attribute is a list of tuples containing the field's name and the field's type. For example, we can recreate a "Person" class from the OOP concept covered earlier as a ctypes
structure:
class PERSON(Structure):
"""docstring for PERSON"""
_fields_ = [("name", c_char_p),
("age", c_int)]
bob = PERSON(b"bob", 30)
print(bob.name)
print(bob.age)
alice = PERSON(b"alice", 20)
print(alice.name)
print(alice.age)
We already know how to work with lists in Python, but when using a list or an array of ctypes
, we need to set up or manage space a little differently. We first need to multiply a data type by the number of elements we want to have in the array. For example, we might want to have a person array:
person_array_t = PERSON * 3
print(person_array_t)
person_array = person_array_t()
person_array[0] = PERSON(b"bob", 30)
person_array[1] = PERSON(b"alice", 20)
person_array[2] = PERSON(b"mallory", 50)
# If we try to add another person to the array, it will throw an error (IndexError: invalid index) because there is not enough space for the 4th person unless we change the type.
# For example:
# person_array[3] = PERSON(b"OHNO", 50)
for person in person_array:
print(person)
print(person.age)
print(person.age)
ctypes
is a really useful and powerful library that enables us to use and interface with lower-level C data types directly from within Python. ctypes
might be daunting if you haven't worked with a lower-level language before, but the documentation is verbose, and ctypes
will enable you to easily work and interface with native code from within Python.