A Python decorator is a function that modifies the behavior of another function. It's used to add functionality to existing code. For example, if you have a function that prints a message, a decorator can be used to log when the function was called. Example:
def logger(func): def wrapper(*args, **kwargs): print('Function called') return func(*args, **kwargs) return wrapper @logger def hello(): print('Hello, world!') hello() Output: Function called Hello, world!
Generators are functions that return an iterable set of items, one at a time, in a special way. They use `yield` instead of `return`. Generators are useful for processing large datasets or streams of data where you don’t want to store everything in memory. Example:
def count_up_to(max): count = 1 while count <= max: yield count count += 1 for number in count_up_to(3): print(number) Output: 1 2 3
`copy` creates a shallow copy of an object, meaning it copies the object but not the objects inside it. `deepcopy` creates a deep copy, meaning it copies the object and all objects inside it recursively. This is useful when you want to modify the copy without affecting the original. Example:
import copy original_list = [1, [2, 3]] shallow_copy = copy.copy(original_list) deep_copy = copy.deepcopy(original_list) shallow_copy[1].append(4) print(original_list) Output: [1, [2, 3, 4]] print(deep_copy) Output: [1, [2, 3]]
Python uses automatic memory management, which includes garbage collection and reference counting. When objects are no longer in use, Python automatically frees up memory. Example:
import gc def create_object(): my_list = [1, 2, 3] create_object() gc.collect() Output: Memory freed by garbage collector
`__str__` is used to define a user-friendly string representation of an object, while `__repr__` is used for debugging and development purposes, providing a detailed string representation of an object. Example:
class Person: def __init__(self, name, age): self.name = name self.age = age def __str__(self): return f'Person(name={self.name}, age={self.age})' def __repr__(self): return f'Person(name={self.name!r}, age={self.age!r})' person = Person('John', 30) print(person) Output: Person(name=John, age=30) print(repr(person)) Output: Person(name='John', age=30)
The GIL is a mechanism that prevents multiple native threads from executing Python bytecodes at once. This can be a limitation for CPU-bound multi-threaded programs in Python, as it essentially makes sure only one thread executes Python code at a time. Example:
import threading def count(): for i in range(1000000): pass thread1 = threading.Thread(target=count) thread2 = threading.Thread(target=count) thread1.start() thread2.start() thread1.join() thread2.join() The threads execute but only one thread runs Python code at a time due to the GIL.
Metaclasses are classes of classes. They define how classes behave. In Python, `type` is the default metaclass, but you can create your own metaclasses to customize class creation and behavior. Example:
class Meta(type): def __new__(cls, name, bases, dct): print('Creating a new class') return super(Meta, cls). __new__(cls, name, bases, dct) class MyClass(metaclass=Meta): pass Output: Creating a new class
Optimizing Python code involves using efficient algorithms and data structures, minimizing the use of global variables, leveraging built-in functions and libraries, and profiling the code to identify bottlenecks. Example:
def optimize_example(data): return [x * 2 for x in data] List comprehension is more efficient than using a loop for the same task
Decorators are used to modify the behavior of functions or methods without changing their actual code. They are often used for logging, access control, and performance measurement. Example:
def my_decorator(func): def wrapper(*args, **kwargs): print('Something is happening before the function is called.') result = func(*args, **kwargs) print('Something is happening after the function is called.') return result return wrapper @my_decorator def say_hello(): print('Hello!') say_hello() Output: Something is happening before the function is called. Hello! Something is happening after the function is called.
Python handles memory management through automatic garbage collection and reference counting. The garbage collector identifies and frees up memory that is no longer in use. Example:
import gc gc.collect() Forces garbage collection
`copy` creates a shallow copy of an object, meaning only the top-level elements are copied. `deepcopy` creates a deep copy, where all nested objects are copied as well. Example:
import copy original = {'a': [1, 2, 3]} shallow_copy = copy.copy(original) deep_copy = copy.deepcopy(original) shallow_copy['a'].append(4) Changes affect both shallow_copy and original deep_copy['a'].append(5) Only deep_copy is affected
Exceptions in Python are handled using `try`, `except`, `else`, and `finally` blocks. You write code inside the `try` block, catch exceptions in the `except` block, run code if no exception occurs in the `else` block, and execute code regardless of whether an exception occurred in the `finally` block. Example:
try: result = 10 / 0 except ZeroDivisionError: print('Cannot divide by zero.') else: print('Division successful.') finally: print('Execution complete.') Output: Cannot divide by zero. Execution complete.
Generators are special functions that return an iterator. They use `yield` instead of `return` to produce a series of values lazily. This means they produce items one at a time and are more memory-efficient for large data sets. Example:
def count_up_to(n): i = 1 while i <= n: yield i i += 1 for number in count_up_to(5): print(number) Output: 1 2 3 4 5
In Python, `is` checks for object identity, meaning it returns `True` if both variables point to the same object in memory. `==` checks for value equality, meaning it returns `True` if the values of both variables are equal, even if they are different objects in memory. Example:
a = [1, 2, 3] b = [1, 2, 3] print(a == b) True, because values are equal print(a is b) False, because they are different objects
Python has several built-in data types, including: - `int` (integer) - `float` (floating-point number) - `str` (string) - `list` (list) - `tuple` (tuple) - `set` (set) - `dict` (dictionary) Example:
number = 10 int pi = 3.14 float text = 'hello' str items = [1, 2, 3] list coordinates = (1, 2) tuple unique_numbers = {1, 2, 3} set person = {'name': 'Alice', 'age': 25} dict
List comprehensions provide a concise way to create lists. They consist of an expression followed by a `for` loop inside square brackets. They can also include `if` conditions. Example:
squares = [x ** 2 for x in range(5)] Output: [0, 1, 4, 9, 16]
A lambda function is an anonymous function defined using the `lambda` keyword. It can have any number of arguments but only one expression. The result of the expression is returned. Example:
add = lambda x, y: x + y print(add(5, 3)) Output: 8
`append` adds its argument as a single element to the end of a list, while `extend` iterates over its argument adding each element to the list. Example:
lst = [1, 2, 3] lst.append([4, 5]) print(lst) Output: [1, 2, 3, [4, 5]] lst = [1, 2, 3] lst.extend([4, 5]) print(lst) Output: [1, 2, 3, 4, 5]
Exceptions in Python are handled using `try`, `except`, `else`, and `finally` blocks. `try` contains the code that might raise an exception, `except` handles the exception, `else` runs if no exception occurs, and `finally` runs code that should execute no matter what. Example:
try: print(1 / 0) except ZeroDivisionError: print('Cannot divide by zero') finally: print('Execution complete') Output: Cannot divide by zero Execution complete
A decorator is a function that takes another function and extends its behavior without explicitly modifying it. They are used to wrap functions with additional functionality. Example:
def my_decorator(func): def wrapper(): print('Something is happening before the function is called.') func() print('Something is happening after the function is called.') return wrapper @my_decorator def say_hello(): print('Hello!') say_hello() Output: Something is happening before the function is called. Hello! Something is happening after the function is called.
The `self` keyword represents the instance of the class and is used to access variables and methods associated with the instance. It differentiates between instance attributes and method arguments. Example:
class Person: def __init__(self, name, age): self.name = name self.age = age person = Person('Alice', 30) print(person.name) Output: Alice
Python handles memory management automatically using a built-in garbage collector. It keeps track of objects and their references, and when an object is no longer needed, it is automatically deallocated. Example:
import gc def create_large_object(): large_list = [i for i in range(1000000)] create_large_object() gc.collect() Force garbage collection
Python uses access control mechanisms like public, protected, and private to encapsulate data: - **Public**: Accessible from anywhere. - **Protected**: Indicated by a single underscore `_`, meant for internal use. - **Private**: Indicated by double underscores `__`, not directly accessible from outside the class. Example:
class MyClass: def __init__(self): self.public_var = 'I am public' self._protected_var = 'I am protected' self.__private_var = 'I am private' obj = MyClass() print(obj.public_var) Output: I am public print(obj._protected_var) Output: I am protected print(obj.__private_var) AttributeError
A lambda function is an anonymous function defined using the `lambda` keyword. It can have any number of arguments but only one expression. It returns the result of the expression. Example:
add = lambda x, y: x + y print(add(5, 3)) Output: 8
Python has several built-in data types, including: - **Numbers**: `int`, `float`, `complex` - **Sequences**: `list`, `tuple`, `range` - **Mappings**: `dict` - **Sets**: `set`, `frozenset` - **Boolean**: `bool` - **None**: `NoneType` Example:
int_var = 10 float_var = 10.5 list_var = [1, 2, 3] dict_var = {'a': 1, 'b': 2} print(int_var, float_var, list_var, dict_var) Output: 10 10.5 [1, 2, 3] {'a': 1, 'b': 2}
You can merge two dictionaries using the `update()` method or using the `**` unpacking operator (Python 3.5+). Example:
dict1 = {'a': 1} dict2 = {'b': 2} dict1.update(dict2) print(dict1) Output: {'a': 1, 'b': 2} dict3 = {**dict1, **dict2} print(dict3) Output: {'a': 1, 'b': 2}
String formatting in Python can be done using the `%` operator, `str.format()` method, or f-strings (Python 3.6+). Example:
Using % operator name = 'Alice' print('Hello, %s!' % name) Output: Hello, Alice! Using str.format() print('Hello, {}!'.format(name)) Output: Hello, Alice! Using f-strings print(f'Hello, {name}!') Output: Hello, Alice!
The `copy` module provides `copy()` for shallow copying and `deepcopy()` for deep copying. A shallow copy creates a new object but does not create copies of nested objects. A deep copy creates a new object and recursively copies all objects found in the original object. Example:
import copy original = {'a': 1, 'b': {'c': 2} shallow_copy = copy.copy(original) deep_copy = copy.deepcopy(original) print(shallow_copy, deep_copy) Output: {'a': 1, 'b': {'c': 2}} {'a': 1, 'b': {'c': 2}}
Exceptions in Python are handled using `try`, `except`, `else`, and `finally` blocks. The `try` block contains code that might raise an exception. The `except` block contains code to handle the exception. The `else` block executes if no exception occurs, and the `finally` block executes code regardless of whether an exception occurred. Example:
try: print('Trying...') result = 10 / 0 except ZeroDivisionError: print('Cannot divide by zero') else: print('Division successful') finally: print('End of try-except') Output: Trying... Cannot divide by zero End of try-except